mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Merge branch 'main' into new_package_relay
This commit is contained in:
commit
c281654e97
@ -178,9 +178,7 @@ object Client : RelayPool.Listener {
|
||||
subscriptions = subscriptions.minus(subscriptionId)
|
||||
}
|
||||
|
||||
fun isActive(subscriptionId: String): Boolean {
|
||||
return subscriptions.contains(subscriptionId)
|
||||
}
|
||||
fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onEvent(
|
||||
@ -204,9 +202,9 @@ object Client : RelayPool.Listener {
|
||||
) {
|
||||
// Releases the Web thread for the new payload.
|
||||
// May need to add a processing queue if processing new events become too costly.
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
|
||||
}
|
||||
// GlobalScope.launch(Dispatchers.Default) {
|
||||
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
|
||||
// }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@ -281,21 +279,15 @@ object Client : RelayPool.Listener {
|
||||
listeners = listeners.plus(listener)
|
||||
}
|
||||
|
||||
fun isSubscribed(listener: Listener): Boolean {
|
||||
return listeners.contains(listener)
|
||||
}
|
||||
fun isSubscribed(listener: Listener): Boolean = listeners.contains(listener)
|
||||
|
||||
fun unsubscribe(listener: Listener) {
|
||||
listeners = listeners.minus(listener)
|
||||
}
|
||||
|
||||
fun allSubscriptions(): Map<String, List<TypedFilter>> {
|
||||
return subscriptions
|
||||
}
|
||||
fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
|
||||
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> {
|
||||
return subscriptions[subId] ?: emptyList()
|
||||
}
|
||||
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
|
||||
|
||||
abstract class Listener {
|
||||
/** A new message was received */
|
||||
|
@ -12,8 +12,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 379
|
||||
versionName "0.88.2"
|
||||
versionCode 381
|
||||
versionName "0.88.4"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"2a34cbadd03212c8162e1ff896ba12641821088a2ec8d5e40d54aa80c0510800\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
@ -328,10 +328,10 @@ object LocalPreferences {
|
||||
)
|
||||
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
|
||||
|
||||
if (account.showSensitiveContent == null) {
|
||||
if (account.showSensitiveContent.value == null) {
|
||||
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
|
||||
} else {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
|
||||
}
|
||||
|
||||
putString(
|
||||
@ -597,7 +597,7 @@ object LocalPreferences {
|
||||
backupContactList = latestContactList,
|
||||
proxy = proxy,
|
||||
proxyPort = proxyPort,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
showSensitiveContent = MutableStateFlow(showSensitiveContent),
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
|
@ -26,7 +26,6 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
@ -195,7 +194,7 @@ class Account(
|
||||
var backupContactList: ContactListEvent? = null,
|
||||
var proxy: Proxy? = null,
|
||||
var proxyPort: Int = 9050,
|
||||
var showSensitiveContent: Boolean? = null,
|
||||
var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
var filterSpamFromStrangers: Boolean = true,
|
||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
||||
@ -203,7 +202,7 @@ class Account(
|
||||
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
|
||||
|
||||
data class PaymentRequest(
|
||||
val relayUrl: String,
|
||||
@ -239,6 +238,8 @@ class Account(
|
||||
getPrivateOutboxRelayListFlow(),
|
||||
userProfile().flow().relays.stateFlow,
|
||||
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
|
||||
checkNotInMainThread()
|
||||
|
||||
val baseRelaySet = activeRelays() ?: convertLocalRelays()
|
||||
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
|
||||
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet
|
||||
@ -395,6 +396,7 @@ class Account(
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
|
||||
userProfile().flow().follows.stateFlow.transformLatest {
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
LiveFollowLists(
|
||||
it.user.cachedFollowingKeySet(),
|
||||
@ -431,6 +433,7 @@ class Account(
|
||||
peopleListFollowsSource: Flow<ListNameNotePair>,
|
||||
): Flow<LiveFollowLists?> =
|
||||
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
|
||||
checkNotInMainThread()
|
||||
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
|
||||
emit(null)
|
||||
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
|
||||
@ -537,10 +540,11 @@ class Account(
|
||||
|
||||
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
|
||||
combineTransform(
|
||||
live.asFlow(),
|
||||
transientHiddenUsers,
|
||||
showSensitiveContent,
|
||||
getBlockListNote().flow().metadata.stateFlow,
|
||||
getMuteListNote().flow().metadata.stateFlow,
|
||||
) { localLive, blockList, muteList ->
|
||||
) { transientHiddenUsers, showSensitiveContent, blockList, muteList ->
|
||||
checkNotInMainThread()
|
||||
|
||||
val resultBlockList =
|
||||
@ -569,8 +573,8 @@ class Account(
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = (resultBlockList.users + resultMuteList.users),
|
||||
hiddenWords = hiddenWords,
|
||||
spammers = localLive.account.transientHiddenUsers,
|
||||
showSensitiveContent = localLive.account.showSensitiveContent,
|
||||
spammers = transientHiddenUsers,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
),
|
||||
)
|
||||
}.stateIn(
|
||||
@ -579,8 +583,8 @@ class Account(
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = setOf(),
|
||||
hiddenWords = setOf(),
|
||||
spammers = transientHiddenUsers,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
spammers = transientHiddenUsers.value,
|
||||
showSensitiveContent = showSensitiveContent.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -633,7 +637,9 @@ class Account(
|
||||
filterSpamFromStrangers = filterSpam
|
||||
LocalCache.antiSpam.active = filterSpamFromStrangers
|
||||
if (!filterSpamFromStrangers) {
|
||||
transientHiddenUsers = setOf()
|
||||
transientHiddenUsers.update {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
@ -2414,7 +2420,9 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
|
||||
transientHiddenUsers.update {
|
||||
it - pubkeyHex
|
||||
}
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
@ -2926,7 +2934,9 @@ class Account(
|
||||
}
|
||||
|
||||
fun updateShowSensitiveContent(show: Boolean?) {
|
||||
showSensitiveContent = show
|
||||
showSensitiveContent.update {
|
||||
show
|
||||
}
|
||||
saveable.invalidateData()
|
||||
live.invalidateData()
|
||||
}
|
||||
@ -2965,10 +2975,12 @@ class Account(
|
||||
// imports transient blocks due to spam.
|
||||
LocalCache.antiSpam.liveSpam.observeForever {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
it.cache.spamMessages.snapshot().values.forEach {
|
||||
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
|
||||
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
|
||||
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
|
||||
it.cache.spamMessages.snapshot().values.forEach { spammer ->
|
||||
if (spammer.pubkeyHex !in transientHiddenUsers.value && spammer.duplicatedMessages.size >= 5) {
|
||||
if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) {
|
||||
transientHiddenUsers.update {
|
||||
it + spammer.pubkeyHex
|
||||
}
|
||||
live.invalidateData()
|
||||
}
|
||||
}
|
||||
|
@ -241,33 +241,25 @@ object LocalCache {
|
||||
return users.get(key)
|
||||
}
|
||||
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? {
|
||||
return addressables.get(key)
|
||||
}
|
||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key)
|
||||
|
||||
fun getNoteIfExists(key: String): Note? {
|
||||
return addressables.get(key) ?: notes.get(key)
|
||||
}
|
||||
fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key)
|
||||
|
||||
fun getChannelIfExists(key: String): Channel? {
|
||||
return channels.get(key)
|
||||
}
|
||||
fun getChannelIfExists(key: String): Channel? = channels.get(key)
|
||||
|
||||
fun getNoteIfExists(event: Event): Note? {
|
||||
return if (event is AddressableEvent) {
|
||||
fun getNoteIfExists(event: Event): Note? =
|
||||
if (event is AddressableEvent) {
|
||||
getAddressableNoteIfExists(event.addressTag())
|
||||
} else {
|
||||
getNoteIfExists(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNote(event: Event): Note {
|
||||
return if (event is AddressableEvent) {
|
||||
fun getOrCreateNote(event: Event): Note =
|
||||
if (event is AddressableEvent) {
|
||||
getOrCreateAddressableNote(event.address())
|
||||
} else {
|
||||
getOrCreateNote(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkGetOrCreateNote(key: String): Note? {
|
||||
checkNotInMainThread()
|
||||
@ -348,8 +340,8 @@ object LocalCache {
|
||||
return HexValidator.isHex(key)
|
||||
}
|
||||
|
||||
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
|
||||
return try {
|
||||
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? =
|
||||
try {
|
||||
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
|
||||
if (addr != null) {
|
||||
getOrCreateAddressableNote(addr)
|
||||
@ -360,7 +352,6 @@ object LocalCache {
|
||||
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
|
||||
// checkNotInMainThread()
|
||||
@ -395,6 +386,10 @@ object LocalCache {
|
||||
val newUserMetadata = event.contactMetaData()
|
||||
if (newUserMetadata != null) {
|
||||
oldUser.updateUserInfo(newUserMetadata, event)
|
||||
if (relay != null) {
|
||||
oldUser.addRelayBeingUsed(relay, event.createdAt)
|
||||
oldUser.latestMetadataRelay = relay.url
|
||||
}
|
||||
}
|
||||
// Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex()} ${oldUser.toBestDisplayName()} from ${relay?.url}")
|
||||
} else {
|
||||
@ -428,11 +423,11 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun formattedDateTime(timestamp: Long): String {
|
||||
return Instant.ofEpochSecond(timestamp)
|
||||
fun formattedDateTime(timestamp: Long): String =
|
||||
Instant
|
||||
.ofEpochSecond(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: TextNoteEvent,
|
||||
@ -755,8 +750,8 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun computeReplyTo(event: Event): List<Note> {
|
||||
return when (event) {
|
||||
fun computeReplyTo(event: Event): List<Note> =
|
||||
when (event) {
|
||||
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
@ -805,7 +800,6 @@ object LocalCache {
|
||||
|
||||
else -> emptyList<Note>()
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: PollNoteEvent,
|
||||
@ -1218,7 +1212,8 @@ object LocalCache {
|
||||
if (deletionIndex.add(event)) {
|
||||
var deletedAtLeastOne = false
|
||||
|
||||
event.deleteEvents()
|
||||
event
|
||||
.deleteEvents()
|
||||
.mapNotNull { getNoteIfExists(it) }
|
||||
.forEach { deleteNote ->
|
||||
// must be the same author
|
||||
@ -2030,16 +2025,16 @@ object LocalCache {
|
||||
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
|
||||
checkNotInMainThread()
|
||||
|
||||
return addressables.filter { _, it ->
|
||||
val noteEvent = it.event
|
||||
(
|
||||
noteEvent is StatusEvent &&
|
||||
noteEvent.pubKey == user.pubkeyHex &&
|
||||
!noteEvent.isExpired() &&
|
||||
noteEvent.content.isNotBlank()
|
||||
)
|
||||
}
|
||||
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
|
||||
return addressables
|
||||
.filter { _, it ->
|
||||
val noteEvent = it.event
|
||||
(
|
||||
noteEvent is StatusEvent &&
|
||||
noteEvent.pubKey == user.pubkeyHex &&
|
||||
!noteEvent.isExpired() &&
|
||||
noteEvent.content.isNotBlank()
|
||||
)
|
||||
}.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
.toImmutableList()
|
||||
}
|
||||
@ -2066,9 +2061,7 @@ object LocalCache {
|
||||
|
||||
val modificationCache = LruCache<HexKey, List<Note>>(20)
|
||||
|
||||
fun cachedModificationEventsForNote(note: Note): List<Note>? {
|
||||
return modificationCache[note.idHex]
|
||||
}
|
||||
fun cachedModificationEventsForNote(note: Note): List<Note>? = modificationCache[note.idHex]
|
||||
|
||||
suspend fun findLatestModificationForNote(note: Note): List<Note> {
|
||||
checkNotInMainThread()
|
||||
@ -2082,11 +2075,12 @@ object LocalCache {
|
||||
val time = TimeUtils.now()
|
||||
|
||||
val newNotes =
|
||||
notes.filter { _, item ->
|
||||
val noteEvent = item.event
|
||||
notes
|
||||
.filter { _, item ->
|
||||
val noteEvent = item.event
|
||||
|
||||
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
||||
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
noteEvent is TextNoteModificationEvent && noteEvent.pubKey == originalAuthor && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time)
|
||||
}.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
|
||||
modificationCache.put(note.idHex, newNotes)
|
||||
|
||||
@ -2210,9 +2204,11 @@ object LocalCache {
|
||||
note.event is GenericRepostEvent
|
||||
) &&
|
||||
note.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
|
||||
note.liveSet?.isInUse() != true && // don't delete if observing.
|
||||
note.liveSet?.isInUse() != true &&
|
||||
// don't delete if observing.
|
||||
note.author?.pubkeyHex !in
|
||||
accounts && // don't delete if it is the logged in account
|
||||
accounts &&
|
||||
// don't delete if it is the logged in account
|
||||
note.event?.isTaggedUsers(accounts) !=
|
||||
true // don't delete if it's a notification to the logged in user
|
||||
}
|
||||
@ -2308,8 +2304,7 @@ object LocalCache {
|
||||
?.hiddenUsers
|
||||
?.map { userHex ->
|
||||
(notes.filter { _, it -> it.event?.pubKey() == userHex } + addressables.filter { _, it -> it.event?.pubKey() == userHex }).toSet()
|
||||
}
|
||||
?.flatten()
|
||||
}?.flatten()
|
||||
?: emptyList()
|
||||
|
||||
toBeRemoved.forEach {
|
||||
@ -2596,8 +2591,8 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasConsumed(notificationEvent: Event): Boolean {
|
||||
return if (notificationEvent is AddressableEvent) {
|
||||
fun hasConsumed(notificationEvent: Event): Boolean =
|
||||
if (notificationEvent is AddressableEvent) {
|
||||
val note = addressables.get(notificationEvent.addressTag())
|
||||
val noteEvent = note?.event
|
||||
noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt()
|
||||
@ -2605,7 +2600,6 @@ object LocalCache {
|
||||
val note = notes.get(notificationEvent.id)
|
||||
note?.event != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@ -2617,8 +2611,7 @@ class LocalCacheLiveData {
|
||||
private val bundler = BundledInsert<Note>(1000, Dispatchers.IO)
|
||||
|
||||
fun invalidateData(newNote: Note) {
|
||||
bundler.invalidateList(newNote) {
|
||||
bundledNewNotes ->
|
||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||
_newEventBundles.emit(bundledNewNotes)
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ enum class ConnectivityType(
|
||||
val resourceId: Int,
|
||||
) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
WIFI_ONLY(true, 1, R.string.connectivity_type_wifi_only),
|
||||
WIFI_ONLY(true, 1, R.string.connectivity_type_unmetered_wifi_only),
|
||||
NEVER(false, 2, R.string.connectivity_type_never),
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,9 @@ import com.vitorpamplona.ammolite.relays.Relay
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Lud06
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
@ -49,10 +51,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Stable
|
||||
class User(val pubkeyHex: String) {
|
||||
class User(
|
||||
val pubkeyHex: String,
|
||||
) {
|
||||
var info: UserMetadata? = null
|
||||
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var latestMetadataRelay: String? = null
|
||||
var latestContactList: ContactListEvent? = null
|
||||
var latestBookmarkList: BookmarkListEvent? = null
|
||||
|
||||
@ -76,7 +81,16 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex()
|
||||
|
||||
fun toNostrUri() = "nostr:${pubkeyNpub()}"
|
||||
fun toNProfile(): String {
|
||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent)?.writeRelays()
|
||||
|
||||
return Nip19Bech32.createNProfile(
|
||||
pubkeyHex,
|
||||
relayList?.take(3) ?: listOfNotNull(latestMetadataRelay),
|
||||
)
|
||||
}
|
||||
|
||||
fun toNostrUri() = "nostr:${toNProfile()}"
|
||||
|
||||
override fun toString(): String = pubkeyHex
|
||||
|
||||
@ -96,17 +110,11 @@ class User(val pubkeyHex: String) {
|
||||
return firstName
|
||||
}
|
||||
|
||||
fun toBestDisplayName(): String {
|
||||
return info?.bestName() ?: pubkeyDisplayHex()
|
||||
}
|
||||
fun toBestDisplayName(): String = info?.bestName() ?: pubkeyDisplayHex()
|
||||
|
||||
fun nip05(): String? {
|
||||
return info?.nip05
|
||||
}
|
||||
fun nip05(): String? = info?.nip05
|
||||
|
||||
fun profilePicture(): String? {
|
||||
return info?.picture
|
||||
}
|
||||
fun profilePicture(): String? = info?.picture
|
||||
|
||||
fun updateBookmark(event: BookmarkListEvent) {
|
||||
if (event.id == latestBookmarkList?.id) return
|
||||
@ -132,10 +140,18 @@ class User(val pubkeyHex: String) {
|
||||
// Update Followers of the past user list
|
||||
// Update Followers of the new contact list
|
||||
(oldContactListEvent)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.invalidateData()
|
||||
}
|
||||
(latestContactList)?.unverifiedFollowKeySet()?.forEach {
|
||||
LocalCache.getUserIfExists(it)?.liveSet?.innerFollowers?.invalidateData()
|
||||
LocalCache
|
||||
.getUserIfExists(it)
|
||||
?.liveSet
|
||||
?.innerFollowers
|
||||
?.invalidateData()
|
||||
}
|
||||
|
||||
liveSet?.innerRelays?.invalidateData()
|
||||
@ -198,25 +214,19 @@ class User(val pubkeyHex: String) {
|
||||
return amount
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> {
|
||||
return reports[user] ?: emptySet()
|
||||
}
|
||||
fun reportsBy(user: User): Set<Note> = reports[user] ?: emptySet()
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int {
|
||||
return reports.count { it.key.pubkeyHex in users }
|
||||
}
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int = reports.count { it.key.pubkeyHex in users }
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> {
|
||||
return reports
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> =
|
||||
reports
|
||||
.mapNotNull {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom {
|
||||
@ -235,9 +245,7 @@ class User(val pubkeyHex: String) {
|
||||
return getOrCreatePrivateChatroom(key)
|
||||
}
|
||||
|
||||
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom {
|
||||
return privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
|
||||
}
|
||||
private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key)
|
||||
|
||||
fun addMessage(
|
||||
room: ChatroomKey,
|
||||
@ -326,13 +334,9 @@ class User(val pubkeyHex: String) {
|
||||
liveSet?.innerMetadata?.invalidateData()
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean {
|
||||
return latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
}
|
||||
fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false
|
||||
|
||||
fun isFollowingHashtag(tag: String): Boolean {
|
||||
return latestContactList?.isTaggedHash(tag) ?: false
|
||||
}
|
||||
fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false
|
||||
|
||||
fun isFollowingHashtagCached(tag: String): Boolean {
|
||||
return latestContactList?.verifiedFollowTagSet?.let {
|
||||
@ -362,37 +366,21 @@ class User(val pubkeyHex: String) {
|
||||
?: false
|
||||
}
|
||||
|
||||
fun transientFollowCount(): Int? {
|
||||
return latestContactList?.unverifiedFollowKeySet()?.size
|
||||
}
|
||||
fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size
|
||||
|
||||
suspend fun transientFollowerCount(): Int {
|
||||
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun cachedFollowingKeySet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingKeySet(): Set<HexKey> = latestContactList?.verifiedFollowKeySet ?: emptySet()
|
||||
|
||||
fun cachedFollowingTagSet(): Set<String> {
|
||||
return latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingTagSet(): Set<String> = latestContactList?.verifiedFollowTagSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingGeohashSet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowGeohashSet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingGeohashSet(): Set<HexKey> = latestContactList?.verifiedFollowGeohashSet ?: emptySet()
|
||||
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
|
||||
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
}
|
||||
fun cachedFollowingCommunitiesSet(): Set<HexKey> = latestContactList?.verifiedFollowCommunitySet ?: emptySet()
|
||||
|
||||
fun cachedFollowCount(): Int? {
|
||||
return latestContactList?.verifiedFollowKeySet?.size
|
||||
}
|
||||
fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size
|
||||
|
||||
suspend fun cachedFollowerCount(): Int {
|
||||
return LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
}
|
||||
suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
|
||||
|
||||
fun hasSentMessagesTo(key: ChatroomKey?): Boolean {
|
||||
val messagesToUser = privateChatrooms[key] ?: return false
|
||||
@ -403,16 +391,13 @@ class User(val pubkeyHex: String) {
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportEvent.ReportType,
|
||||
): Boolean {
|
||||
return reports[loggedIn]?.firstOrNull {
|
||||
): Boolean =
|
||||
reports[loggedIn]?.firstOrNull {
|
||||
it.event is ReportEvent &&
|
||||
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
|
||||
} != null
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(username: String): Boolean {
|
||||
return info?.anyNameStartsWith(username) ?: false
|
||||
}
|
||||
fun anyNameStartsWith(username: String): Boolean = info?.anyNameStartsWith(username) ?: false
|
||||
|
||||
var liveSet: UserLiveSet? = null
|
||||
var flowSet: UserFlowSet? = null
|
||||
@ -473,14 +458,14 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserFlowSet(u: User) {
|
||||
class UserFlowSet(
|
||||
u: User,
|
||||
) {
|
||||
// Observers line up here.
|
||||
val follows = UserBundledRefresherFlow(u)
|
||||
val relays = UserBundledRefresherFlow(u)
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
fun isInUse(): Boolean = relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
|
||||
|
||||
fun destroy() {
|
||||
relays.destroy()
|
||||
@ -489,7 +474,9 @@ class UserFlowSet(u: User) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserLiveSet(u: User) {
|
||||
class UserLiveSet(
|
||||
u: User,
|
||||
) {
|
||||
val innerMetadata = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
@ -521,8 +508,8 @@ class UserLiveSet(u: User) {
|
||||
|
||||
val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged()
|
||||
|
||||
fun isInUse(): Boolean {
|
||||
return metadata.hasObservers() ||
|
||||
fun isInUse(): Boolean =
|
||||
metadata.hasObservers() ||
|
||||
follows.hasObservers() ||
|
||||
followers.hasObservers() ||
|
||||
reports.hasObservers() ||
|
||||
@ -535,7 +522,6 @@ class UserLiveSet(u: User) {
|
||||
profilePictureChanges.hasObservers() ||
|
||||
nip05Changes.hasObservers() ||
|
||||
userMetadataInfo.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
innerMetadata.destroy()
|
||||
@ -558,7 +544,9 @@ data class RelayInfo(
|
||||
var counter: Long,
|
||||
)
|
||||
|
||||
class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
class UserBundledRefresherLiveData(
|
||||
val user: User,
|
||||
) : LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
@ -585,7 +573,9 @@ class UserBundledRefresherLiveData(val user: User) : LiveData<UserState>(UserSta
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserBundledRefresherFlow(val user: User) {
|
||||
class UserBundledRefresherFlow(
|
||||
val user: User,
|
||||
) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
val stateFlow = MutableStateFlow(UserState(user))
|
||||
@ -605,7 +595,10 @@ class UserBundledRefresherFlow(val user: User) {
|
||||
}
|
||||
}
|
||||
|
||||
class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveData<Y>(initialValue) {
|
||||
class UserLoadingLiveData<Y>(
|
||||
val user: User,
|
||||
initialValue: Y?,
|
||||
) : MediatorLiveData<Y>(initialValue) {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
NostrSingleUserDataSource.add(user)
|
||||
@ -617,4 +610,6 @@ class UserLoadingLiveData<Y>(val user: User, initialValue: Y?) : MediatorLiveDat
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable class UserState(val user: User)
|
||||
@Immutable class UserState(
|
||||
val user: User,
|
||||
)
|
||||
|
@ -186,8 +186,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun updateNetworkCapabilities(networkCapabilities: NetworkCapabilities): Boolean {
|
||||
val isOnMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val isOnWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
|
||||
val isOnMobileData = !unmetered || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val isOnWifi = unmetered && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
|
||||
var changedNetwork = false
|
||||
|
||||
|
@ -20,12 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@ -33,16 +43,86 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
fun <T> CrossfadeIfEnabled(
|
||||
targetState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
|
||||
Box(modifier) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
Crossfade(targetState, modifier, animationSpec, label, content)
|
||||
MyCrossfade(targetState, modifier, contentAlignment, animationSpec, label, content)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> MyCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val transition = updateTransition(targetState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun <T> Transition<T>.MyCrossfade(
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
contentKey: (targetState: T) -> Any? = { it },
|
||||
content: @Composable (targetState: T) -> Unit,
|
||||
) {
|
||||
val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }
|
||||
val contentMap =
|
||||
remember {
|
||||
mutableMapOf<T, @Composable () -> Unit>()
|
||||
}
|
||||
if (currentState == targetState) {
|
||||
// If not animating, just display the current state
|
||||
if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) {
|
||||
// Remove all the intermediate items from the list once the animation is finished.
|
||||
currentlyVisible.removeAll { it != targetState }
|
||||
contentMap.clear()
|
||||
}
|
||||
}
|
||||
if (!contentMap.contains(targetState)) {
|
||||
// Replace target with the same key if any
|
||||
val replacementId =
|
||||
currentlyVisible.indexOfFirst {
|
||||
contentKey(it) == contentKey(targetState)
|
||||
}
|
||||
if (replacementId == -1) {
|
||||
currentlyVisible.add(targetState)
|
||||
} else {
|
||||
currentlyVisible[replacementId] = targetState
|
||||
}
|
||||
contentMap.clear()
|
||||
currentlyVisible.fastForEach { stateForContent ->
|
||||
contentMap[stateForContent] = {
|
||||
val alpha by animateFloat(
|
||||
transitionSpec = { animationSpec },
|
||||
) { if (it == stateForContent) 1f else 0f }
|
||||
Box(Modifier.graphicsLayer { this.alpha = alpha }, contentAlignment) {
|
||||
content(stateForContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment) {
|
||||
currentlyVisible.fastForEach {
|
||||
key(contentKey(it)) {
|
||||
contentMap[it]?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,10 +100,10 @@ class NewMessageTagger(
|
||||
val results = parseDirtyWordForKey(word)
|
||||
when (val entity = results?.key?.entity) {
|
||||
is Nip19Bech32.NPub -> {
|
||||
getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord)
|
||||
getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord)
|
||||
}
|
||||
is Nip19Bech32.NProfile -> {
|
||||
getNostrAddress(dao.getOrCreateUser(entity.hex).pubkeyNpub(), results.restOfWord)
|
||||
getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord)
|
||||
}
|
||||
|
||||
is Nip19Bech32.Note -> {
|
||||
@ -138,17 +138,15 @@ class NewMessageTagger(
|
||||
word
|
||||
}
|
||||
}
|
||||
}
|
||||
.joinToString(" ")
|
||||
}
|
||||
.joinToString("\n")
|
||||
}.joinToString(" ")
|
||||
}.joinToString("\n")
|
||||
}
|
||||
|
||||
fun getNostrAddress(
|
||||
bechAddress: String,
|
||||
restOfTheWord: String?,
|
||||
): String {
|
||||
return if (restOfTheWord.isNullOrEmpty()) {
|
||||
): String =
|
||||
if (restOfTheWord.isNullOrEmpty()) {
|
||||
"nostr:$bechAddress"
|
||||
} else {
|
||||
if (Bech32.ALPHABET.contains(restOfTheWord.get(0), true)) {
|
||||
@ -157,9 +155,11 @@ class NewMessageTagger(
|
||||
"nostr:${bechAddress}$restOfTheWord"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable data class DirtyKeyInfo(val key: Nip19Bech32.ParseReturn, val restOfWord: String?)
|
||||
@Immutable data class DirtyKeyInfo(
|
||||
val key: Nip19Bech32.ParseReturn,
|
||||
val restOfWord: String?,
|
||||
)
|
||||
|
||||
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
||||
var key = mightBeAKey
|
||||
|
@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.toNpub
|
||||
import com.vitorpamplona.quartz.events.AddressableEvent
|
||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
@ -88,7 +89,7 @@ enum class UserSuggestionAnchor {
|
||||
}
|
||||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
open class NewPostViewModel : ViewModel() {
|
||||
var draftTag: String by mutableStateOf(UUID.randomUUID().toString())
|
||||
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
@ -175,17 +176,11 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
val draftTextChanges = Channel<String>(Channel.CONFLATED)
|
||||
|
||||
fun lnAddress(): String? {
|
||||
return account?.userProfile()?.info?.lnAddress()
|
||||
}
|
||||
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean {
|
||||
return account?.userProfile()?.info?.lnAddress() != null
|
||||
}
|
||||
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null
|
||||
|
||||
fun user(): User? {
|
||||
return account?.userProfile()
|
||||
}
|
||||
fun user(): User? = account?.userProfile()
|
||||
|
||||
open fun load(
|
||||
accountViewModel: AccountViewModel,
|
||||
@ -368,15 +363,21 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
originalNote =
|
||||
draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" }.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "reply" }
|
||||
.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
|
||||
if (originalNote == null) {
|
||||
originalNote =
|
||||
draftEvent.tags().filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }
|
||||
.map {
|
||||
LocalCache.checkGetOrCreateNote(it[1])
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
|
||||
@ -403,12 +404,45 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
wantsProduct = draftEvent.kind() == 30402
|
||||
|
||||
title = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "title" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
price = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "price" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
category = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "t" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
locationText = TextFieldValue(draftEvent.tags().filter { it.size > 1 && it[0] == "location" }.map { it[1] }?.firstOrNull() ?: "")
|
||||
title =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && it[0] == "title" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
price =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && it[0] == "price" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
category =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && it[0] == "t" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
locationText =
|
||||
TextFieldValue(
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && it[0] == "location" }
|
||||
.map { it[1] }
|
||||
?.firstOrNull() ?: "",
|
||||
)
|
||||
condition = ClassifiedsEvent.CONDITION.entries.firstOrNull {
|
||||
it.value == draftEvent.tags().filter { it.size > 1 && it[0] == "condition" }.map { it[1] }.firstOrNull()
|
||||
it.value ==
|
||||
draftEvent
|
||||
.tags()
|
||||
.filter { it.size > 1 && it[0] == "condition" }
|
||||
.map { it[1] }
|
||||
.firstOrNull()
|
||||
} ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW
|
||||
|
||||
wantsDirectMessage = draftEvent is PrivateDmEvent || draftEvent is ChatMessageEvent
|
||||
@ -479,7 +513,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
if (split.percentage > 0.00001) {
|
||||
val homeRelay =
|
||||
accountViewModel?.getRelayListFor(split.key)?.writeRelays()?.firstOrNull()
|
||||
?: split.key.relaysBeingUsed.keys.firstOrNull { !it.contains("localhost") }
|
||||
?: split.key.relaysBeingUsed.keys
|
||||
.firstOrNull { !it.contains("localhost") }
|
||||
|
||||
ZapSplitSetup(
|
||||
lnAddressOrPubKeyHex = split.key.pubkeyHex,
|
||||
@ -851,9 +886,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? {
|
||||
return RichTextParser().parseValidUrls(message.text).firstOrNull()
|
||||
}
|
||||
open fun findUrlInMessage(): String? = RichTextParser().parseValidUrls(message.text).firstOrNull()
|
||||
|
||||
open fun removeFromReplyList(userToRemove: User) {
|
||||
pTags = pTags?.filter { it != userToRemove }
|
||||
@ -869,14 +902,18 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
if (it.selection.collapsed) {
|
||||
val lastWord =
|
||||
it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
it.text
|
||||
.substring(0, it.selection.end)
|
||||
.substringAfterLast("\n")
|
||||
.substringAfterLast(" ")
|
||||
userSuggestionAnchor = it.selection
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions =
|
||||
LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
LocalCache
|
||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||
.reversed()
|
||||
}
|
||||
@ -894,14 +931,18 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
if (it.selection.collapsed) {
|
||||
val lastWord =
|
||||
it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
it.text
|
||||
.substring(0, it.selection.end)
|
||||
.substringAfterLast("\n")
|
||||
.substringAfterLast(" ")
|
||||
userSuggestionAnchor = it.selection
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
if (lastWord.startsWith("@") && lastWord.length > 2) {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions =
|
||||
LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
LocalCache
|
||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex }))
|
||||
.reversed()
|
||||
}
|
||||
@ -928,15 +969,15 @@ open class NewPostViewModel() : ViewModel() {
|
||||
NostrSearchEventOrUserDataSource.search(lastWord.removePrefix("@"))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
userSuggestions =
|
||||
LocalCache.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
LocalCache
|
||||
.findUsersStartingWith(lastWord.removePrefix("@"))
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ account?.isFollowing(it) },
|
||||
{ it.toBestDisplayName() },
|
||||
{ it.pubkeyHex },
|
||||
),
|
||||
)
|
||||
.reversed()
|
||||
).reversed()
|
||||
}
|
||||
} else {
|
||||
NostrSearchEventOrUserDataSource.clear()
|
||||
@ -949,7 +990,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestionAnchor?.let {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord =
|
||||
message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
message.text
|
||||
.substring(0, it.end)
|
||||
.substringAfterLast("\n")
|
||||
.substringAfterLast(" ")
|
||||
val lastWordStart = it.end - lastWord.length
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
|
||||
@ -963,7 +1007,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord =
|
||||
toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
|
||||
toUsers.text
|
||||
.substring(0, it.end)
|
||||
.substringAfterLast("\n")
|
||||
.substringAfterLast(" ")
|
||||
val lastWordStart = it.end - lastWord.length
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
|
||||
@ -972,6 +1019,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
toUsers.text.replaceRange(lastWordStart, it.end, wordToInsert),
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
||||
)
|
||||
|
||||
val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelays()
|
||||
nip17 = relayList != null
|
||||
}
|
||||
|
||||
userSuggestionAnchor = null
|
||||
@ -982,12 +1032,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
|
||||
return mutableStateMapOf(Pair(0, ""), Pair(1, ""))
|
||||
}
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> = mutableStateMapOf(Pair(0, ""), Pair(1, ""))
|
||||
|
||||
fun canPost(): Boolean {
|
||||
return message.text.isNotBlank() &&
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||
@ -1009,7 +1057,6 @@ open class NewPostViewModel() : ViewModel() {
|
||||
)
|
||||
) &&
|
||||
contentToAddUrl == null
|
||||
}
|
||||
|
||||
suspend fun createNIP94Record(
|
||||
uploadingResult: Nip96Uploader.PartialEvent,
|
||||
@ -1020,11 +1067,20 @@ open class NewPostViewModel() : ViewModel() {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
val imageUrl = uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "url" }?.get(1)
|
||||
val remoteMimeType =
|
||||
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null }
|
||||
uploadingResult.tags
|
||||
?.firstOrNull { it.size > 1 && it[0] == "m" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val originalHash =
|
||||
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null }
|
||||
uploadingResult.tags
|
||||
?.firstOrNull { it.size > 1 && it[0] == "ox" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val dim =
|
||||
uploadingResult.tags?.firstOrNull { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null }
|
||||
uploadingResult.tags
|
||||
?.firstOrNull { it.size > 1 && it[0] == "dim" }
|
||||
?.get(1)
|
||||
?.ifBlank { null }
|
||||
val magnet =
|
||||
uploadingResult.tags
|
||||
?.firstOrNull { it.size > 1 && it[0] == "magnet" }
|
||||
@ -1270,7 +1326,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
enum class GeohashPrecision(val digits: Int) {
|
||||
enum class GeohashPrecision(
|
||||
val digits: Int,
|
||||
) {
|
||||
KM_5000_X_5000(1), // 5,000km × 5,000km
|
||||
KM_1250_X_625(2), // 1,250km × 625km
|
||||
KM_156_X_156(3), // 156km × 156km
|
||||
|
@ -20,7 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
@ -33,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
|
||||
@Composable
|
||||
fun ClickableBox(
|
||||
modifier: Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
@ -49,3 +51,25 @@ fun ClickableBox(
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ClickableBox(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = Size24dp),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -50,6 +49,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
@ -97,10 +97,9 @@ fun SensitivityWarning(
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val accountState = accountViewModel.account.showSensitiveContent.collectAsStateWithLifecycle()
|
||||
|
||||
var showContentWarningNote by
|
||||
remember(accountState) { mutableStateOf(accountState?.account?.showSensitiveContent != true) }
|
||||
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
|
||||
|
||||
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
@ -118,18 +117,26 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
Box(
|
||||
Modifier.height(80.dp).width(90.dp),
|
||||
Modifier
|
||||
.height(80.dp)
|
||||
.width(90.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Visibility,
|
||||
contentDescription = stringRes(R.string.content_warning),
|
||||
modifier = Modifier.size(70.dp).align(Alignment.BottomStart),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(70.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Warning,
|
||||
contentDescription = stringRes(R.string.content_warning),
|
||||
modifier = Modifier.size(30.dp).align(Alignment.TopEnd),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ fun TextSpinner(
|
||||
readOnly = true,
|
||||
label = { label?.let { Text(it) } },
|
||||
modifier = modifier,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -200,4 +201,7 @@ fun <T> SpinnerSelectionDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable data class TitleExplainer(val title: String, val explainer: String? = null)
|
||||
@Immutable data class TitleExplainer(
|
||||
val title: String,
|
||||
val explainer: String? = null,
|
||||
)
|
||||
|
@ -26,17 +26,15 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
class HiddenAccountsFeedFilter(
|
||||
val account: Account,
|
||||
) : FeedFilter<User>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun showHiddenKey(): Boolean = true
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
|
||||
override fun feed(): List<User> =
|
||||
account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
|
||||
try {
|
||||
LocalCache.getOrCreateUser(it)
|
||||
} catch (e: Exception) {
|
||||
@ -45,33 +43,26 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
class HiddenWordsFeedFilter(
|
||||
val account: Account,
|
||||
) : FeedFilter<String>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun showHiddenKey(): Boolean = true
|
||||
|
||||
override fun feed(): List<String> {
|
||||
return account.flowHiddenUsers.value.hiddenWords.toList()
|
||||
}
|
||||
override fun feed(): List<String> =
|
||||
account.flowHiddenUsers.value.hiddenWords
|
||||
.toList()
|
||||
}
|
||||
|
||||
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
class SpammerAccountsFeedFilter(
|
||||
val account: Account,
|
||||
) : FeedFilter<User>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun showHiddenKey(): Boolean = true
|
||||
|
||||
override fun feed(): List<User> {
|
||||
return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) }
|
||||
}
|
||||
override fun feed(): List<User> = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) }
|
||||
}
|
||||
|
@ -145,10 +145,12 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
|
||||
@ -758,16 +760,18 @@ class FollowListViewModel(
|
||||
|
||||
private val _kind3GlobalPeopleRoutes =
|
||||
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow))
|
||||
.flatten()
|
||||
.toImmutableList(),
|
||||
)
|
||||
}
|
||||
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||
|
||||
private val _kind3GlobalPeople =
|
||||
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow))
|
||||
.flatten()
|
||||
@ -775,7 +779,7 @@ class FollowListViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
val kind3GlobalPeople = _kind3GlobalPeople.stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||
val kind3GlobalPeople = _kind3GlobalPeople.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists)
|
||||
|
||||
class Factory(
|
||||
val account: Account,
|
||||
|
@ -168,8 +168,7 @@ fun DrawerContent(
|
||||
BottomContent(
|
||||
accountViewModel.account.userProfile(),
|
||||
drawerState,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
@ -741,8 +740,7 @@ fun IconRowRelays(
|
||||
fun BottomContent(
|
||||
user: User,
|
||||
drawerState: DrawerState,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@ -800,8 +798,7 @@ fun BottomContent(
|
||||
if (dialogOpen) {
|
||||
ShowQRDialog(
|
||||
user,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
accountViewModel,
|
||||
onScan = {
|
||||
dialogOpen = false
|
||||
coroutineScope.launch { drawerState.close() }
|
||||
|
@ -31,6 +31,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeOff
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.AddReaction
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
@ -44,6 +45,7 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.outlined.AddReaction
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
@ -164,7 +166,20 @@ fun LikedIcon(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Liked,
|
||||
stringRes(id = R.string.like_description),
|
||||
contentDescription = stringRes(id = R.string.like_description),
|
||||
modifier = modifier,
|
||||
tint = tint,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeReactionIcon(
|
||||
modifier: Modifier,
|
||||
tint: Color = Color.Unspecified,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddReaction,
|
||||
contentDescription = stringRes(id = R.string.change_reaction),
|
||||
modifier = modifier,
|
||||
tint = tint,
|
||||
)
|
||||
|
@ -310,7 +310,7 @@ fun DisplayStatus(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayNIP05(
|
||||
fun DisplayNIP05(
|
||||
nip05: String,
|
||||
nip05Verified: MutableState<Boolean?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
|
@ -84,6 +84,7 @@ import androidx.core.graphics.ColorUtils
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostView
|
||||
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
|
||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private fun lightenColor(
|
||||
@ -110,6 +110,10 @@ private fun lightenColor(
|
||||
return Color(argb)
|
||||
}
|
||||
|
||||
val externalLinkForUser = { user: User ->
|
||||
"https://njump.me/${user.toNProfile()}"
|
||||
}
|
||||
|
||||
val externalLinkForNote = { note: Note ->
|
||||
if (note is AddressableNote) {
|
||||
if (note.event?.getReward() != null) {
|
||||
@ -298,10 +302,12 @@ private fun RenderMainPopup(
|
||||
Icons.Default.AlternateEmail,
|
||||
stringRes(R.string.quick_action_copy_user_id),
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
note.author?.let {
|
||||
scope.launch {
|
||||
clipboardManager.setText(AnnotatedString(it.toNostrUri()))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalDivider(color = primaryLight)
|
||||
@ -309,8 +315,8 @@ private fun RenderMainPopup(
|
||||
Icons.Default.FormatQuote,
|
||||
stringRes(R.string.quick_action_copy_note_id),
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.toNEvent()}"))
|
||||
scope.launch {
|
||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||
showToast(R.string.copied_note_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -49,7 +48,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@ -85,10 +83,12 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DarkerGreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
|
||||
@ -127,14 +128,20 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size28Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
|
||||
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
|
||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -817,7 +824,6 @@ fun BoostText(
|
||||
SlidingAnimationCount(boostState, grayTint, accountViewModel)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LikeReaction(
|
||||
baseNote: Note,
|
||||
@ -831,29 +837,21 @@ fun LikeReaction(
|
||||
var wantsToChangeReactionSymbol by remember { mutableStateOf(false) }
|
||||
var wantsToReact by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
contentAlignment = Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = Size24dp),
|
||||
onClick = {
|
||||
likeClick(
|
||||
accountViewModel,
|
||||
baseNote,
|
||||
onMultipleChoices = { wantsToReact = true },
|
||||
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
|
||||
)
|
||||
},
|
||||
onLongClick = { wantsToChangeReactionSymbol = true },
|
||||
),
|
||||
ClickableBox(
|
||||
onClick = {
|
||||
likeClick(
|
||||
accountViewModel,
|
||||
baseNote,
|
||||
onMultipleChoices = { wantsToReact = true },
|
||||
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
|
||||
)
|
||||
},
|
||||
onLongClick = { wantsToChangeReactionSymbol = true },
|
||||
) {
|
||||
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
|
||||
CrossfadeIfEnabled(targetState = reactionType, label = "LikeIcon", accountViewModel = accountViewModel) {
|
||||
if (it != null) {
|
||||
RenderReactionType(it, heartSizeModifier, iconFontSize)
|
||||
CrossfadeIfEnabled(targetState = reactionType, contentAlignment = Center, label = "LikeIcon", accountViewModel = accountViewModel) {
|
||||
if (reactionType != null) {
|
||||
RenderReactionType(reactionType, heartSizeModifier, iconFontSize)
|
||||
} else {
|
||||
LikeIcon(heartSizeModifier, grayTint)
|
||||
}
|
||||
@ -1307,7 +1305,6 @@ private fun BoostTypeChoicePopup(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
@ -1320,6 +1317,7 @@ fun ReactionChoicePopup(
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val toRemove = remember { baseNote.reactedBy(account.userProfile()).toImmutableSet() }
|
||||
val reactions = remember { account.reactionChoices.toImmutableList() }
|
||||
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
@ -1328,108 +1326,139 @@ fun ReactionChoicePopup(
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
toRemove = toRemove,
|
||||
onClick = { reactionType ->
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType,
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
onChangeAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ReactionChoicePopupContent(
|
||||
listOfReactions: ImmutableList<String>,
|
||||
toRemove: ImmutableSet<String>,
|
||||
onClick: (reactionType: String) -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
Box(HalfPadding, contentAlignment = Center) {
|
||||
ElevatedCard(
|
||||
Modifier
|
||||
.border(width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(5.dp)),
|
||||
shape = SmallBorder,
|
||||
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Box(modifier = Modifier.padding(5.dp)) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
account.reactionChoices.forEach { reactionType ->
|
||||
ActionableReactionButton(
|
||||
baseNote,
|
||||
reactionType,
|
||||
accountViewModel,
|
||||
onDismiss,
|
||||
onChangeAmount,
|
||||
toRemove,
|
||||
)
|
||||
}
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 5.dp, vertical = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
listOfReactions.forEach { reactionType ->
|
||||
ActionableReactionButton(
|
||||
reactionType = reactionType,
|
||||
onClick = { onClick(reactionType) },
|
||||
onChangeAmount = onChangeAmount,
|
||||
toRemove = toRemove,
|
||||
)
|
||||
}
|
||||
|
||||
ClickableBox(modifier = reactionBox, onClick = onChangeAmount) {
|
||||
ChangeReactionIcon(modifier = Size28Modifier, MaterialTheme.colorScheme.placeholderText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview()
|
||||
@Composable
|
||||
fun ReactionChoicePopupPeeview() {
|
||||
ThemeComparisonColumn {
|
||||
ReactionChoicePopupContent(
|
||||
persistentListOf(
|
||||
"\uD83D\uDE80",
|
||||
"\uD83E\uDEC2",
|
||||
"\uD83D\uDC40",
|
||||
"\uD83D\uDE02",
|
||||
"\uD83C\uDF89",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
"+",
|
||||
"-",
|
||||
"\uD83C\uDF89",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
"\uD83E\uDD14",
|
||||
"\uD83D\uDE31",
|
||||
),
|
||||
onClick = {},
|
||||
onChangeAmount = {},
|
||||
toRemove = persistentSetOf("\uD83D\uDE80", "\uD83E\uDD14", "\uD83D\uDE31"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun ActionableReactionButton(
|
||||
baseNote: Note,
|
||||
reactionType: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
toRemove: ImmutableSet<String>,
|
||||
) {
|
||||
val thisModifier =
|
||||
remember(reactionType) {
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
accountViewModel.reactToOrDelete(
|
||||
baseNote,
|
||||
reactionType,
|
||||
)
|
||||
onDismiss()
|
||||
},
|
||||
onLongClick = { onChangeAmount() },
|
||||
)
|
||||
.padding(5.dp)
|
||||
}
|
||||
|
||||
val removeSymbol =
|
||||
remember(reactionType) {
|
||||
if (reactionType in toRemove) {
|
||||
" ✖"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
ClickableBox(
|
||||
modifier = if (reactionType in toRemove) MaterialTheme.colorScheme.selectedReactionBoxModifier else reactionBox,
|
||||
onClick = onClick,
|
||||
onLongClick = onChangeAmount,
|
||||
) {
|
||||
RenderReaction(reactionType)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderReaction(reactionType: String) {
|
||||
if (reactionType.startsWith(":")) {
|
||||
val noStartColon = reactionType.removePrefix(":")
|
||||
val url = noStartColon.substringAfter(":")
|
||||
|
||||
val renderable =
|
||||
InLineIconRenderer(
|
||||
persistentListOf(
|
||||
Nip30CustomEmoji.ImageUrlType(url),
|
||||
Nip30CustomEmoji.TextType(removeSymbol),
|
||||
)
|
||||
|
||||
InLineIconRenderer(
|
||||
renderable,
|
||||
style = SpanStyle(color = Color.White),
|
||||
),
|
||||
style = SpanStyle(color = MaterialTheme.colorScheme.onBackground),
|
||||
maxLines = 1,
|
||||
modifier = thisModifier,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
} else {
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
LikedIcon(modifier = thisModifier.size(16.dp), tint = Color.White)
|
||||
LikedIcon(modifier = Size28Modifier)
|
||||
}
|
||||
|
||||
"-" -> {
|
||||
Text(
|
||||
text = removeSymbol,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = thisModifier,
|
||||
text = "\uD83D\uDC4E",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
}
|
||||
"-" ->
|
||||
else -> {
|
||||
Text(
|
||||
text = "\uD83D\uDC4E$removeSymbol",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = thisModifier,
|
||||
)
|
||||
else ->
|
||||
Text(
|
||||
"$reactionType$removeSymbol",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = thisModifier,
|
||||
reactionType,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -277,8 +277,7 @@ private fun RenderReactionOption(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.surfaceDim,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(8.dp),
|
||||
).padding(8.dp),
|
||||
) {
|
||||
if (reactionType.startsWith(":")) {
|
||||
val noStartColon = reactionType.removePrefix(":")
|
||||
@ -298,13 +297,15 @@ private fun RenderReactionOption(
|
||||
} else {
|
||||
when (reactionType) {
|
||||
"+" -> {
|
||||
LikedIcon(modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onBackground)
|
||||
Row {
|
||||
LikedIcon(modifier = Modifier.size(20.dp), tint = Color.Unspecified)
|
||||
|
||||
Text(
|
||||
text = " ✖",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = " ✖",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
"-" ->
|
||||
Text(
|
||||
|
@ -197,9 +197,11 @@ fun NoteDropDownMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.copy_user_pubkey)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
note.author?.let {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -207,7 +209,7 @@ fun NoteDropDownMenu(
|
||||
text = { Text(stringRes(R.string.copy_note_id)) },
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent()))
|
||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
@ -412,7 +414,7 @@ fun WatchBookmarksFollowsAndAccount(
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
showSensitiveContent = showSensitiveContent.value,
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
|
@ -45,16 +45,22 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.quartz.events.UserMetadata
|
||||
@ -62,21 +68,20 @@ import com.vitorpamplona.quartz.events.UserMetadata
|
||||
@Preview
|
||||
@Composable
|
||||
fun ShowQRDialogPreview() {
|
||||
val user = User("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||
|
||||
user.info =
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
accountViewModel.userProfile().info =
|
||||
UserMetadata().apply {
|
||||
name = "My Name"
|
||||
picture = "Picture"
|
||||
nip05 = null
|
||||
banner = "http://banner.com/test"
|
||||
website = "http://mywebsite.com/test"
|
||||
about = "This is the about me"
|
||||
}
|
||||
|
||||
ShowQRDialog(
|
||||
user = user,
|
||||
loadProfilePicture = false,
|
||||
loadRobohash = false,
|
||||
user = accountViewModel.userProfile(),
|
||||
accountViewModel = accountViewModel,
|
||||
onScan = {},
|
||||
onClose = {},
|
||||
)
|
||||
@ -85,8 +90,7 @@ fun ShowQRDialogPreview() {
|
||||
@Composable
|
||||
fun ShowQRDialog(
|
||||
user: User,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
onScan: (String) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
@ -126,8 +130,8 @@ fun ShowQRDialog(
|
||||
.height(100.dp)
|
||||
.clip(shape = CircleShape)
|
||||
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape),
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
@ -141,13 +145,34 @@ fun ShowQRDialog(
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
val nip05 = user.nip05()
|
||||
if (nip05 != null) {
|
||||
val nip05Verified =
|
||||
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
|
||||
|
||||
DisplayNIP05(nip05, nip05Verified, accountViewModel)
|
||||
} else {
|
||||
Text(
|
||||
text = user.pubkeyDisplayHex(),
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp),
|
||||
) {
|
||||
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
|
||||
QrCodeDrawer(user.toNostrUri())
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(horizontal = 30.dp)) {
|
||||
|
@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.joinAll
|
||||
@ -308,22 +309,19 @@ class AccountViewModel(
|
||||
noteIsHiddenFlows.get(note)
|
||||
?: combineTransform(
|
||||
account.flowHiddenUsers,
|
||||
account.liveKind3FollowsFlow,
|
||||
account.liveKind3Follows,
|
||||
note.flow().metadata.stateFlow,
|
||||
note.flow().reports.stateFlow,
|
||||
) { hiddenUsers, followingUsers, metadata, reports ->
|
||||
val isAcceptable =
|
||||
withContext(Dispatchers.IO) {
|
||||
isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users)
|
||||
}
|
||||
emit(isAcceptable)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
NoteComposeReportState(),
|
||||
).also {
|
||||
noteIsHiddenFlows.put(note, it)
|
||||
}
|
||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
|
||||
}.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Eagerly,
|
||||
NoteComposeReportState(),
|
||||
).also {
|
||||
noteIsHiddenFlows.put(note, it)
|
||||
}
|
||||
|
||||
private val noteMustShowExpandButtonFlows = LruCache<Note, StateFlow<Boolean>>(300)
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
@ -103,6 +104,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -136,6 +138,7 @@ import com.vitorpamplona.amethyst.ui.note.DrawPlayName
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
@ -996,9 +999,8 @@ private fun DrawAdditionalInfo(
|
||||
|
||||
if (dialogOpen) {
|
||||
ShowQRDialog(
|
||||
user,
|
||||
accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
user = user,
|
||||
accountViewModel = accountViewModel,
|
||||
onScan = {
|
||||
dialogOpen = false
|
||||
nav(it)
|
||||
@ -1870,6 +1872,32 @@ fun UserProfileDropDownMenu(
|
||||
},
|
||||
)
|
||||
|
||||
val actContext = LocalContext.current
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.quick_action_share)) },
|
||||
onClick = {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForUser(user),
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
stringRes(actContext, R.string.quick_action_share_browser_link),
|
||||
)
|
||||
}
|
||||
|
||||
val shareIntent =
|
||||
Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share))
|
||||
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
|
||||
if (accountViewModel.userProfile() != user) {
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
if (accountViewModel.account.isHidden(user)) {
|
||||
|
@ -135,6 +135,7 @@ val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size25Modifier = Modifier.size(25.dp)
|
||||
val Size26Modifier = Modifier.size(26.dp)
|
||||
val Size28Modifier = Modifier.size(28.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size35Modifier = Modifier.size(35.dp)
|
||||
val Size39Modifier = Modifier.size(39.dp)
|
||||
@ -268,3 +269,9 @@ val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp)
|
||||
val noteComposeRelayBox = Modifier.width(55.dp).heightIn(min = 17.dp).padding(start = 2.dp, end = 1.dp)
|
||||
|
||||
val previewCardImageModifier = Modifier.fillMaxWidth().heightIn(max = 200.dp).padding(bottom = 5.dp)
|
||||
|
||||
val reactionBox =
|
||||
Modifier
|
||||
.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||
.size(Size40dp)
|
||||
.padding(5.dp)
|
||||
|
@ -94,18 +94,12 @@ private val LightLessImportantLink = LightColorPalette.primary.copy(alpha = 0.52
|
||||
private val DarkMediumImportantLink = DarkColorPalette.primary.copy(alpha = 0.32f)
|
||||
private val LightMediumImportantLink = LightColorPalette.primary.copy(alpha = 0.32f)
|
||||
|
||||
private val DarkVeryImportantLink = DarkColorPalette.primary.copy(alpha = 0.12f)
|
||||
private val LightVeryImportantLink = LightColorPalette.primary.copy(alpha = 0.12f)
|
||||
|
||||
private val DarkGrayText = DarkColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
private val LightGrayText = LightColorPalette.onSurface.copy(alpha = 0.52f)
|
||||
|
||||
private val DarkPlaceholderText = DarkColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
private val LightPlaceholderText = LightColorPalette.onSurface.copy(alpha = 0.32f)
|
||||
|
||||
private val DarkPlaceholderTextColorFilter = ColorFilter.tint(DarkPlaceholderText)
|
||||
private val LightPlaceholderTextColorFilter = ColorFilter.tint(LightPlaceholderText)
|
||||
|
||||
private val DarkOnBackgroundColorFilter = ColorFilter.tint(DarkColorPalette.onBackground)
|
||||
private val LightOnBackgroundColorFilter = ColorFilter.tint(LightColorPalette.onBackground)
|
||||
|
||||
@ -115,20 +109,9 @@ private val LightSubtleButton = LightColorPalette.onSurface.copy(alpha = 0.22f)
|
||||
private val DarkSubtleBorder = DarkColorPalette.onSurface.copy(alpha = 0.12f)
|
||||
private val LightSubtleBorder = LightColorPalette.onSurface.copy(alpha = 0.12f)
|
||||
|
||||
private val DarkReplyItemBackground = DarkColorPalette.onSurface.copy(alpha = 0.05f)
|
||||
private val LightReplyItemBackground = LightColorPalette.onSurface.copy(alpha = 0.05f)
|
||||
|
||||
private val DarkZapraiserBackground =
|
||||
BitcoinOrange.copy(0.52f).compositeOver(DarkColorPalette.background)
|
||||
private val LightZapraiserBackground =
|
||||
BitcoinOrange.copy(0.52f).compositeOver(LightColorPalette.background)
|
||||
|
||||
private val DarkOverPictureBackground = DarkColorPalette.background.copy(0.62f)
|
||||
private val LightOverPictureBackground = LightColorPalette.background.copy(0.62f)
|
||||
|
||||
val RepostPictureBorderDark = Modifier.border(2.dp, DarkColorPalette.background, CircleShape)
|
||||
val RepostPictureBorderLight = Modifier.border(2.dp, LightColorPalette.background, CircleShape)
|
||||
|
||||
val DarkImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@ -179,6 +162,22 @@ val LightInnerPostBorderModifier =
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkSelectedReactionBoxModifier =
|
||||
Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 5.dp)
|
||||
.size(Size40dp)
|
||||
.clip(shape = SmallBorder)
|
||||
.background(DarkColorPalette.secondaryContainer)
|
||||
.padding(5.dp)
|
||||
|
||||
val LightSelectedReactionBoxModifier =
|
||||
Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 5.dp)
|
||||
.size(Size40dp)
|
||||
.clip(shape = SmallBorder)
|
||||
.background(LightColorPalette.secondaryContainer)
|
||||
.padding(5.dp)
|
||||
|
||||
val DarkChannelNotePictureModifier =
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
@ -225,16 +224,6 @@ val DarkLargeRelayIconModifier =
|
||||
.size(Size55dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val LightBottomIconModifier =
|
||||
Modifier
|
||||
.size(Size10dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val DarkBottomIconModifier =
|
||||
Modifier
|
||||
.size(Size10dp)
|
||||
.clip(shape = CircleShape)
|
||||
|
||||
val RichTextDefaults = RichTextStyle().resolveDefaults()
|
||||
|
||||
val MarkDownStyleOnDark =
|
||||
@ -319,9 +308,6 @@ val ColorScheme.isLight: Boolean
|
||||
val ColorScheme.newItemBackgroundColor: Color
|
||||
get() = if (isLight) LightNewItemBackground else DarkNewItemBackground
|
||||
|
||||
val ColorScheme.replyBackground: Color
|
||||
get() = if (isLight) LightReplyItemBackground else DarkReplyItemBackground
|
||||
|
||||
val ColorScheme.selectedNote: Color
|
||||
get() = if (isLight) LightSelectedNote else DarkSelectedNote
|
||||
|
||||
@ -331,13 +317,8 @@ val ColorScheme.secondaryButtonBackground: Color
|
||||
val ColorScheme.lessImportantLink: Color
|
||||
get() = if (isLight) LightLessImportantLink else DarkLessImportantLink
|
||||
|
||||
val ColorScheme.zapraiserBackground: Color
|
||||
get() = if (isLight) LightZapraiserBackground else DarkZapraiserBackground
|
||||
|
||||
val ColorScheme.mediumImportanceLink: Color
|
||||
get() = if (isLight) LightMediumImportantLink else DarkMediumImportantLink
|
||||
val ColorScheme.veryImportantLink: Color
|
||||
get() = if (isLight) LightVeryImportantLink else DarkVeryImportantLink
|
||||
|
||||
val ColorScheme.placeholderText: Color
|
||||
get() = if (isLight) LightPlaceholderText else DarkPlaceholderText
|
||||
@ -345,9 +326,6 @@ val ColorScheme.placeholderText: Color
|
||||
val ColorScheme.nip05: Color
|
||||
get() = if (isLight) Nip05EmailColorLight else Nip05EmailColorDark
|
||||
|
||||
val ColorScheme.placeholderTextColorFilter: ColorFilter
|
||||
get() = if (isLight) LightPlaceholderTextColorFilter else DarkPlaceholderTextColorFilter
|
||||
|
||||
val ColorScheme.onBackgroundColorFilter: ColorFilter
|
||||
get() = if (isLight) LightOnBackgroundColorFilter else DarkOnBackgroundColorFilter
|
||||
|
||||
@ -375,9 +353,6 @@ val ColorScheme.allGoodColor: Color
|
||||
val ColorScheme.markdownStyle: RichTextStyle
|
||||
get() = if (isLight) MarkDownStyleOnLight else MarkDownStyleOnDark
|
||||
|
||||
val ColorScheme.repostProfileBorder: Modifier
|
||||
get() = if (isLight) RepostPictureBorderLight else RepostPictureBorderDark
|
||||
|
||||
val ColorScheme.imageModifier: Modifier
|
||||
get() = if (isLight) LightImageModifier else DarkImageModifier
|
||||
|
||||
@ -402,8 +377,8 @@ val ColorScheme.relayIconModifier: Modifier
|
||||
val ColorScheme.largeRelayIconModifier: Modifier
|
||||
get() = if (isLight) LightLargeRelayIconModifier else DarkLargeRelayIconModifier
|
||||
|
||||
val ColorScheme.bottomIconModifier: Modifier
|
||||
get() = if (isLight) LightBottomIconModifier else DarkBottomIconModifier
|
||||
val ColorScheme.selectedReactionBoxModifier: Modifier
|
||||
get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier
|
||||
|
||||
val ColorScheme.chartStyle: ChartStyle
|
||||
get() {
|
||||
|
@ -451,7 +451,7 @@
|
||||
<string name="connectivity_type_never">कभी नहीं</string>
|
||||
<string name="ui_feature_set_type_complete">सम्पूर्ण</string>
|
||||
<string name="ui_feature_set_type_simplified">सरलीकृत</string>
|
||||
<string name="ui_feature_set_type_performance">प्रदर्शन</string>
|
||||
<string name="ui_feature_set_type_performance">वेगवान</string>
|
||||
<string name="system">यन्त्रव्यवस्था</string>
|
||||
<string name="light">प्रकाशवान</string>
|
||||
<string name="dark">अन्धकारमय</string>
|
||||
|
@ -451,6 +451,7 @@
|
||||
<string name="connectivity_type_never">从不</string>
|
||||
<string name="ui_feature_set_type_complete">完整版</string>
|
||||
<string name="ui_feature_set_type_simplified">简化版</string>
|
||||
<string name="ui_feature_set_type_performance">极速版</string>
|
||||
<string name="system">系统</string>
|
||||
<string name="light">浅色</string>
|
||||
<string name="dark">深色</string>
|
||||
|
@ -511,6 +511,7 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="connectivity_type_always">Always</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi-only</string>
|
||||
<string name="connectivity_type_unmetered_wifi_only">Unmetered WiFi</string>
|
||||
<string name="connectivity_type_never">Never</string>
|
||||
|
||||
<string name="ui_feature_set_type_complete">Complete</string>
|
||||
@ -785,6 +786,8 @@
|
||||
<string name="like_description">Like</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
|
||||
<string name="change_reaction">Change Quick Reactions</string>
|
||||
|
||||
<string name="profile_image_of_user">Profile Picture of %1$s</string>
|
||||
<string name="relay_info">Relay %1$s</string>
|
||||
<string name="expand_relay_list">Expand relay list</string>
|
||||
|
@ -14,7 +14,7 @@ coil = "2.6.0"
|
||||
composeBom = "2024.06.00"
|
||||
coreKtx = "1.13.1"
|
||||
espressoCore = "3.5.1"
|
||||
firebaseBom = "33.1.0"
|
||||
firebaseBom = "33.1.1"
|
||||
fragmentKtx = "1.8.0"
|
||||
gms = "4.4.1"
|
||||
jacksonModuleKotlin = "2.17.1"
|
||||
|
@ -39,7 +39,9 @@ object Nip19Bech32 {
|
||||
ADDRESS,
|
||||
}
|
||||
|
||||
enum class TlvTypes(val id: Byte) {
|
||||
enum class TlvTypes(
|
||||
val id: Byte,
|
||||
) {
|
||||
SPECIAL(0),
|
||||
RELAY(1),
|
||||
AUTHOR(2),
|
||||
@ -53,33 +55,59 @@ object Nip19Bech32 {
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ParseReturn(val entity: Entity, val additionalChars: String? = null)
|
||||
data class ParseReturn(
|
||||
val entity: Entity,
|
||||
val additionalChars: String? = null,
|
||||
)
|
||||
|
||||
interface Entity
|
||||
|
||||
@Immutable
|
||||
data class NSec(val hex: String) : Entity
|
||||
data class NSec(
|
||||
val hex: String,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NPub(val hex: String) : Entity
|
||||
data class NPub(
|
||||
val hex: String,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class Note(val hex: String) : Entity
|
||||
data class Note(
|
||||
val hex: String,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NProfile(val hex: String, val relay: List<String>) : Entity
|
||||
data class NProfile(
|
||||
val hex: String,
|
||||
val relay: List<String>,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NEvent(val hex: String, val relay: List<String>, val author: String?, val kind: Int?) : Entity
|
||||
data class NEvent(
|
||||
val hex: String,
|
||||
val relay: List<String>,
|
||||
val author: String?,
|
||||
val kind: Int?,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NAddress(val atag: String, val relay: List<String>, val author: String, val kind: Int) : Entity
|
||||
data class NAddress(
|
||||
val atag: String,
|
||||
val relay: List<String>,
|
||||
val author: String,
|
||||
val kind: Int,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NRelay(val relay: List<String>) : Entity
|
||||
data class NRelay(
|
||||
val relay: List<String>,
|
||||
) : Entity
|
||||
|
||||
@Immutable
|
||||
data class NEmbed(val event: Event) : Entity
|
||||
data class NEmbed(
|
||||
val event: Event,
|
||||
) : Entity
|
||||
|
||||
fun uriToRoute(uri: String?): ParseReturn? {
|
||||
if (uri == null) return null
|
||||
@ -108,8 +136,8 @@ object Nip19Bech32 {
|
||||
type: String,
|
||||
key: String?,
|
||||
additionalChars: String?,
|
||||
): ParseReturn? {
|
||||
return try {
|
||||
): ParseReturn? =
|
||||
try {
|
||||
val bytes = (type + key).bechToBytes()
|
||||
|
||||
when (type.lowercase()) {
|
||||
@ -129,7 +157,6 @@ object Nip19Bech32 {
|
||||
Log.w("NIP19 Parser", "Issue trying to Decode NIP19 $key: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun nembed(bytes: ByteArray): NEmbed? {
|
||||
if (bytes.isEmpty()) return null
|
||||
@ -205,21 +232,30 @@ object Nip19Bech32 {
|
||||
author: String?,
|
||||
kind: Int?,
|
||||
relay: String?,
|
||||
): String {
|
||||
return TlvBuilder()
|
||||
): String =
|
||||
TlvBuilder()
|
||||
.apply {
|
||||
addHex(TlvTypes.SPECIAL, idHex)
|
||||
addStringIfNotNull(TlvTypes.RELAY, relay)
|
||||
addHexIfNotNull(TlvTypes.AUTHOR, author)
|
||||
addIntIfNotNull(TlvTypes.KIND, kind)
|
||||
}
|
||||
.build()
|
||||
}.build()
|
||||
.toNEvent()
|
||||
}
|
||||
|
||||
fun createNEmbed(event: Event): String {
|
||||
return gzip(event.toJson()).toNEmbed()
|
||||
}
|
||||
fun createNProfile(
|
||||
authorPubKeyHex: String,
|
||||
relay: List<String>,
|
||||
): String =
|
||||
TlvBuilder()
|
||||
.apply {
|
||||
addHex(TlvTypes.SPECIAL, authorPubKeyHex)
|
||||
relay.forEach {
|
||||
addStringIfNotNull(TlvTypes.RELAY, it)
|
||||
}
|
||||
}.build()
|
||||
.toNProfile()
|
||||
|
||||
fun createNEmbed(event: Event): String = gzip(event.toJson()).toNEmbed()
|
||||
|
||||
fun gzip(content: String): ByteArray {
|
||||
val bos = ByteArrayOutputStream()
|
||||
@ -239,23 +275,24 @@ fun ByteArray.toNote() = Bech32.encodeBytes(hrp = "note", this, Bech32.Encoding.
|
||||
|
||||
fun ByteArray.toNEvent() = Bech32.encodeBytes(hrp = "nevent", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun ByteArray.toNProfile() = Bech32.encodeBytes(hrp = "nprofile", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun ByteArray.toNAddress() = Bech32.encodeBytes(hrp = "naddr", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun ByteArray.toLnUrl() = Bech32.encodeBytes(hrp = "lnurl", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun ByteArray.toNEmbed() = Bech32.encodeBytes(hrp = "nembed", this, Bech32.Encoding.Bech32)
|
||||
|
||||
fun decodePublicKey(key: String): ByteArray {
|
||||
return when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
|
||||
fun decodePublicKey(key: String): ByteArray =
|
||||
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
|
||||
is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey
|
||||
is Nip19Bech32.NPub -> parsed.hex.hexToByteArray()
|
||||
is Nip19Bech32.NProfile -> parsed.hex.hexToByteArray()
|
||||
else -> Hex.decode(key) // crashes on purpose
|
||||
}
|
||||
}
|
||||
|
||||
fun decodePrivateKeyAsHexOrNull(key: String): HexKey? {
|
||||
return try {
|
||||
fun decodePrivateKeyAsHexOrNull(key: String): HexKey? =
|
||||
try {
|
||||
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
|
||||
is Nip19Bech32.NSec -> parsed.hex
|
||||
is Nip19Bech32.NPub -> null
|
||||
@ -271,10 +308,9 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
|
||||
return try {
|
||||
fun decodePublicKeyAsHexOrNull(key: String): HexKey? =
|
||||
try {
|
||||
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
|
||||
is Nip19Bech32.NSec -> KeyPair(privKey = key.bechToBytes()).pubKey.toHexKey()
|
||||
is Nip19Bech32.NPub -> parsed.hex
|
||||
@ -290,10 +326,9 @@ fun decodePublicKeyAsHexOrNull(key: String): HexKey? {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeEventIdAsHexOrNull(key: String): HexKey? {
|
||||
return try {
|
||||
fun decodeEventIdAsHexOrNull(key: String): HexKey? =
|
||||
try {
|
||||
when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) {
|
||||
is Nip19Bech32.NSec -> null
|
||||
is Nip19Bech32.NPub -> null
|
||||
@ -309,7 +344,6 @@ fun decodeEventIdAsHexOrNull(key: String): HexKey? {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun TlvBuilder.addString(
|
||||
type: Nip19Bech32.TlvTypes,
|
||||
|
Loading…
x
Reference in New Issue
Block a user