Groups zaps in profile by user
Moves author's zap cache to the Profile viewmodel Redesigns composables to match. Updates Notifications to not use the old zap cache.
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
-73
@@ -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<Int> {
|
||||
// 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<UserState?> {
|
||||
// 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<BigDecimal> {
|
||||
// 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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<User?>(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<String?>(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,
|
||||
)
|
||||
}
|
||||
+16
-8
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-11
@@ -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) },
|
||||
|
||||
+17
@@ -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))
|
||||
}
|
||||
|
||||
+10
@@ -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,
|
||||
|
||||
+4
-4
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
-1
@@ -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)
|
||||
|
||||
-47
@@ -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<ImmutableList<ZapReqResponse>>,
|
||||
) : LnZapFeedState()
|
||||
|
||||
object Empty : LnZapFeedState()
|
||||
|
||||
class FeedError(
|
||||
val errorMessage: String,
|
||||
) : LnZapFeedState()
|
||||
}
|
||||
-85
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-106
@@ -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<ZapReqResponse>,
|
||||
) : ViewModel() {
|
||||
private val _feedContent = MutableStateFlow<LnZapFeedState>(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<ZapReqResponse>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
+21
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-40
@@ -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() }
|
||||
}
|
||||
+115
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+4
-4
@@ -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)}")
|
||||
}
|
||||
|
||||
-50
@@ -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<ZapReqResponse>() {
|
||||
override fun feedKey(): String = user.pubkeyHex
|
||||
|
||||
override fun feed(): List<ZapReqResponse> = forProfileFeed(user.zaps)
|
||||
|
||||
override fun limit() = 400
|
||||
|
||||
companion object {
|
||||
fun forProfileFeed(zaps: Map<Note, Note?>?): List<ZapReqResponse> {
|
||||
if (zaps == null) return emptyList()
|
||||
|
||||
return (
|
||||
zaps
|
||||
.mapNotNull { entry -> entry.value?.let { ZapReqResponse(entry.key, it) } }
|
||||
.sortedBy { (it.zapEvent.event as? LnZapEvent)?.amount() }
|
||||
.reversed()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -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 <T : ViewModel> create(modelClass: Class<T>): T = UserProfileZapsFeedViewModel(user) as T
|
||||
}
|
||||
}
|
||||
+145
@@ -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<ZapAmount> =
|
||||
compareByDescending<ZapAmount> { 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<Event>.sumAmountsByUser(): List<ZapAmount> {
|
||||
val results = mutableMapOf<User, BigDecimal>()
|
||||
|
||||
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<List<ZapAmount>> =
|
||||
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 <T : ViewModel> create(modelClass: Class<T>): T = UserProfileZapsViewModel(user, account) as T
|
||||
}
|
||||
}
|
||||
@@ -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<Note, Note?>()
|
||||
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<User>.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex }
|
||||
|
||||
fun Set<User>.toSortedHexes() = map { it.pubkeyHex }.sorted()
|
||||
|
||||
Reference in New Issue
Block a user