From 42408978c485cf2daca5c3ecf6b5cd433c0d3c2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 12 Jun 2024 15:05:46 -0400 Subject: [PATCH] - Moves away from persistent collections (slower) - Adds a hashcode cash for spam and blocked user public keys - Adds a cache for isHidden - Moves isHidden composable from LiveData to Flow --- .../vitorpamplona/amethyst/model/Account.kt | 322 ++++++++---------- .../com/vitorpamplona/amethyst/model/Note.kt | 173 +++++----- .../amethyst/ui/note/BlockReportChecker.kt | 9 +- .../amethyst/ui/note/NoteCompose.kt | 20 +- .../ui/screen/loggedIn/AccountViewModel.kt | 243 ++++++------- .../com/vitorpamplona/quartz/events/Event.kt | 68 ++-- .../quartz/events/PeopleListEvent.kt | 35 +- 7 files changed, 384 insertions(+), 486 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 2dc8e074d..81a9f568e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -107,10 +107,6 @@ import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.utils.DualCase -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -206,7 +202,7 @@ class Account( var pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), val scope: CoroutineScope = Amethyst.instance.applicationIOScope, ) { - var transientHiddenUsers: ImmutableSet = persistentSetOf() + var transientHiddenUsers: Set = setOf() data class PaymentRequest( val relayUrl: String, @@ -223,13 +219,16 @@ class Account( @Immutable class LiveFollowLists( - val users: ImmutableSet = persistentSetOf(), - val hashtags: ImmutableSet = persistentSetOf(), - val geotags: ImmutableSet = persistentSetOf(), - val communities: ImmutableSet = persistentSetOf(), + val users: Set = emptySet(), + val hashtags: Set = emptySet(), + val geotags: Set = emptySet(), + val communities: Set = emptySet(), ) - class ListNameNotePair(val listName: String, val event: GeneralListEvent?) + class ListNameNotePair( + val listName: String, + val event: GeneralListEvent?, + ) val connectToRelaysFlow = combineTransform( @@ -361,10 +360,10 @@ class Account( userProfile().flow().follows.stateFlow.transformLatest { emit( LiveFollowLists( - it.user.cachedFollowingKeySet().toImmutableSet(), - it.user.cachedFollowingTagSet().toImmutableSet(), - it.user.cachedFollowingGeohashSet().toImmutableSet(), - it.user.cachedFollowingCommunitiesSet().toImmutableSet(), + it.user.cachedFollowingKeySet(), + it.user.cachedFollowingTagSet(), + it.user.cachedFollowingGeohashSet(), + it.user.cachedFollowingCommunitiesSet(), ), ) } @@ -379,8 +378,8 @@ class Account( } @OptIn(ExperimentalCoroutinesApi::class) - fun loadPeopleListFlowFromListName(listName: String): Flow { - return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) { + fun loadPeopleListFlowFromListName(listName: String): Flow = + if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) { val note = LocalCache.checkGetOrCreateAddressableNote(listName) note?.flow()?.metadata?.stateFlow?.mapLatest { val noteEvent = it.note.event as? GeneralListEvent @@ -389,13 +388,12 @@ class Account( } else { MutableStateFlow(ListNameNotePair(listName, null)) } - } fun combinePeopleListFlows( kind3FollowsSource: Flow, peopleListFollowsSource: Flow, - ): Flow { - return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows -> + ): Flow = + combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows -> if (peopleListFollows.listName == GLOBAL_FOLLOWS) { emit(null) } else if (peopleListFollows.listName == KIND3_FOLLOWS) { @@ -411,7 +409,6 @@ class Account( } } } - } val liveHomeFollowLists: StateFlow by lazy { combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList) @@ -465,38 +462,41 @@ class Account( onReady( LiveFollowLists( users = - (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toImmutableSet(), + (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(), hashtags = - (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toImmutableSet(), + (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(), geotags = - (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toImmutableSet(), + (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(), communities = (listEvent.taggedAddresses() + listEvent.filterAddresses(privateTagList)) .map { it.toTag() } - .toImmutableSet(), + .toSet(), ), ) } } - suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? { - return withTimeoutOrNull(1000) { + suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? = + withTimeoutOrNull(1000) { suspendCancellableCoroutine { continuation -> decryptLiveFollows(peopleListFollows) { continuation.resume(it) } } } - } @Immutable - data class LiveHiddenUsers( - val hiddenUsers: ImmutableSet, - val spammers: ImmutableSet, - val hiddenWords: ImmutableSet, - val hiddenWordsCase: List, + class LiveHiddenUsers( + val hiddenUsers: Set, + val spammers: Set, + val hiddenWords: Set, val showSensitiveContent: Boolean?, - ) + ) { + // speeds up isHidden calculations + val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() } + val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() } + val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) } + } val flowHiddenUsers: StateFlow by lazy { combineTransform( @@ -530,25 +530,22 @@ class Account( emit( LiveHiddenUsers( - hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(), - hiddenWords = hiddenWords.toPersistentSet(), - hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) }, + hiddenUsers = (resultBlockList.users + resultMuteList.users), + hiddenWords = hiddenWords, spammers = localLive.account.transientHiddenUsers, showSensitiveContent = localLive.account.showSensitiveContent, ), ) - } - .stateIn( - scope, - SharingStarted.Eagerly, - LiveHiddenUsers( - hiddenUsers = persistentSetOf(), - hiddenWords = persistentSetOf(), - hiddenWordsCase = emptyList(), - spammers = transientHiddenUsers, - showSensitiveContent = showSensitiveContent, - ), - ) + }.stateIn( + scope, + SharingStarted.Eagerly, + LiveHiddenUsers( + hiddenUsers = setOf(), + hiddenWords = setOf(), + spammers = transientHiddenUsers, + showSensitiveContent = showSensitiveContent, + ), + ) } val liveHiddenUsers = flowHiddenUsers.asLiveData() @@ -599,24 +596,21 @@ class Account( filterSpamFromStrangers = filterSpam LocalCache.antiSpam.active = filterSpamFromStrangers if (!filterSpamFromStrangers) { - transientHiddenUsers = persistentSetOf() + transientHiddenUsers = setOf() } live.invalidateData() saveable.invalidateData() } - fun userProfile(): User { - return userProfileCache + fun userProfile(): User = + userProfileCache ?: run { val myUser: User = LocalCache.getOrCreateUser(keyPair.pubKey.toHexKey()) userProfileCache = myUser myUser } - } - fun isWriteable(): Boolean { - return keyPair.privKey != null || signer is NostrSignerExternal - } + fun isWriteable(): Boolean = keyPair.privKey != null || signer is NostrSignerExternal fun sendKind3RelayList(relays: Map) { if (!isWriteable()) return @@ -689,24 +683,16 @@ class Account( fun reactionTo( note: Note, reaction: String, - ): List { - return note.reactedBy(userProfile(), reaction) - } + ): List = note.reactedBy(userProfile(), reaction) - fun hasBoosted(note: Note): Boolean { - return boostsTo(note).isNotEmpty() - } + fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty() - fun boostsTo(note: Note): List { - return note.boostedBy(userProfile()) - } + fun boostsTo(note: Note): List = note.boostedBy(userProfile()) fun hasReacted( note: Note, reaction: String, - ): Boolean { - return note.hasReacted(userProfile(), reaction) - } + ): Boolean = note.hasReacted(userProfile(), reaction) suspend fun reactTo( note: Note, @@ -721,7 +707,12 @@ class Account( if (note.event is ChatMessageEvent) { val event = note.event as ChatMessageEvent - val users = event.recipientsPubKey().plus(event.pubKey).toSet().toList() + val users = + event + .recipientsPubKey() + .plus(event.pubKey) + .toSet() + .toList() if (reaction.startsWith(":")) { val emojiUrl = EmojiUrl.decode(reaction) @@ -801,24 +792,23 @@ class Account( } } - fun getReceivingRelays(): Set { - return getNIP65RelayList()?.readRelays()?.toSet() - ?: userProfile().latestContactList?.relays()?.filter { it.value.read }?.keys?.ifEmpty { null } + fun getReceivingRelays(): Set = + getNIP65RelayList()?.readRelays()?.toSet() + ?: userProfile() + .latestContactList + ?.relays() + ?.filter { it.value.read } + ?.keys + ?.ifEmpty { null } ?: localRelays.filter { it.read }.map { it.url }.toSet() - } - fun hasWalletConnectSetup(): Boolean { - return zapPaymentRequest != null - } + fun hasWalletConnectSetup(): Boolean = zapPaymentRequest != null - fun isNIP47Author(pubkeyHex: String?): Boolean { - return (getNIP47Signer().pubKey == pubkeyHex) - } + fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex) - fun getNIP47Signer(): NostrSigner { - return zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } + fun getNIP47Signer(): NostrSigner = + zapPaymentRequest?.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer - } fun decryptZapPaymentResponseEvent( zapResponseEvent: LnZapPaymentResponseEvent, @@ -885,7 +875,11 @@ class Account( ) { LnZapRequestEvent.create( userPubKeyHex, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } + userProfile() + .latestContactList + ?.relays() + ?.keys + ?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), signer, message, @@ -2010,7 +2004,8 @@ class Account( if (receiver != null) { val relayList = ( - LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver)) + LocalCache + .getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver)) ?.event as? ChatMessageRelayListEvent )?.relays()?.ifEmpty { null } @@ -2201,9 +2196,7 @@ class Account( relay: Relay, challenge: String, onReady: (RelayAuthEvent) -> Unit, - ) { - return createAuthEvent(relay.url, challenge, onReady = onReady) - } + ) = createAuthEvent(relay.url, challenge, onReady = onReady) fun createAuthEvent( relayUrl: String, @@ -2271,13 +2264,9 @@ class Account( return LocalCache.getOrCreateAddressableNote(aTag) } - fun getBlockList(): PeopleListEvent? { - return getBlockListNote().event as? PeopleListEvent - } + fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent - fun getMuteList(): MuteListEvent? { - return getMuteListNote().event as? MuteListEvent - } + fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent fun hideWord(word: String) { val muteList = getMuteList() @@ -2388,7 +2377,7 @@ class Account( } } - transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() + transientHiddenUsers = (transientHiddenUsers - pubkeyHex) live.invalidateData() saveable.invalidateData() } @@ -2509,9 +2498,7 @@ class Account( return event.cachedGossip(signer, onReady) } - fun cachedDecryptContent(note: Note): String? { - return cachedDecryptContent(note.event) - } + fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event) fun cachedDecryptContent(event: EventInterface?): String? { if (event == null) return null @@ -2591,9 +2578,7 @@ class Account( fun preferenceBetween( source: String, target: String, - ): String? { - return languagePreferences.get("$source,$target") - } + ): String? = languagePreferences.get("$source,$target") private fun updateContactListTo(newContactList: ContactListEvent?) { if (newContactList == null || newContactList.tags.isEmpty()) return @@ -2625,39 +2610,27 @@ class Account( return usersRelayList.toTypedArray() } - fun convertLocalRelays(): Array { - return localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray() - } + fun convertLocalRelays(): Array = localRelays.map { RelaySetupInfo(RelayUrlFormatter.normalize(it.url), it.read, it.write, it.feedTypes) }.toTypedArray() - fun activeGlobalRelays(): Array { - return connectToRelays.value + fun activeGlobalRelays(): Array = + connectToRelays.value .filter { it.feedTypes.contains(FeedType.GLOBAL) } .map { it.url } .toTypedArray() - } - fun activeWriteRelays(): List { - return connectToRelays.value.filter { it.write } - } + fun activeWriteRelays(): List = connectToRelays.value.filter { it.write } - fun isAllHidden(users: Set): Boolean { - return users.all { isHidden(it) } - } + fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } fun isHidden(user: User) = isHidden(user.pubkeyHex) - fun isHidden(userHex: String): Boolean { - return flowHiddenUsers.value.hiddenUsers.contains(userHex) || + fun isHidden(userHex: String): Boolean = + flowHiddenUsers.value.hiddenUsers.contains(userHex) || flowHiddenUsers.value.spammers.contains(userHex) - } - fun followingKeySet(): Set { - return userProfile().cachedFollowingKeySet() - } + fun followingKeySet(): Set = userProfile().cachedFollowingKeySet() - fun followingTagSet(): Set { - return userProfile().cachedFollowingTagSet() - } + fun followingTagSet(): Set = userProfile().cachedFollowingTagSet() fun isAcceptable(user: User): Boolean { if (userProfile().pubkeyHex == user.pubkeyHex) { @@ -2669,11 +2642,14 @@ class Account( } if (!warnAboutPostsWithReports) { - return !isHidden(user) && // if user hasn't hided this author + return !isHidden(user) && + // if user hasn't hided this author user.reportsBy(userProfile()).isEmpty() // if user has not reported this post } - return !isHidden(user) && // if user hasn't hided this author - user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post + return !isHidden(user) && + // if user hasn't hided this author + user.reportsBy(userProfile()).isEmpty() && + // if user has not reported this post user.countReportAuthorsBy(followingKeySet()) < 5 } @@ -2681,20 +2657,18 @@ class Account( if (!warnAboutPostsWithReports) { return !note.hasReportsBy(userProfile()) } - return !note.hasReportsBy(userProfile()) && // if user has not reported this post + return !note.hasReportsBy(userProfile()) && + // if user has not reported this post note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users } - fun isFollowing(user: User): Boolean { - return user.pubkeyHex in followingKeySet() - } + fun isFollowing(user: User): Boolean = user.pubkeyHex in followingKeySet() - fun isFollowing(user: HexKey): Boolean { - return user in followingKeySet() - } + fun isFollowing(user: HexKey): Boolean = user in followingKeySet() fun isAcceptable(note: Note): Boolean { - return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author + return note.author?.let { isAcceptable(it) } ?: true && + // if user hasn't hided this author isAcceptableDirect(note) && ( (note.event !is RepostEvent && note.event !is GenericRepostEvent) || @@ -2719,8 +2693,7 @@ class Account( note.reportsBy(followsPlusMe) + (note.author?.reportsBy(followsPlusMe) ?: emptyList()) + innerReports - ) - .toSet() + ).toSet() } fun saveKind3RelayList(value: List) { @@ -2734,19 +2707,14 @@ class Account( } } - fun getDMRelayListNote(): AddressableNote { - return LocalCache.getOrCreateAddressableNote( + fun getDMRelayListNote(): AddressableNote = + LocalCache.getOrCreateAddressableNote( ChatMessageRelayListEvent.createAddressATag(signer.pubKey), ) - } - fun getDMRelayListFlow(): StateFlow { - return getDMRelayListNote().flow().metadata.stateFlow - } + fun getDMRelayListFlow(): StateFlow = getDMRelayListNote().flow().metadata.stateFlow - fun getDMRelayList(): ChatMessageRelayListEvent? { - return getDMRelayListNote().event as? ChatMessageRelayListEvent - } + fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent fun saveDMRelayList(dmRelays: List) { if (!isWriteable()) return @@ -2772,19 +2740,14 @@ class Account( } } - fun getPrivateOutboxRelayListNote(): AddressableNote { - return LocalCache.getOrCreateAddressableNote( + fun getPrivateOutboxRelayListNote(): AddressableNote = + LocalCache.getOrCreateAddressableNote( PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey), ) - } - fun getPrivateOutboxRelayListFlow(): StateFlow { - return getPrivateOutboxRelayListNote().flow().metadata.stateFlow - } + fun getPrivateOutboxRelayListFlow(): StateFlow = getPrivateOutboxRelayListNote().flow().metadata.stateFlow - fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? { - return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent - } + fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent fun savePrivateOutboxRelayList(relays: List) { if (!isWriteable()) return @@ -2811,19 +2774,14 @@ class Account( } } - fun getSearchRelayListNote(): AddressableNote { - return LocalCache.getOrCreateAddressableNote( + fun getSearchRelayListNote(): AddressableNote = + LocalCache.getOrCreateAddressableNote( SearchRelayListEvent.createAddressATag(signer.pubKey), ) - } - fun getSearchRelayListFlow(): StateFlow { - return getSearchRelayListNote().flow().metadata.stateFlow - } + fun getSearchRelayListFlow(): StateFlow = getSearchRelayListNote().flow().metadata.stateFlow - fun getSearchRelayList(): SearchRelayListEvent? { - return getSearchRelayListNote().event as? SearchRelayListEvent - } + fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent fun saveSearchRelayList(searchRelays: List) { if (!isWriteable()) return @@ -2850,19 +2808,14 @@ class Account( } } - fun getNIP65RelayListNote(): AddressableNote { - return LocalCache.getOrCreateAddressableNote( + fun getNIP65RelayListNote(): AddressableNote = + LocalCache.getOrCreateAddressableNote( AdvertisedRelayListEvent.createAddressATag(signer.pubKey), ) - } - fun getNIP65RelayListFlow(): StateFlow { - return getNIP65RelayListNote().flow().metadata.stateFlow - } + fun getNIP65RelayListFlow(): StateFlow = getNIP65RelayListNote().flow().metadata.stateFlow - fun getNIP65RelayList(): AdvertisedRelayListEvent? { - return getNIP65RelayListNote().event as? AdvertisedRelayListEvent - } + fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent fun sendNip65RelayList(relays: List) { if (!isWriteable()) return @@ -2889,17 +2842,11 @@ class Account( } } - fun getFileServersList(): FileServersEvent? { - return getFileServersNote().event as? FileServersEvent - } + fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent - fun getFileServersListFlow(): StateFlow { - return getFileServersNote().flow().metadata.stateFlow - } + fun getFileServersListFlow(): StateFlow = getFileServersNote().flow().metadata.stateFlow - fun getFileServersNote(): AddressableNote { - return LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex)) - } + fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex)) fun sendFileServersList(servers: List) { if (!isWriteable()) return @@ -2961,13 +2908,9 @@ class Account( } } - fun loadLastRead(route: String): Long { - return lastReadPerRoute[route] ?: 0 - } + fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0 - fun hasDonatedInThisVersion(): Boolean { - return hasDonatedInVersion.contains(BuildConfig.VERSION_NAME) - } + fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME) fun markDonatedInThisVersion() { hasDonatedInVersion = hasDonatedInVersion + BuildConfig.VERSION_NAME @@ -2988,7 +2931,7 @@ class Account( 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).toImmutableSet() + transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex) live.invalidateData() } } @@ -3009,8 +2952,9 @@ class Account( } } -class AccountLiveData(private val account: Account) : - LiveData(AccountState(account)) { +class AccountLiveData( + private val account: Account, +) : LiveData(AccountState(account)) { // Refreshes observers in batches. private val bundler = BundledUpdate(300, Dispatchers.Default) @@ -3027,4 +2971,6 @@ class AccountLiveData(private val account: Account) : } } -@Immutable class AccountState(val account: Account) +@Immutable class AccountState( + val account: Account, +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 4d511d4e4..0e5f62e69 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -75,7 +75,9 @@ import java.math.BigDecimal import kotlin.coroutines.resume @Stable -class AddressableNote(val address: ATag) : Note(address.toTag()) { +class AddressableNote( + val address: ATag, +) : Note(address.toTag()) { override fun idNote() = address.toNAddr() override fun toNEvent() = address.toNAddr() @@ -93,9 +95,7 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) { return minOf(publishedAt, lastCreatedAt) } - fun dTag(): String? { - return (event as? AddressableEvent)?.dTag() - } + fun dTag(): String? = (event as? AddressableEvent)?.dTag() override fun wasOrShouldBeDeletedBy( deletionEvents: Set, @@ -107,7 +107,9 @@ class AddressableNote(val address: ATag) : Note(address.toTag()) { } @Stable -open class Note(val idHex: String) { +open class Note( + val idHex: String, +) { // These fields are only available after the Text Note event is received. // They are immutable after that. var event: EventInterface? = null @@ -163,14 +165,12 @@ open class Note(val idHex: String) { } } - fun toNostrUri(): String { - return "nostr:${toNEvent()}" - } + fun toNostrUri(): String = "nostr:${toNEvent()}" open fun idDisplayNote() = idNote().toShortenHex() - fun channelHex(): HexKey? { - return if ( + fun channelHex(): HexKey? = + if ( event is ChannelMessageEvent || event is ChannelMetadataEvent || event is ChannelCreateEvent || @@ -185,7 +185,6 @@ open class Note(val idHex: String) { } else { null } - } open fun address(): ATag? = null @@ -523,39 +522,30 @@ open class Note(val idHex: String) { isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor) } - fun getReactionBy(user: User): String? { - return reactions.firstNotNullOfOrNull { + fun getReactionBy(user: User): String? = + reactions.firstNotNullOfOrNull { if (it.value.any { it.author?.pubkeyHex == user.pubkeyHex }) { it.key } else { null } } - } - fun isBoostedBy(user: User): Boolean { - return boosts.any { it.author?.pubkeyHex == user.pubkeyHex } - } + fun isBoostedBy(user: User): Boolean = boosts.any { it.author?.pubkeyHex == user.pubkeyHex } - fun hasReportsBy(user: User): Boolean { - return reports[user]?.isNotEmpty() ?: false - } + fun hasReportsBy(user: User): Boolean = reports[user]?.isNotEmpty() ?: false - fun countReportAuthorsBy(users: Set): Int { - return reports.count { it.key.pubkeyHex in users } - } + fun countReportAuthorsBy(users: Set): Int = reports.count { it.key.pubkeyHex in users } - fun reportsBy(users: Set): List { - return reports + fun reportsBy(users: Set): List = + reports .mapNotNull { if (it.key.pubkeyHex in users) { it.value } else { null } - } - .flatten() - } + }.flatten() private fun updateZapTotal() { var sumOfAmounts = BigDecimal.ZERO @@ -635,8 +625,8 @@ open class Note(val idHex: String) { ) } - fun hasPledgeBy(user: User): Boolean { - return replies + fun hasPledgeBy(user: User): Boolean = + replies .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } .any { val pledgeValue = @@ -650,10 +640,9 @@ open class Note(val idHex: String) { pledgeValue != null && it.author == user } - } - fun pledgedAmountByOthers(): BigDecimal { - return replies + fun pledgedAmountByOthers(): BigDecimal = + replies .filter { it.event?.isTaggedHash("bounty-added-reward") ?: false } .mapNotNull { try { @@ -663,9 +652,7 @@ open class Note(val idHex: String) { null // do nothing if it can't convert to bigdecimal } - } - .sumOf { it } - } + }.sumOf { it } fun hasAnyReports(): Boolean { val dayAgo = TimeUtils.oneDayAgo() @@ -676,8 +663,8 @@ open class Note(val idHex: String) { ) } - fun isNewThread(): Boolean { - return ( + fun isNewThread(): Boolean = + ( event is RepostEvent || event is GenericRepostEvent || replyTo == null || @@ -685,29 +672,20 @@ open class Note(val idHex: String) { ) && event !is ChannelMessageEvent && event !is LiveActivitiesChatMessageEvent - } - fun hasZapped(loggedIn: User): Boolean { - return zaps.any { it.key.author == loggedIn } - } + fun hasZapped(loggedIn: User): Boolean = zaps.any { it.key.author == loggedIn } fun hasReacted( loggedIn: User, content: String, - ): Boolean { - return reactedBy(loggedIn, content).isNotEmpty() - } + ): Boolean = reactedBy(loggedIn, content).isNotEmpty() fun reactedBy( loggedIn: User, content: String, - ): List { - return reactions[content]?.filter { it.author == loggedIn } ?: emptyList() - } + ): List = reactions[content]?.filter { it.author == loggedIn } ?: emptyList() - fun reactedBy(loggedIn: User): List { - return reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } - } + fun reactedBy(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { return boosts.firstOrNull { @@ -715,9 +693,7 @@ open class Note(val idHex: String) { } != null // 5 minute protection } - fun boostedBy(loggedIn: User): List { - return boosts.filter { it.author == loggedIn } - } + fun boostedBy(loggedIn: User): List = boosts.filter { it.author == loggedIn } fun moveAllReferencesTo(note: AddressableNote) { // migrates these comments to a new version @@ -762,33 +738,38 @@ open class Note(val idHex: String) { fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { val thisEvent = event ?: return false + val hash = thisEvent.pubKey().hashCode() - val isBoostedNoteHidden = - if ( - thisEvent is GenericRepostEvent || - thisEvent is RepostEvent || - thisEvent is CommunityPostApprovalEvent - ) { - replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false - } else { - false + // if the author is hidden by spam or blocked + if (accountChoices.hiddenUsersHashCodes.contains(hash) || + accountChoices.spammersHashCodes.contains(hash) + ) { + return true + } + + // if the post is sensitive and the user doesn't want to see sensitive content + if (accountChoices.showSensitiveContent == false && thisEvent.isSensitive()) { + return true + } + + // if this is a repost, consider the inner event. + if ( + thisEvent is GenericRepostEvent || + thisEvent is RepostEvent || + thisEvent is CommunityPostApprovalEvent + ) { + if (replyTo?.lastOrNull()?.isHiddenFor(accountChoices) == true) { + return true } + } - val isHiddenByWord = - if (thisEvent is BaseTextNoteEvent) { - accountChoices.hiddenWords.any { - thisEvent.content.containsAny(accountChoices.hiddenWordsCase) - } - } else { - false + if (thisEvent is BaseTextNoteEvent) { + if (thisEvent.content.containsAny(accountChoices.hiddenWordsCase)) { + return true } + } - val isSensitive = thisEvent.isSensitive() - return isBoostedNoteHidden || - isHiddenByWord || - accountChoices.hiddenUsers.contains(author?.pubkeyHex) || - accountChoices.spammers.contains(author?.pubkeyHex) || - (isSensitive && accountChoices.showSensitiveContent == false) + return false } var liveSet: NoteLiveSet? = null @@ -858,13 +839,13 @@ open class Note(val idHex: String) { } @Stable -class NoteFlowSet(u: Note) { +class NoteFlowSet( + u: Note, +) { // Observers line up here. val metadata = NoteBundledRefresherFlow(u) - fun isInUse(): Boolean { - return metadata.stateFlow.subscriptionCount.value > 0 - } + fun isInUse(): Boolean = metadata.stateFlow.subscriptionCount.value > 0 fun destroy() { metadata.destroy() @@ -872,7 +853,9 @@ class NoteFlowSet(u: Note) { } @Stable -class NoteLiveSet(u: Note) { +class NoteLiveSet( + u: Note, +) { // Observers line up here. val innerMetadata = NoteBundledRefresherLiveData(u) val innerReactions = NoteBundledRefresherLiveData(u) @@ -901,8 +884,7 @@ class NoteLiveSet(u: Note) { ?: false || boostState?.note?.boosts?.isNotEmpty() ?: false || reactionState?.note?.reactions?.isNotEmpty() ?: false - } - .distinctUntilChanged() + }.distinctUntilChanged() val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged() @@ -912,8 +894,7 @@ class NoteLiveSet(u: Note) { var total = 0 it.note.reactions.forEach { total += it.value.size } total - } - .distinctUntilChanged() + }.distinctUntilChanged() val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged() @@ -921,8 +902,8 @@ class NoteLiveSet(u: Note) { val content = innerMetadata.map { it.note.event?.content() ?: "" } - fun isInUse(): Boolean { - return metadata.hasObservers() || + fun isInUse(): Boolean = + metadata.hasObservers() || reactions.hasObservers() || boosts.hasObservers() || replies.hasObservers() || @@ -936,7 +917,6 @@ class NoteLiveSet(u: Note) { boostCount.hasObservers() || innerOts.hasObservers() || innerModifications.hasObservers() - } fun destroy() { innerMetadata.destroy() @@ -952,7 +932,9 @@ class NoteLiveSet(u: Note) { } @Stable -class NoteBundledRefresherFlow(val note: Note) { +class NoteBundledRefresherFlow( + val note: Note, +) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) val stateFlow = MutableStateFlow(NoteState(note)) @@ -973,7 +955,9 @@ class NoteBundledRefresherFlow(val note: Note) { } @Stable -class NoteBundledRefresherLiveData(val note: Note) : LiveData(NoteState(note)) { +class NoteBundledRefresherLiveData( + val note: Note, +) : LiveData(NoteState(note)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -1000,7 +984,10 @@ class NoteBundledRefresherLiveData(val note: Note) : LiveData(NoteSta } @Stable -class NoteLoadingLiveData(val note: Note, initialValue: Y?) : MediatorLiveData(initialValue) { +class NoteLoadingLiveData( + val note: Note, + initialValue: Y?, +) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() if (note is AddressableNote) { @@ -1020,7 +1007,9 @@ class NoteLoadingLiveData(val note: Note, initialValue: Y?) : MediatorLiveDat } } -@Immutable class NoteState(val note: Note) +@Immutable class NoteState( + val note: Note, +) object RelayBriefInfoCache { val cache = LruCache(50) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index f2be7e5a7..11ff460d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -30,8 +30,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -64,12 +62,7 @@ fun WatchBlockAndReport( nav: (String) -> Unit, normalNote: @Composable (canPreview: Boolean) -> Unit, ) { - val isHiddenState by remember(note) { - accountViewModel.account.liveHiddenUsers - .map { note.isHiddenFor(it) } - .distinctUntilChanged() - } - .observeAsState(accountViewModel.isNoteHidden(note)) + val isHiddenState by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle() val showAnyway = remember { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index c56b6d699..dc102e7cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -436,8 +436,7 @@ fun ClickableNote( } }, onLongClick = showPopup, - ) - .background(backgroundColor.value) + ).background(backgroundColor.value) } Column(modifier = updatedModifier) { content() } @@ -476,8 +475,11 @@ fun InnerNoteWithReactions( Column(Modifier.fillMaxWidth()) { val showSecondRow = - baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && - !isBoostedNote && !isQuotedNote && accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED + baseNote.event !is RepostEvent && + baseNote.event !is GenericRepostEvent && + !isBoostedNote && + !isQuotedNote && + accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED NoteBody( baseNote = baseNote, showAuthorPicture = isQuotedNote, @@ -843,15 +845,14 @@ fun RenderRepost( } } -fun getGradient(backgroundColor: MutableState): Brush { - return Brush.verticalGradient( +fun getGradient(backgroundColor: MutableState): Brush = + Brush.verticalGradient( colors = listOf( backgroundColor.value.copy(alpha = 0f), backgroundColor.value, ), ) -} @Composable fun ReplyNoteComposition( @@ -1126,7 +1127,10 @@ private fun ChannelNotePicture( loadProfilePicture: Boolean, ) { val model by - baseChannel.live.map { it.channel.profilePicture() }.distinctUntilChanged().observeAsState() + baseChannel.live + .map { it.channel.profilePicture() } + .distinctUntilChanged() + .observeAsState() Box(Size30Modifier) { RobohashFallbackAsyncImage( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 2dc07d36b..0b5fc112d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context import android.graphics.drawable.Drawable import android.util.Log +import android.util.LruCache import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -106,7 +107,11 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -116,9 +121,12 @@ import java.util.Locale import kotlin.coroutines.resume import kotlin.time.measureTimedValue -@Immutable open class ToastMsg() +@Immutable open class ToastMsg -@Immutable class StringToastMsg(val title: String, val msg: String) : ToastMsg() +@Immutable class StringToastMsg( + val title: String, + val msg: String, +) : ToastMsg() @Immutable class ResourceToastMsg( val titleResId: Int, @@ -126,16 +134,34 @@ import kotlin.time.measureTimedValue val params: Array? = null, ) : ToastMsg() -@Immutable class ThrowableToastMsg(val titleResId: Int, val msg: String? = null, val throwable: Throwable) : ToastMsg() +@Immutable class ThrowableToastMsg( + val titleResId: Int, + val msg: String? = null, + val throwable: Throwable, +) : ToastMsg() @Stable -class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao { +class AccountViewModel( + val account: Account, + val settings: SettingsState, +) : ViewModel(), + Dao { val accountLiveData: LiveData = account.live.map { it } val accountLanguagesLiveData: LiveData = account.liveLanguages.map { it } val accountMarkAsReadUpdates = mutableIntStateOf(0) - val userFollows: LiveData = account.userProfile().live().follows.map { it } - val userRelays: LiveData = account.userProfile().live().relays.map { it } + val userFollows: LiveData = + account + .userProfile() + .live() + .follows + .map { it } + val userRelays: LiveData = + account + .userProfile() + .live() + .relays + .map { it } val kind3Relays: StateFlow = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey) val dmRelays: StateFlow = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey) @@ -182,13 +208,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId, params)) } } - fun isWriteable(): Boolean { - return account.isWriteable() - } + fun isWriteable(): Boolean = account.isWriteable() - fun userProfile(): User { - return account.userProfile() - } + fun userProfile(): User = account.userProfile() suspend fun reactTo( note: Note, @@ -200,16 +222,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View fun observeByETag( kind: Int, eTag: HexKey, - ): StateFlow { - return LocalCache.observeETag(kind = kind, eventId = eTag, viewModelScope).latest - } + ): StateFlow = LocalCache.observeETag(kind = kind, eventId = eTag, viewModelScope).latest fun observeByAuthor( kind: Int, pubkeyHex: HexKey, - ): StateFlow { - return LocalCache.observeAuthor(kind = kind, pubkey = pubkeyHex, viewModelScope).latest - } + ): StateFlow = LocalCache.observeAuthor(kind = kind, pubkey = pubkeyHex, viewModelScope).latest fun reactToOrDelete( note: Note, @@ -236,16 +254,24 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun isNoteHidden(note: Note): Boolean { - return note.isHiddenFor(account.flowHiddenUsers.value) - } + val noteIsHiddenFlows = LruCache>(300) + + fun createIsHiddenFlow(note: Note): StateFlow = + noteIsHiddenFlows.get(note) + ?: combineTransform(account.flowHiddenUsers, note.flow().metadata.stateFlow) { hiddenUsers, metadata -> + emit(metadata.note.isHiddenFor(hiddenUsers)) + }.stateIn( + viewModelScope, + SharingStarted.Eagerly, + false, + ).also { + noteIsHiddenFlows.put(note, it) + } fun hasReactedTo( baseNote: Note, reaction: String, - ): Boolean { - return account.hasReacted(baseNote, reaction) - } + ): Boolean = account.hasReacted(baseNote, reaction) suspend fun deleteReactionTo( note: Note, @@ -254,9 +280,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View account.delete(account.reactionTo(note, reaction)) } - fun hasBoosted(baseNote: Note): Boolean { - return account.hasBoosted(baseNote) - } + fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote) fun deleteBoostsTo(note: Note) { viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) } @@ -332,15 +356,17 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View ) { viewModelScope.launch(Dispatchers.IO) { val initialResults = - zaps.associate { - it.request to - ZapAmountCommentNotification( - it.request.author, - it.request.event?.content()?.ifBlank { null }, - showAmountAxis((it.response.event as? LnZapEvent)?.amount), - ) - } - .toMutableMap() + zaps + .associate { + it.request to + ZapAmountCommentNotification( + it.request.author, + it.request.event + ?.content() + ?.ifBlank { null }, + showAmountAxis((it.response.event as? LnZapEvent)?.amount), + ) + }.toMutableMap() collectSuccessfulSigningOperations( operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, @@ -359,8 +385,8 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun cachedDecryptAmountMessageInGroup(zapNotes: List): ImmutableList { - return zapNotes + fun cachedDecryptAmountMessageInGroup(zapNotes: List): ImmutableList = + zapNotes .map { val request = it.request.event as? LnZapRequestEvent if (request?.isPrivateZap() == true) { @@ -374,20 +400,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } else { ZapAmountCommentNotification( it.request.author, - it.request.event?.content()?.ifBlank { null }, + it.request.event + ?.content() + ?.ifBlank { null }, showAmountAxis((it.response.event as? LnZapEvent)?.amount), ) } } else { ZapAmountCommentNotification( it.request.author, - it.request.event?.content()?.ifBlank { null }, + it.request.event + ?.content() + ?.ifBlank { null }, showAmountAxis((it.response.event as? LnZapEvent)?.amount), ) } - } - .toImmutableList() - } + }.toImmutableList() fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList { val myList = baseNote.zaps.toList() @@ -406,19 +434,22 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } else { ZapAmountCommentNotification( it.first.author, - it.first.event?.content()?.ifBlank { null }, + it.first.event + ?.content() + ?.ifBlank { null }, showAmountAxis((it.second?.event as? LnZapEvent)?.amount), ) } } else { ZapAmountCommentNotification( it.first.author, - it.first.event?.content()?.ifBlank { null }, + it.first.event + ?.content() + ?.ifBlank { null }, showAmountAxis((it.second?.event as? LnZapEvent)?.amount), ) } - } - .toImmutableList() + }.toImmutableList() } fun decryptAmountMessageInGroup( @@ -434,11 +465,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View it.first to ZapAmountCommentNotification( it.first.author, - it.first.event?.content()?.ifBlank { null }, + it.first.event + ?.content() + ?.ifBlank { null }, showAmountAxis((it.second?.event as? LnZapEvent)?.amount), ) - } - .toMutableMap() + }.toMutableMap() collectSuccessfulSigningOperations, ZapAmountCommentNotification>( operationsInput = myList, @@ -588,9 +620,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View account.isInPrivateBookmarks(note, onReady) } - fun isInPublicBookmarks(note: Note): Boolean { - return account.isInPublicBookmarks(note) - } + fun isInPublicBookmarks(note: Note): Boolean = account.isInPublicBookmarks(note) fun broadcast(note: Note) { viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) } @@ -615,13 +645,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { account.delete(note) } } - fun cachedDecrypt(note: Note): String? { - return account.cachedDecryptContent(note) - } + fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note) - fun cachedDecrypt(event: EventInterface?): String? { - return account.cachedDecryptContent(event) - } + fun cachedDecrypt(event: EventInterface?): String? = account.cachedDecryptContent(event) fun decrypt( note: Note, @@ -685,18 +711,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) } } - fun isLoggedUser(user: User?): Boolean { - return account.userProfile().pubkeyHex == user?.pubkeyHex - } + fun isLoggedUser(user: User?): Boolean = account.userProfile().pubkeyHex == user?.pubkeyHex fun isFollowing(user: User?): Boolean { if (user == null) return false return account.userProfile().isFollowingCached(user) } - fun isFollowing(user: HexKey): Boolean { - return account.userProfile().isFollowingCached(user) - } + fun isFollowing(user: HexKey): Boolean = account.userProfile().isFollowingCached(user) val hideDeleteRequestDialog: Boolean get() = account.hideDeleteRequestDialog @@ -737,9 +759,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun defaultZapType(): LnZapEvent.ZapType { - return account.defaultZapType - } + fun defaultZapType(): LnZapEvent.ZapType = account.defaultZapType @Immutable data class NoteComposeReportState( @@ -898,13 +918,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { runOnIO() } } - suspend fun checkGetOrCreateUser(key: HexKey): User? { - return LocalCache.checkGetOrCreateUser(key) - } + suspend fun checkGetOrCreateUser(key: HexKey): User? = LocalCache.checkGetOrCreateUser(key) - override suspend fun getOrCreateUser(key: HexKey): User { - return LocalCache.getOrCreateUser(key) - } + override suspend fun getOrCreateUser(key: HexKey): User = LocalCache.getOrCreateUser(key) fun checkGetOrCreateUser( key: HexKey, @@ -913,17 +929,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateUser(key)) } } - fun getUserIfExists(hex: HexKey): User? { - return LocalCache.getUserIfExists(hex) - } + fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex) - private suspend fun checkGetOrCreateNote(key: HexKey): Note? { - return LocalCache.checkGetOrCreateNote(key) - } + private suspend fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key) - override suspend fun getOrCreateNote(key: HexKey): Note { - return LocalCache.getOrCreateNote(key) - } + override suspend fun getOrCreateNote(key: HexKey): Note = LocalCache.getOrCreateNote(key) fun checkGetOrCreateNote( key: HexKey, @@ -948,13 +958,9 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun getNoteIfExists(hex: HexKey): Note? { - return LocalCache.getNoteIfExists(hex) - } + fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex) - override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? { - return LocalCache.checkGetOrCreateAddressableNote(key) - } + override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? = LocalCache.checkGetOrCreateAddressableNote(key) fun checkGetOrCreateAddressableNote( key: HexKey, @@ -963,9 +969,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) } } - suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? { - return LocalCache.getOrCreateAddressableNote(key) - } + suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? = LocalCache.getOrCreateAddressableNote(key) fun getOrCreateAddressableNote( key: ATag, @@ -974,9 +978,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(getOrCreateAddressableNote(key)) } } - fun getAddressableNoteIfExists(key: String): AddressableNote? { - return LocalCache.getAddressableNoteIfExists(key) - } + fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) suspend fun findStatusesForUser( myUser: User, @@ -1007,9 +1009,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? { - return LocalCache.checkGetOrCreateChannel(key) - } + private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key) fun checkGetOrCreateChannel( key: HexKey, @@ -1018,9 +1018,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) } } - fun getChannelIfExists(hex: HexKey): Channel? { - return LocalCache.getChannelIfExists(hex) - } + fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex) fun loadParticipants( participants: List, @@ -1036,8 +1034,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View it, ) } - } - .toImmutableList() + }.toImmutableList() onReady(participantUsers) } @@ -1150,10 +1147,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - class Factory(val account: Account, val settings: SettingsState) : ViewModelProvider.Factory { - override fun create(modelClass: Class): AccountViewModel { - return AccountViewModel(account, settings) as AccountViewModel - } + class Factory( + val account: Account, + val settings: SettingsState, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): AccountViewModel = AccountViewModel(account, settings) as AccountViewModel } private var collectorJob: Job? = null @@ -1404,19 +1402,18 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - fun getRelayListFor(user: User): AdvertisedRelayListEvent? { - return (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?) - } + fun getRelayListFor(user: User): AdvertisedRelayListEvent? = (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?) - fun getRelayListNoteFor(user: User): AddressableNote? { - return LocalCache.getAddressableNoteIfExists( + fun getRelayListNoteFor(user: User): AddressableNote? = + LocalCache.getAddressableNoteIfExists( AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex), ) - } val draftNoteCache = CachedDraftNotes(this) - class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync(20) { + class CachedDraftNotes( + val accountViewModel: AccountViewModel, + ) : GenericBaseCacheAsync(20) { override suspend fun compute( key: DraftEvent, onReady: (Note?) -> Unit, @@ -1431,9 +1428,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View val bechLinkCache = CachedLoadedBechLink(this) - class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache(20) { - override suspend fun compute(key: String): LoadedBechLink? { - return Nip19Bech32.uriToRoute(key)?.let { + class CachedLoadedBechLink( + val accountViewModel: AccountViewModel, + ) : GenericBaseCache(20) { + override suspend fun compute(key: String): LoadedBechLink? = + Nip19Bech32.uriToRoute(key)?.let { var returningNote: Note? = null when (val parsed = it.entity) { @@ -1460,11 +1459,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View LoadedBechLink(returningNote, it) } - } } } -class HasNotificationDot(bottomNavigationItems: ImmutableList) { +class HasNotificationDot( + bottomNavigationItems: ImmutableList, +) { val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) } fun update( @@ -1489,7 +1489,10 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList) { } } -@Immutable data class LoadedBechLink(val baseNote: Note?, val nip19: Nip19Bech32.ParseReturn) +@Immutable data class LoadedBechLink( + val baseNote: Note?, + val nip19: Nip19Bech32.ParseReturn, +) public suspend fun collectSuccessfulSigningOperations( operationsInput: List, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index a075ad47c..5051d9386 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -59,14 +59,13 @@ open class Event( ) : EventInterface { override fun isContentEncoded() = false - override fun countMemory(): Long { - return 12L + + override fun countMemory(): Long = + 12L + id.bytesUsedInMemory() + pubKey.bytesUsedInMemory() + tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } + content.bytesUsedInMemory() + sig.bytesUsedInMemory() - } override fun id(): HexKey = id @@ -152,8 +151,8 @@ open class Event( override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" } - override fun zapSplitSetup(): List { - return tags + override fun zapSplitSetup(): List = + tags .filter { it.size > 1 && it[0] == "zap" } .mapNotNull { val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true) @@ -170,7 +169,6 @@ open class Event( null } } - } override fun taggedAddresses() = tags @@ -272,29 +270,23 @@ open class Event( return rank } - override fun getGeoHash(): String? { - return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null } - } + override fun getGeoHash(): String? = tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null } - override fun getReward(): BigDecimal? { - return try { + override fun getReward(): BigDecimal? = + try { tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) } } catch (e: Exception) { null } - } - open fun toNIP19(): String { - return if (this is AddressableEvent) { + open fun toNIP19(): String = + if (this is AddressableEvent) { ATag(kind, pubKey, dTag(), null).toNAddr() } else { Nip19Bech32.createNEvent(id, pubKey, kind, null) } - } - fun toNostrUri(): String { - return "nostr:${toNIP19()}" - } + fun toNostrUri(): String = "nostr:${toNIP19()}" fun hasCorrectIDHash(): Boolean { if (id.isEmpty()) return false @@ -320,8 +312,7 @@ open class Event( | Event: ${toJson()} | Actual ID: $id | Generated: ${generateId()} - """ - .trimIndent(), + """.trimIndent(), ) } if (!hasVerifiedSignature()) { @@ -329,22 +320,17 @@ open class Event( } } - override fun hasValidSignature(): Boolean { - return try { + override fun hasValidSignature(): Boolean = + try { hasCorrectIDHash() && hasVerifiedSignature() } catch (e: Exception) { Log.w("Event", "Event $id does not have a valid signature: ${toJson()}", e) false } - } - fun makeJsonForId(): String { - return makeJsonForId(pubKey, createdAt, kind, tags, content) - } + fun makeJsonForId(): String = makeJsonForId(pubKey, createdAt, kind, tags, content) - fun generateId(): String { - return CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey() - } + fun generateId(): String = CryptoUtils.sha256(makeJsonForId().toByteArray()).toHexKey() fun generateId2(): String { val sha256 = MessageDigest.getInstance("SHA-256") @@ -356,9 +342,7 @@ open class Event( override fun deserialize( jp: JsonParser, ctxt: DeserializationContext, - ): Event { - return fromJson(jp.codec.readTree(jp)) - } + ): Event = fromJson(jp.codec.readTree(jp)) } private class GossipDeserializer : StdDeserializer(Gossip::class.java) { @@ -460,8 +444,8 @@ open class Event( .addDeserializer(Request::class.java, RequestDeserializer()), ) - fun fromJson(jsonObject: JsonNode): Event { - return EventFactory.create( + fun fromJson(jsonObject: JsonNode): Event = + EventFactory.create( id = jsonObject.get("id").asText().intern(), pubKey = jsonObject.get("pubkey").asText().intern(), createdAt = jsonObject.get("created_at").asLong(), @@ -473,11 +457,8 @@ open class Event( content = jsonObject.get("content").asText(), sig = jsonObject.get("sig").asText(), ) - } - private inline fun JsonNode.toTypedArray(transform: (JsonNode) -> R): Array { - return Array(size()) { transform(get(it)) } - } + private inline fun JsonNode.toTypedArray(transform: (JsonNode) -> R): Array = Array(size()) { transform(get(it)) } fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) @@ -518,9 +499,7 @@ open class Event( kind: Int, tags: Array>, content: String, - ): ByteArray { - return CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) - } + ): ByteArray = CryptoUtils.sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) fun create( signer: NostrSigner, @@ -529,9 +508,7 @@ open class Event( content: String = "", createdAt: Long = TimeUtils.now(), onReady: (Event) -> Unit, - ) { - return signer.sign(createdAt, kind, tags, content, onReady) - } + ) = signer.sign(createdAt, kind, tags, content, onReady) } } @@ -572,7 +549,8 @@ open class BaseAddressableEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { +) : Event(id, pubKey, createdAt, kind, tags, content, sig), + AddressableEvent { override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" override fun address() = ATag(kind, pubKey, dTag(), null) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt index 8c35d3410..c872f2d06 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf @Immutable class PeopleListEvent( @@ -71,9 +70,9 @@ class PeopleListEvent( } @Immutable - data class UsersAndWords( - val users: ImmutableSet = persistentSetOf(), - val words: ImmutableSet = persistentSetOf(), + class UsersAndWords( + val users: Set = setOf(), + val words: Set = setOf(), ) fun publicAndPrivateUsersAndWords( @@ -120,9 +119,7 @@ class PeopleListEvent( const val BLOCK_LIST_D_TAG = "mute" const val ALT = "List of people" - fun blockListFor(pubKeyHex: HexKey): String { - return "30000:$pubKeyHex:$BLOCK_LIST_D_TAG" - } + fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG" fun createListWithTag( name: String, @@ -161,9 +158,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - } + ) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) fun createListWithWord( name: String, @@ -172,9 +167,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady) - } + ) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady) fun addUsers( earlierVersion: PeopleListEvent, @@ -223,9 +216,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) - } + ) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) fun addUser( earlierVersion: PeopleListEvent, @@ -234,9 +225,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - } + ) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) fun addTag( earlierVersion: PeopleListEvent, @@ -284,9 +273,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) - } + ) = removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) fun removeUser( earlierVersion: PeopleListEvent, @@ -295,9 +282,7 @@ class PeopleListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PeopleListEvent) -> Unit, - ) { - return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - } + ) = removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) fun removeTag( earlierVersion: PeopleListEvent,