From 5fdff97cf8e8e855e16dbaa0a1e60045887a1812 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 6 Aug 2024 15:36:04 -0400 Subject: [PATCH] Moves the ContactList cache lists to AccountViewModel, where it can be disposed more efficiently. --- .../vitorpamplona/amethyst/model/Account.kt | 54 ++++++++++++------- .../com/vitorpamplona/amethyst/model/User.kt | 36 ++----------- .../amethyst/ui/navigation/AppTopBar.kt | 10 ++-- .../amethyst/ui/navigation/DrawerContent.kt | 44 +++++---------- .../amethyst/ui/note/ChannelCardCompose.kt | 6 +-- .../amethyst/ui/note/UserProfilePicture.kt | 5 +- .../ui/note/elements/DisplayHashtags.kt | 7 ++- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- .../ui/screen/loggedIn/GeoHashScreen.kt | 2 +- .../ui/screen/loggedIn/HashtagScreen.kt | 2 +- .../vitorpamplona/quartz/crypto/KeyPair.kt | 4 ++ 11 files changed, 74 insertions(+), 100 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0a88c7358..17a17b4d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -225,6 +225,7 @@ class Account( @Immutable class LiveFollowLists( val users: Set = emptySet(), + val usersPlusMe: Set, val hashtags: Set = emptySet(), val geotags: Set = emptySet(), val communities: Set = emptySet(), @@ -423,17 +424,29 @@ class Account( val liveKind3FollowsFlow: Flow = userProfile().flow().follows.stateFlow.transformLatest { checkNotInMainThread() + + // makes sure the output include only valid p tags + val verifiedFollowingUsers = it.user.latestContactList?.verifiedFollowKeySet() ?: emptySet() + emit( LiveFollowLists( - it.user.cachedFollowingKeySet(), - it.user.cachedFollowingTagSet(), - it.user.cachedFollowingGeohashSet(), - it.user.cachedFollowingCommunitiesSet(), + verifiedFollowingUsers, + verifiedFollowingUsers + keyPair.pubKeyHex, + it.user.latestContactList + ?.unverifiedFollowTagSet() + ?.map { it.lowercase() } + ?.toSet() ?: emptySet(), + it.user.latestContactList + ?.unverifiedFollowGeohashSet() + ?.toSet() ?: emptySet(), + it.user.latestContactList + ?.verifiedFollowAddressSet() + ?.toSet() ?: emptySet(), ), ) } - val liveKind3Follows = liveKind3FollowsFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + val liveKind3Follows = liveKind3FollowsFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) @OptIn(ExperimentalCoroutinesApi::class) private val liveHomeList: Flow by lazy { @@ -465,11 +478,11 @@ class Account( } else if (peopleListFollows.listName == KIND3_FOLLOWS) { emit(kind3Follows) } else if (peopleListFollows.event == null) { - emit(LiveFollowLists()) + emit(LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } else { val result = waitToDecrypt(peopleListFollows.event) if (result == null) { - emit(LiveFollowLists()) + emit(LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } else { emit(result) } @@ -481,7 +494,7 @@ class Account( } val liveHomeFollowLists: StateFlow by lazy { - liveHomeFollowListFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + liveHomeFollowListFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } fun relaysFromPeopleListFlows( @@ -540,7 +553,7 @@ class Account( val liveNotificationFollowLists: StateFlow by lazy { combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList) - .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } @OptIn(ExperimentalCoroutinesApi::class) @@ -552,7 +565,7 @@ class Account( val liveStoriesFollowLists: StateFlow by lazy { combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList) - .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } @OptIn(ExperimentalCoroutinesApi::class) @@ -564,7 +577,7 @@ class Account( val liveDiscoveryFollowLists: StateFlow by lazy { combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList) - .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists()) + .stateIn(scope, SharingStarted.Eagerly, LiveFollowLists(usersPlusMe = setOf(keyPair.pubKeyHex))) } private fun decryptLiveFollows( @@ -572,10 +585,11 @@ class Account( onReady: (LiveFollowLists) -> Unit, ) { listEvent.privateTags(signer) { privateTagList -> + val users = (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet() onReady( LiveFollowLists( - users = - (listEvent.bookmarkedPeople() + listEvent.filterUsers(privateTagList)).toSet(), + users = users, + usersPlusMe = users + userProfile().pubkeyHex, hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(), geotags = @@ -759,6 +773,10 @@ class Account( } } + suspend fun countFollowersOf(pubkey: HexKey): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false } + + suspend fun followerCount(): Int = countFollowersOf(keyPair.pubKeyHex) + fun sendNewUserMetadata( name: String? = null, picture: String? = null, @@ -2813,9 +2831,7 @@ class Account( flowHiddenUsers.value.hiddenUsers.contains(userHex) || flowHiddenUsers.value.spammers.contains(userHex) - fun followingKeySet(): Set = userProfile().cachedFollowingKeySet() - - fun followingTagSet(): Set = userProfile().cachedFollowingTagSet() + fun followingKeySet(): Set = liveKind3Follows.value.users fun isAcceptable(user: User): Boolean { if (userProfile().pubkeyHex == user.pubkeyHex) { @@ -2865,8 +2881,6 @@ class Account( } fun getRelevantReports(note: Note): Set { - val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet() - val innerReports = if (note.event is RepostEvent || note.event is GenericRepostEvent) { note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList() @@ -2875,8 +2889,8 @@ class Account( } return ( - note.reportsBy(followsPlusMe) + - (note.author?.reportsBy(followsPlusMe) ?: emptyList()) + + note.reportsBy(liveKind3Follows.value.usersPlusMe) + + (note.author?.reportsBy(liveKind3Follows.value.usersPlusMe) ?: emptyList()) + innerReports ).toSet() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 50964cd07..de79660e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -336,52 +336,24 @@ class User( fun isFollowing(user: User): Boolean = latestContactList?.isTaggedUser(user.pubkeyHex) ?: false - fun isFollowingHashtag(tag: String): Boolean = latestContactList?.isTaggedHash(tag) ?: false - - fun isFollowingHashtagCached(tag: String): Boolean { - return latestContactList?.verifiedFollowTagSet?.let { + fun isFollowingHashtag(tag: String): Boolean { + return latestContactList?.unverifiedFollowTagSet()?.map { it.lowercase() }?.toSet()?.let { return tag.lowercase() in it } ?: false } - fun isFollowingGeohashCached(geoTag: String): Boolean { - return latestContactList?.verifiedFollowGeohashSet?.let { + fun isFollowingGeohash(geoTag: String): Boolean { + return latestContactList?.unverifiedFollowAddressSet()?.toSet()?.let { return geoTag.lowercase() in it } ?: false } - fun isFollowingCached(user: User): Boolean { - return latestContactList?.verifiedFollowKeySet?.let { - return user.pubkeyHex in it - } - ?: false - } - - fun isFollowingCached(userHex: String): Boolean { - return latestContactList?.verifiedFollowKeySet?.let { - return userHex in it - } - ?: false - } - fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - fun cachedFollowingKeySet(): Set = latestContactList?.verifiedFollowKeySet ?: emptySet() - - fun cachedFollowingTagSet(): Set = latestContactList?.verifiedFollowTagSet ?: emptySet() - - fun cachedFollowingGeohashSet(): Set = latestContactList?.verifiedFollowGeohashSet ?: emptySet() - - fun cachedFollowingCommunitiesSet(): Set = latestContactList?.verifiedFollowCommunitySet ?: emptySet() - - fun cachedFollowCount(): Int? = latestContactList?.verifiedFollowKeySet?.size - - suspend fun cachedFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - fun hasSentMessagesTo(key: ChatroomKey?): Boolean { val messagesToUser = privateChatrooms[key] ?: return false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 3603f7a21..ad469df87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -763,9 +763,11 @@ class FollowListViewModel( @OptIn(ExperimentalCoroutinesApi::class) val liveKind3FollowsFlow: Flow> = - account.userProfile().flow().follows.stateFlow.transformLatest { + account.liveKind3FollowsFlow.transformLatest { + checkNotInMainThread() + val communities = - it.user.cachedFollowingCommunitiesSet().mapNotNull { + it.communities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote -> CodeName( "Community/${communityNote.idHex}", @@ -776,12 +778,12 @@ class FollowListViewModel( } val hashtags = - it.user.cachedFollowingTagSet().map { + it.hashtags.map { CodeName("Hashtag/$it", HashtagName(it), CodeNameType.ROUTE) } val geotags = - it.user.cachedFollowingGeohashSet().map { + it.geotags.map { CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index e8fcfcba0..fbd2e364c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -83,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView @@ -114,7 +115,6 @@ import com.vitorpamplona.ammolite.relays.RelayPoolStatus import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.ImmutableListOfLists -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable @@ -151,7 +151,7 @@ fun DrawerContent( EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel, drawerState) } - FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser) + FollowingAndFollowerCounts(accountViewModel.account, onClickUser) HorizontalDivider( thickness = DividerThickness, @@ -380,18 +380,12 @@ fun UserStatusDeleteButton(onClick: () -> Unit) { @Composable private fun FollowingAndFollowerCounts( - baseAccountUser: User, + baseAccountUser: Account, onClick: () -> Unit, ) { - var followingCount by remember { mutableStateOf("--") } + val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle() var followerCount by remember { mutableStateOf("--") } - WatchFollow(baseAccountUser = baseAccountUser) { newFollowing -> - if (followingCount != newFollowing) { - followingCount = newFollowing - } - } - WatchFollower(baseAccountUser = baseAccountUser) { newFollower -> if (followerCount != newFollower) { followerCount = newFollower @@ -402,7 +396,9 @@ private fun FollowingAndFollowerCounts( modifier = drawerSpacing.clickable(onClick = onClick), ) { Text( - text = followingCount, + text = + followingCount.value.users.size + .toString(), fontWeight = FontWeight.Bold, ) @@ -419,31 +415,19 @@ private fun FollowingAndFollowerCounts( } } -@Composable -fun WatchFollow( - baseAccountUser: User, - onReady: (String) -> Unit, -) { - val accountUserFollowsState by baseAccountUser.live().follows.observeAsState() - - LaunchedEffect(key1 = accountUserFollowsState) { - launch(Dispatchers.IO) { - onReady(accountUserFollowsState?.user?.cachedFollowCount()?.toString() ?: "--") - } - } -} - @Composable fun WatchFollower( - baseAccountUser: User, + baseAccountUser: Account, onReady: (String) -> Unit, ) { - val accountUserFollowersState by baseAccountUser.live().followers.observeAsState() + val accountUserFollowersState by baseAccountUser + .userProfile() + .live() + .followers + .observeAsState() LaunchedEffect(key1 = accountUserFollowersState) { - launch(Dispatchers.IO) { - onReady(accountUserFollowersState?.user?.cachedFollowerCount()?.toString() ?: "--") - } + onReady(baseAccountUser.followerCount().toString() ?: "--") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 986f2fd6c..95ab46759 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -676,7 +676,7 @@ fun LoadModerators( val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() + val allFollows = accountViewModel.account.liveKind3Follows.value.users val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts) @@ -732,7 +732,7 @@ private fun LoadParticipants( val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() + val allFollows = accountViewModel.account.liveKind3Follows.value.users val followingParticipants = ParticipantListBuilder() .followsThatParticipateOn(baseNote, allFollows) @@ -889,7 +889,7 @@ fun RenderChannelThumb( val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.userProfile().cachedFollowingKeySet() + val allFollows = accountViewModel.account.liveKind3Follows.value.users val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index c3ddbc8e3..d21901c5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -367,11 +367,10 @@ fun WatchUserFollows( remember { accountViewModel.userFollows .map { - it.user.isFollowingCached(userHex) || - (userHex == accountViewModel.account.userProfile().pubkeyHex) + accountViewModel.isFollowing(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex) }.distinctUntilChanged() }.observeAsState( - accountViewModel.account.userProfile().isFollowingCached(userHex) || + accountViewModel.isFollowing(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 54d3d424c..02edff45e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -29,12 +29,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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 import androidx.compose.ui.Alignment import androidx.compose.ui.text.AnnotatedString +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -44,12 +44,11 @@ fun DisplayFollowingHashtagsInPost( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val userFollowState by accountViewModel.userFollows.observeAsState() + val userFollowState by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle() var firstTag by remember(baseNote) { mutableStateOf(null) } LaunchedEffect(key1 = userFollowState) { - val followingTags = userFollowState?.user?.cachedFollowingTagSet() ?: emptySet() - val newFirstTag = baseNote.event?.firstIsTaggedHashes(followingTags) + val newFirstTag = baseNote.event?.firstIsTaggedHashes(userFollowState.hashtags) if (firstTag != newFirstTag) { firstTag = newFirstTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 67f412c5f..5bf6ee7c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -807,10 +807,10 @@ class AccountViewModel( fun isFollowing(user: User?): Boolean { if (user == null) return false - return account.userProfile().isFollowingCached(user) + return account.isFollowing(user) } - fun isFollowing(user: HexKey): Boolean = account.userProfile().isFollowingCached(user) + fun isFollowing(user: HexKey): Boolean = account.isFollowing(user) val hideDeleteRequestDialog: Boolean get() = account.hideDeleteRequestDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index 4063ceef1..85f726e7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -188,7 +188,7 @@ fun GeoHashActionOptions( .observeAsState() val isFollowingTag by remember(userState) { - derivedStateOf { userState?.user?.isFollowingGeohashCached(tag) ?: false } + derivedStateOf { userState?.user?.isFollowingGeohash(tag) ?: false } } if (isFollowingTag) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index 3f9c8602e..9acaf32d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -167,7 +167,7 @@ fun HashtagActionOptions( .observeAsState() val isFollowingTag by remember(userState) { - derivedStateOf { userState?.user?.isFollowingHashtagCached(tag) ?: false } + derivedStateOf { userState?.user?.isFollowingHashtag(tag) ?: false } } if (isFollowingTag) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt index d6390abc2..0b696fa52 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/KeyPair.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.crypto +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.toHexKey class KeyPair( @@ -29,6 +30,7 @@ class KeyPair( ) { val privKey: ByteArray? val pubKey: ByteArray + val pubKeyHex: HexKey init { if (privKey == null) { @@ -52,6 +54,8 @@ class KeyPair( this.pubKey = pubKey } } + + this.pubKeyHex = this.pubKey.toHexKey().intern() } override fun toString(): String = "KeyPair(privateKey=${privKey?.toHexKey()}, publicKey=${pubKey.toHexKey()}"