diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 3832f39b2..be8c0a13f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -98,8 +98,6 @@ fun debugState(context: Context) { Log.d( STATE_DUMP_TAG, "Users: " + - LocalCache.users.filter { _, it -> it.flowSet != null }.size + - " / " + LocalCache.users.filter { _, it -> it.metadataOrNull() != null }.size + " / " + LocalCache.users.size(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b65847756..baa34540e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1852,13 +1852,11 @@ object LocalCache : ILocalCache, ICacheProvider { } val author = getOrCreateUser(event.pubKey) - val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = computeReplyTo(event) note.loadEvent(event, author, repliesTo) repliesTo.forEach { it.addZap(zapRequest, note) } - mentions.forEach { it.addZap(zapRequest, note) } refreshNewNoteObservers(note) @@ -1886,7 +1884,6 @@ object LocalCache : ILocalCache, ICacheProvider { note.loadEvent(event, author, repliesTo) repliesTo.forEach { it.addZap(note, null) } - mentions.forEach { it.addZap(note, null) } refreshNewNoteObservers(note) @@ -2365,7 +2362,6 @@ object LocalCache : ILocalCache, ICacheProvider { fun cleanObservers() { notes.forEach { _, it -> it.clearFlow() } addressables.forEach { _, it -> it.clearFlow() } - users.forEach { _, it -> it.clearFlow() } } fun pruneHiddenMessagesChannel( @@ -2559,18 +2555,6 @@ object LocalCache : ILocalCache, ICacheProvider { val noteEvent = note.event - if (noteEvent is LnZapEvent) { - noteEvent.zappedAuthor().forEach { - val author = getUserIfExists(it) - author?.removeZap(note) - } - } - if (noteEvent is LnZapRequestEvent) { - noteEvent.zappedAuthor().mapNotNull { - val author = getUserIfExists(it) - author?.removeZap(note) - } - } if (noteEvent is ReportEvent) { noteEvent.reportedAuthor().forEach { getUserIfExists(it.pubkey)?.reportsOrNull()?.removeReport(note) @@ -2792,7 +2776,7 @@ object LocalCache : ILocalCache, ICacheProvider { return justConsumeAndUpdateIndexes(event, relay?.url, wasVerified) } - private fun checkDeletionAndConsume( + fun checkDeletionAndConsume( event: Event, relay: NormalizedRelayUrl?, wasVerified: Boolean, 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 bb0642420..bdbf0b64d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -22,5 +22,3 @@ package com.vitorpamplona.amethyst.model // Re-export from commons for backwards compatibility typealias User = com.vitorpamplona.amethyst.commons.model.User -typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.nip01Core.RelayInfo -typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 251c228fd..3a2870fe8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent @@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.sample -import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) @Composable @@ -178,37 +176,6 @@ fun observeUserPicture( ) } -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) -@Composable -fun observeUserFollowCount( - user: User, - accountViewModel: AccountViewModel, -): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - val note = - remember(user) { - accountViewModel.follows(user) - } - - // Subscribe in the LocalCache for changes that arrive in the device - val flow = - remember(user) { - note - .flow() - .metadata.stateFlow - .mapLatest { noteState -> - (noteState.note.event as? ContactListEvent)?.followCount() ?: 0 - }.distinctUntilChanged() - .flowOn(Dispatchers.IO) - } - - return flow.collectAsStateWithLifecycle( - (note.event as? ContactListEvent)?.followCount() ?: 0, - ) -} - @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable fun observeUserTagFollowCount( @@ -440,46 +407,6 @@ fun observeUserIsFollowingChannel( return flow.collectAsStateWithLifecycle(channel.roomId in account.ephemeralChatList.liveEphemeralChatList.value) } -@Composable -fun observeUserZaps( - user: User, - accountViewModel: AccountViewModel, -): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - // Subscribe in the LocalCache for changes that arrive in the device - return user - .flow() - .zaps.stateFlow - .collectAsStateWithLifecycle() -} - -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) -@Composable -fun observeUserZapAmount( - user: User, - accountViewModel: AccountViewModel, -): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - // Subscribe in the LocalCache for changes that arrive in the device - val flow = - remember(user) { - user - .flow() - .zaps.stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.zappedAmount() - }.distinctUntilChanged() - .flowOn(Dispatchers.IO) - } - - return flow.collectAsStateWithLifecycle(BigDecimal.ZERO) -} - @Composable fun observeUserReports( user: User, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowUserButton.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowUserButton.kt index c87e7365d..6dbc752cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ShowUserButton.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps +package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonDefaults diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index f5670dac8..5db9bc229 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -24,15 +24,19 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -42,9 +46,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ListButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText @Composable fun UserCompose( @@ -141,3 +145,18 @@ fun UserComposeNoAction( } } } + +@Composable +fun AboutDisplay( + baseAuthor: User, + accountViewModel: AccountViewModel, +) { + val aboutMe by observeUserAboutMe(baseAuthor, accountViewModel) + + Text( + aboutMe, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt deleted file mode 100644 index 27374f6a0..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.note - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.sp -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse -import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import com.vitorpamplona.amethyst.ui.theme.Size55dp -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -@Composable -fun ZapNoteCompose( - baseReqResponse: ZapReqResponse, - accountViewModel: AccountViewModel, - nav: INav, -) { - val baseNoteRequest by observeNote(baseReqResponse.zapRequest, accountViewModel) - - var baseAuthor by remember { mutableStateOf(baseReqResponse.zapRequest.author) } - - LaunchedEffect(baseNoteRequest) { - accountViewModel.decryptAmountMessage(baseNoteRequest.note, baseReqResponse.zapEvent) { - baseAuthor = it?.user - } - } - - if (baseAuthor == null) { - BlankNote() - } else { - Column( - modifier = - Modifier.clickable( - onClick = { baseAuthor?.let { nav.nav(routeFor(it)) } }, - ), - verticalArrangement = Arrangement.Center, - ) { - baseAuthor?.let { RenderZapNoteSlim(it, baseReqResponse.zapEvent, accountViewModel, nav) } - } - } -} - -@Preview -@Composable -fun RenderZapNotePreview() { - val accountViewModel = mockAccountViewModel() - - val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") - val note1: Note = LocalCache.getOrCreateNote("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b") - - ThemeComparisonColumn { - RenderZapNote( - user1, - note1, - accountViewModel, - EmptyNav(), - ) - } -} - -@Preview -@Composable -fun RenderZapNoteSlimPreview() { - val accountViewModel = mockAccountViewModel() - - val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") - val note1: Note = LocalCache.getOrCreateNote("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b") - - ThemeComparisonColumn { - RenderZapNoteSlim( - user1, - note1, - accountViewModel, - EmptyNav(), - ) - } -} - -@Composable -private fun RenderZapNote( - baseAuthor: User, - zapNote: Note, - accountViewModel: AccountViewModel, - nav: INav, -) { - ListItem( - leadingContent = { - UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav) - }, - headlineContent = { - UsernameDisplay(baseAuthor, accountViewModel = accountViewModel) - }, - supportingContent = { - ZapAmount(zapNote, accountViewModel) - }, - trailingContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - UserActionOptions(baseAuthor, accountViewModel, nav) - } - }, - ) -} - -@Composable -private fun RenderZapNoteSlim( - baseAuthor: User, - zapNote: Note, - accountViewModel: AccountViewModel, - nav: INav, -) { - SlimListItem( - leadingContent = { - UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav) - }, - headlineContent = { - UsernameDisplay(baseAuthor, accountViewModel = accountViewModel) - }, - supportingContent = { - ZapAmount(zapNote, accountViewModel) - }, - trailingContent = { - Row(verticalAlignment = Alignment.CenterVertically) { - UserActionOptions(baseAuthor, accountViewModel, nav) - } - }, - ) -} - -@Composable -private fun ZapAmount( - zapEventNote: Note, - accountViewModel: AccountViewModel, -) { - val noteState by observeNote(zapEventNote, accountViewModel) - - var zapAmount by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = noteState) { - launch(Dispatchers.IO) { - val newZapAmount = showAmountInteger((noteState?.note?.event as? LnZapEvent)?.amount) - if (zapAmount != newZapAmount) { - zapAmount = newZapAmount - } - } - } - - zapAmount?.let { - Text( - text = it, - color = BitcoinOrange, - fontSize = 20.sp, - fontWeight = FontWeight.W500, - ) - } -} - -@Composable -fun AboutDisplayNoFormat( - baseAuthor: User, - accountViewModel: AccountViewModel, -) { - val aboutMe by observeUserAboutMe(baseAuthor, accountViewModel) - - Text( - aboutMe, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) -} - -@Composable -fun AboutDisplay( - baseAuthor: User, - accountViewModel: AccountViewModel, -) { - val aboutMe by observeUserAboutMe(baseAuthor, accountViewModel) - - Text( - aboutMe, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index eb0fd60a9..751ed495c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.getNoteIfExists import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.BundledInsert @@ -201,15 +202,22 @@ class CardFeedContentState( LocalCache.getUserIfExists(it) // don't create user if it doesn't exist } if (author != null) { - val zapRequest = - author.zaps - .filter { it.value == zapEvent } - .keys - .firstOrNull() + val existingZapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } + if (existingZapRequest == null || existingZapRequest.event == null) { + // tries to add it + event.zapRequest?.let { + LocalCache.checkDeletionAndConsume(it, null, false) + } + } + + val zapRequest = event.zapRequest if (zapRequest != null) { - zapsPerUser - .getOrPut(author, { mutableListOf() }) - .add(CombinedZap(zapRequest, zapEvent)) + val zapRequestNote = LocalCache.getNoteIfExists(zapRequest.id) + if (zapRequestNote != null) { + zapsPerUser + .getOrPut(author, { mutableListOf() }) + .add(CombinedZap(zapRequestNote, zapEvent)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index a9cd5b3e7..19d982a2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -93,7 +93,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.TabReports import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.TabReceivedZaps import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapTabHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size8dp @@ -169,13 +169,11 @@ fun PrepareViewModels( factory = UserAppRecommendationsFeedViewModel.Factory(baseUser), ) - val zapFeedViewModel: UserProfileZapsFeedViewModel = + val zapFeedViewModel: UserProfileZapsViewModel = viewModel( key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel", factory = - UserProfileZapsFeedViewModel.Factory( - baseUser, - ), + UserProfileZapsViewModel.Factory(baseUser, accountViewModel.account), ) val threadsViewModel: UserProfileNewThreadsFeedViewModel = @@ -253,7 +251,7 @@ fun ProfileScreen( followsFeedViewModel: UserProfileFollowsUserFeedViewModel, followersFeedViewModel: UserProfileFollowersUserFeedViewModel, appRecommendations: UserAppRecommendationsFeedViewModel, - zapFeedViewModel: UserProfileZapsFeedViewModel, + zapFeedViewModel: UserProfileZapsViewModel, bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel, galleryFeedViewModel: UserProfileGalleryFeedViewModel, reportsFeedViewModel: UserProfileReportFeedViewModel, @@ -374,7 +372,7 @@ private fun RenderScreen( appRecommendations: UserAppRecommendationsFeedViewModel, followsFeedViewModel: UserProfileFollowsUserFeedViewModel, followersFeedViewModel: UserProfileFollowersUserFeedViewModel, - zapFeedViewModel: UserProfileZapsFeedViewModel, + zapFeedViewModel: UserProfileZapsViewModel, bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel, galleryFeedViewModel: UserProfileGalleryFeedViewModel, reportsFeedViewModel: UserProfileReportFeedViewModel, @@ -396,6 +394,15 @@ private fun RenderScreen( CreateAndRenderTabs( baseUser, pagerState, + threadsViewModel, + repliesViewModel, + mutualViewModel, + followsFeedViewModel, + followersFeedViewModel, + zapFeedViewModel, + bookmarksFeedViewModel, + galleryFeedViewModel, + reportsFeedViewModel, accountViewModel, ) } @@ -431,7 +438,7 @@ private fun CreateAndRenderPages( mutualViewModel: UserProfileMutualFeedViewModel, followsFeedViewModel: UserProfileFollowsUserFeedViewModel, followersFeedViewModel: UserProfileFollowersUserFeedViewModel, - zapFeedViewModel: UserProfileZapsFeedViewModel, + zapFeedViewModel: UserProfileZapsViewModel, bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel, galleryFeedViewModel: UserProfileGalleryFeedViewModel, reportsFeedViewModel: UserProfileReportFeedViewModel, @@ -479,6 +486,15 @@ fun UpdateThreadsAndRepliesWhenBlockUnblock( private fun CreateAndRenderTabs( baseUser: User, pagerState: PagerState, + threadsViewModel: UserProfileNewThreadsFeedViewModel, + repliesViewModel: UserProfileConversationsFeedViewModel, + mutualViewModel: UserProfileMutualFeedViewModel, + followsFeedViewModel: UserProfileFollowsUserFeedViewModel, + followersFeedViewModel: UserProfileFollowersUserFeedViewModel, + zapFeedViewModel: UserProfileZapsViewModel, + bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel, + galleryFeedViewModel: UserProfileGalleryFeedViewModel, + reportsFeedViewModel: UserProfileReportFeedViewModel, accountViewModel: AccountViewModel, ) { val coroutineScope = rememberCoroutineScope() @@ -489,9 +505,9 @@ private fun CreateAndRenderTabs( { Text(text = stringRes(R.string.replies)) }, { Text(text = stringRes(R.string.mutual)) }, { Text(text = stringRes(R.string.gallery)) }, - { FollowTabHeader(baseUser, accountViewModel) }, - { FollowersTabHeader(baseUser, accountViewModel) }, - { ZapTabHeader(baseUser, accountViewModel) }, + { FollowTabHeader(followsFeedViewModel, accountViewModel) }, + { FollowersTabHeader(baseUser, followersFeedViewModel, accountViewModel) }, + { ZapTabHeader(zapFeedViewModel, accountViewModel) }, { BookmarkTabHeader(baseUser, accountViewModel) }, { FollowedTagsTabHeader(baseUser, accountViewModel) }, { ReportsTabHeader(baseUser, accountViewModel) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt index da530a8df..bb2173f43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -23,18 +23,35 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes @Composable fun FollowersTabHeader( baseUser: User, + followersFeedViewModel: UserProfileFollowersUserFeedViewModel, accountViewModel: AccountViewModel, ) { val followerCount by observeUserContactCardsFollowerCount(baseUser, accountViewModel) + if (followerCount == "--") { + FollowersTabHeaderLocal(followersFeedViewModel, accountViewModel) + } else { + Text(text = stringRes(R.string.number_followers, followerCount)) + } +} + +@Composable +fun FollowersTabHeaderLocal( + followersFeedViewModel: UserProfileFollowersUserFeedViewModel, + accountViewModel: AccountViewModel, +) { + val followerCount by followersFeedViewModel.followerCount.collectAsStateWithLifecycle() + Text(text = stringRes(R.string.number_followers, followerCount)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index 9e88f49cb..6cd03217b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -76,6 +76,16 @@ class UserProfileFollowersUserFeedViewModel( started = SharingStarted.Lazily, ) + val followerCount = + followersFlow + .map { it.size } + .flowOn(Dispatchers.IO) + .stateIn( + viewModelScope, + initialValue = 0, + started = SharingStarted.Lazily, + ) + class Factory( val user: User, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt index 58e085517..2c737b3a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt @@ -23,18 +23,18 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes @Composable fun FollowTabHeader( - baseUser: User, + followsFeedViewModel: UserProfileFollowsUserFeedViewModel, accountViewModel: AccountViewModel, ) { - val followCount by observeUserFollowCount(baseUser, accountViewModel) + val followCount by followsFeedViewModel.followCount.collectAsStateWithLifecycle() val text = if (followCount > 0) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt index 0d9c6b30e..8c41b2343 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -28,9 +28,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.note.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ListButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton @Composable fun ProfileActions( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 6b050d03c..49020a322 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -80,7 +80,6 @@ class RelayFeedViewModel : .map { relay -> val info = relayState?.get(relay) if (info != null) { - println("AABBCC convert() relays: ${relay.url} ${info.counter}") MyRelayInfo(relay, info.lastEvent, info.counter) } else { MyRelayInfo(relay, 0, 0) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt deleted file mode 100644 index 868c1fc97..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Note -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.MutableStateFlow - -@Immutable data class ZapReqResponse( - val zapRequest: Note, - val zapEvent: Note, -) - -@Stable -sealed class LnZapFeedState { - object Loading : LnZapFeedState() - - class Loaded( - val feed: MutableStateFlow>, - ) : LnZapFeedState() - - object Empty : LnZapFeedState() - - class FeedError( - val errorMessage: String, - ) : LnZapFeedState() -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt deleted file mode 100644 index b82dc987e..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps - -import androidx.compose.animation.core.tween -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty -import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed -import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.FeedPadding - -@Composable -fun LnZapFeedView( - viewModel: LnZapFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedState by viewModel.feedContent.collectAsStateWithLifecycle() - - CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state -> - when (state) { - is LnZapFeedState.Empty -> { - FeedEmpty { viewModel.invalidateData() } - } - is LnZapFeedState.FeedError -> { - FeedError(state.errorMessage) { viewModel.invalidateData() } - } - is LnZapFeedState.Loaded -> { - LnZapFeedLoaded(state, accountViewModel, nav) - } - is LnZapFeedState.Loading -> { - LoadingFeed() - } - } - } -} - -@Composable -private fun LnZapFeedLoaded( - state: LnZapFeedState.Loaded, - accountViewModel: AccountViewModel, - nav: INav, -) { - val items by state.feed.collectAsStateWithLifecycle() - val listState = rememberLazyListState() - - LazyColumn( - contentPadding = FeedPadding, - state = listState, - ) { - itemsIndexed(items, key = { _, item -> item.zapEvent.idHex }) { _, item -> - ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) - HorizontalDivider(thickness = DividerThickness) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt deleted file mode 100644 index 7fa6baad1..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps - -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.BundledUpdate -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.quartz.utils.Log -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch - -@Stable -open class LnZapFeedViewModel( - val dataSource: FeedFilter, -) : ViewModel() { - private val _feedContent = MutableStateFlow(LnZapFeedState.Loading) - val feedContent = _feedContent.asStateFlow() - - private fun refresh() { - viewModelScope.launch(Dispatchers.IO) { refreshSuspended() } - } - - private fun refreshSuspended() { - checkNotInMainThread() - val notes = dataSource.loadTop().toImmutableList() - - val oldNotesState = _feedContent.value - if (oldNotesState is LnZapFeedState.Loaded) { - // Using size as a proxy for has changed. - if (!equalImmutableLists(notes, oldNotesState.feed.value)) { - updateFeed(notes) - } - } else { - updateFeed(notes) - } - } - - private fun updateFeed(notes: ImmutableList) { - val currentState = _feedContent.value - if (notes.isEmpty()) { - _feedContent.tryEmit(LnZapFeedState.Empty) - } else if (currentState is LnZapFeedState.Loaded) { - // updates the current list - currentState.feed.tryEmit(notes) - } else { - _feedContent.tryEmit(LnZapFeedState.Loaded(MutableStateFlow(notes))) - } - } - - private val bundler = BundledUpdate(250, Dispatchers.IO) - - fun invalidateData() { - bundler.invalidate { - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - refreshSuspended() - } - } - - init { - Log.d("Init", "${this.javaClass.simpleName}") - viewModelScope.launch(Dispatchers.IO) { - LocalCache.live.newEventBundles.collect { newNotes -> - invalidateData() - } - } - viewModelScope.launch(Dispatchers.IO) { - LocalCache.live.deletedEventBundles.collect { newNotes -> - invalidateData() - } - } - } - - override fun onCleared() { - Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") - bundler.cancel() - super.onCleared() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt index e51280ff2..66fd1a995 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt @@ -22,23 +22,39 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding @Composable fun TabReceivedZaps( baseUser: User, - zapFeedViewModel: UserProfileZapsFeedViewModel, + zapFeedViewModel: UserProfileZapsViewModel, accountViewModel: AccountViewModel, nav: INav, ) { - WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel, accountViewModel) - Column(Modifier.fillMaxHeight()) { - LnZapFeedView(zapFeedViewModel, accountViewModel, nav) + val feedState by zapFeedViewModel.receivedZapAmountsByUser.collectAsStateWithLifecycle() + + LazyColumn( + contentPadding = FeedPadding, + state = rememberLazyListState(), + ) { + itemsIndexed(feedState, key = { _, item -> item.user.pubkeyHex }) { _, item -> + ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) + HorizontalDivider(thickness = DividerThickness) + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt deleted file mode 100644 index 4930c1184..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZaps -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel - -@Composable -fun WatchZapsAndUpdateFeed( - baseUser: User, - feedViewModel: UserProfileZapsFeedViewModel, - accountViewModel: AccountViewModel, -) { - val userState by observeUserZaps(baseUser, accountViewModel) - - LaunchedEffect(userState) { feedViewModel.invalidateData() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt new file mode 100644 index 000000000..0ceda0eea --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.note.UserActionOptions +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.showAmountInteger +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.ZapAmount +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.quartz.utils.BigDecimal + +@Composable +fun ZapNoteCompose( + zap: ZapAmount, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column( + modifier = + Modifier.clickable( + onClick = { zap.user.let { nav.nav(routeFor(it)) } }, + ), + verticalArrangement = Arrangement.Center, + ) { + RenderZapNoteSlim(zap, accountViewModel, nav) + } +} + +@Preview +@Composable +fun RenderZapNoteSlimPreview() { + val accountViewModel = mockAccountViewModel() + + val user1: User = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + + ThemeComparisonColumn { + RenderZapNoteSlim( + ZapAmount(user1, BigDecimal.TEN), + accountViewModel, + EmptyNav(), + ) + } +} + +@Composable +private fun RenderZapNoteSlim( + zap: ZapAmount, + accountViewModel: AccountViewModel, + nav: INav, +) { + SlimListItem( + leadingContent = { + UserPicture(zap.user, Size55dp, accountViewModel = accountViewModel, nav = nav) + }, + headlineContent = { + UsernameDisplay(zap.user, accountViewModel = accountViewModel) + }, + supportingContent = { + Text( + text = remember { showAmountInteger(zap.amount) }, + color = BitcoinOrange, + fontSize = 20.sp, + fontWeight = FontWeight.W500, + ) + }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + UserActionOptions(zap.user, accountViewModel, nav) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt index c2d73597a..220e2f48a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -23,19 +23,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserZapAmount import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel import com.vitorpamplona.amethyst.ui.stringRes @Composable fun ZapTabHeader( - baseUser: User, + zapsViewModel: UserProfileZapsViewModel, accountViewModel: AccountViewModel, ) { - val zapAmount by observeUserZapAmount(baseUser, accountViewModel) + val zapAmount by zapsViewModel.totalReceivedZaps.collectAsStateWithLifecycle() Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt deleted file mode 100644 index 06ea5284c..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedFilter.kt +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal - -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent - -class UserProfileZapsFeedFilter( - val user: User, -) : FeedFilter() { - override fun feedKey(): String = user.pubkeyHex - - override fun feed(): List = forProfileFeed(user.zaps) - - override fun limit() = 400 - - companion object { - fun forProfileFeed(zaps: Map?): List { - if (zaps == null) return emptyList() - - return ( - zaps - .mapNotNull { entry -> entry.value?.let { ZapReqResponse(entry.key, it) } } - .sortedBy { (it.zapEvent.event as? LnZapEvent)?.amount() } - .reversed() - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt deleted file mode 100644 index 40ef049d6..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsFeedViewModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.LnZapFeedViewModel - -class UserProfileZapsFeedViewModel( - user: User, -) : LnZapFeedViewModel(UserProfileZapsFeedFilter(user)) { - class Factory( - val user: User, - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = UserProfileZapsFeedViewModel(user) as T - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt new file mode 100644 index 000000000..3e77bdc04 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.utils.BigDecimal +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.flow.stateIn + +@Immutable +data class ZapAmount( + val user: User, + val amount: BigDecimal, +) + +@Stable +class UserProfileZapsViewModel( + val user: User, + val account: Account, +) : ViewModel() { + val zapsToUser = + Filter( + kinds = listOf(LnZapEvent.KIND), + tags = mapOf("p" to listOf(user.pubkeyHex)), + ) + + val sortingModel: Comparator = + compareByDescending { it.amount }.thenBy { it.user.pubkeyHex } + + suspend fun mapRequest(zapEvent: LnZapEvent): ZapAmount? { + val zapRequest = + zapEvent.zapRequest ?: return ZapAmount( + LocalCache.getOrCreateUser(zapEvent.pubKey), + zapEvent.amount ?: BigDecimal.ZERO, + ) + + return if (zapRequest.isPrivateZap()) { + // if user is not the logged in, we cannot decrypt. + if (user.pubkeyHex == account.pubKey) { + val cachedPrivateRequest = account.privateZapsDecryptionCache.decryptPrivateZap(zapRequest) + if (cachedPrivateRequest != null) { + ZapAmount( + LocalCache.getOrCreateUser(cachedPrivateRequest.pubKey), + zapEvent.amount ?: BigDecimal.ZERO, + ) + } else { + ZapAmount( + LocalCache.getOrCreateUser(zapRequest.pubKey), + zapEvent.amount ?: BigDecimal.ZERO, + ) + } + } else { + ZapAmount( + LocalCache.getOrCreateUser(zapRequest.pubKey), + zapEvent.amount ?: BigDecimal.ZERO, + ) + } + } else { + ZapAmount( + LocalCache.getOrCreateUser(zapRequest.pubKey), + zapEvent.amount ?: BigDecimal.ZERO, + ) + } + } + + suspend fun List.sumAmountsByUser(): List { + val results = mutableMapOf() + + this.forEach { zapEvent -> + if (zapEvent is LnZapEvent) { + val zapAmount = mapRequest(zapEvent) + if (zapAmount != null) { + val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO + results[zapAmount.user] = existingAmount + zapAmount.amount + } + } + } + + return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel) + } + + val receivedZapAmountsByUser: StateFlow> = + account.cache + .observeEvents(zapsToUser) + .sample(500) + .map { zapEvents -> + zapEvents.sumAmountsByUser() + }.flowOn(Dispatchers.IO) + .stateIn( + viewModelScope, + initialValue = emptyList(), + started = SharingStarted.Lazily, + ) + + val totalReceivedZaps = + receivedZapAmountsByUser + .map { it.sumOf { it.amount } } + .flowOn(Dispatchers.IO) + .stateIn( + viewModelScope, + initialValue = BigDecimal.ZERO, + started = SharingStarted.Lazily, + ) + + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = UserProfileZapsViewModel(user, account) as T + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index aa807363f..99a37e060 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -35,11 +35,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Hex -import java.math.BigDecimal interface UserDependencies @@ -58,11 +56,6 @@ class User( private var status: UserStatusCache? = null private var relays: UserRelaysCache? = null - var zaps = mapOf() - private set - - var flowSet: UserFlowSet? = null - fun pubkey() = Hex.decode(pubkeyHex) fun pubkeyNpub() = pubkey().toNpub() @@ -101,38 +94,6 @@ class User( fun lnAddress(): String? = metadataOrNull()?.lnAddress() - fun addZap( - zapRequest: Note, - zap: Note?, - ) { - if (zaps[zapRequest] == null) { - zaps = zaps + Pair(zapRequest, zap) - flowSet?.zaps?.invalidateData() - } - } - - fun removeZap(zapRequestOrZapEvent: Note) { - if (zaps.containsKey(zapRequestOrZapEvent)) { - zaps = zaps.minus(zapRequestOrZapEvent) - flowSet?.zaps?.invalidateData() - } else if (zaps.containsValue(zapRequestOrZapEvent)) { - zaps = zaps.filter { it.value != zapRequestOrZapEvent } - flowSet?.zaps?.invalidateData() - } - } - - fun zappedAmount(): BigDecimal { - var amount = BigDecimal.ZERO - zaps.forEach { - val itemValue = (it.value?.event as? LnZapEvent)?.amount - if (itemValue != null) { - amount += itemValue - } - } - - return amount - } - fun addRelayBeingUsed( relay: NormalizedRelayUrl, eventTime: Long, @@ -186,47 +147,8 @@ class User( return metadataOrNull()?.containsAny(hiddenWordsCase) == true } - - @Synchronized - fun createOrDestroyFlowSync(create: Boolean) { - if (create) { - if (flowSet == null) { - flowSet = UserFlowSet(this) - } - } else { - if (flowSet != null && flowSet?.isInUse() == false) { - flowSet = null - } - } - } - - fun flow(): UserFlowSet { - if (flowSet == null) { - createOrDestroyFlowSync(true) - } - return flowSet!! - } - - fun clearFlow() { - if (flowSet != null && flowSet?.isInUse() == false) { - createOrDestroyFlowSync(false) - } - } } -@Stable -class UserFlowSet( - u: User, -) { - val zaps = UserBundledRefresherFlow(u) - - fun isInUse(): Boolean = zaps.hasObservers() -} - -// Re-export from commons.state for backwards compatibility -typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.state.UserMetadataState -typealias UserState = com.vitorpamplona.amethyst.commons.state.UserState - fun Set.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex } fun Set.toSortedHexes() = map { it.pubkeyHex }.sorted()