From 86b49570675e763937b003fa6c4440ec1d327eb2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 7 Aug 2025 15:35:59 -0400 Subject: [PATCH 01/21] Removes NWC events from notificatuions --- .../loggedIn/notifications/dal/NotificationFeedFilter.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 460224419..7aeb8571d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -40,6 +40,8 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent @@ -129,6 +131,8 @@ class NotificationFeedFilter( it.event !is NIP90ContentDiscoveryRequestEvent && it.event !is GiftWrapEvent && it.event !is PrivateTagArrayEvent && + it.event !is LnZapPaymentRequestEvent && + it.event !is LnZapPaymentResponseEvent && (it.event is LnZapEvent || notifAuthor != loggedInUserHex) && (filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && it.event?.isTaggedUser(loggedInUserHex) ?: false && From 10c5db82810a2f46fea5a69b2e298fb9af1e118e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 7 Aug 2025 16:07:15 -0400 Subject: [PATCH 02/21] Fixes the encryption of NWC --- .../amethyst/model/nip47WalletConnect/NwcSignerState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 9444bedba..1cfa440ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -153,7 +153,7 @@ class NwcSignerState( val walletService = settings.zapPaymentRequest.value if (walletService == null) throw IllegalArgumentException("No NIP47 setup") - val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, signer) + val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value) val filter = NWCPaymentQueryState( From 8ba96ca52765497ee387718c9d2215b32635904b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 7 Aug 2025 16:39:44 -0400 Subject: [PATCH 03/21] Simplifies Zap animation sizes --- .../java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 6 +++--- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/video/VideoScreen.kt | 3 +-- .../main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 485d665cd..76927db46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -135,7 +135,7 @@ import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightWithPadding import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiser import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiserWithPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing -import com.vitorpamplona.amethyst.ui.theme.Size14dp +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier import com.vitorpamplona.amethyst.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.ui.theme.Size18dp import com.vitorpamplona.amethyst.ui.theme.Size19Modifier @@ -977,7 +977,7 @@ fun ZapReaction( accountViewModel: AccountViewModel, iconSize: Dp = Size20dp, iconSizeModifier: Modifier = Size20Modifier, - animationSize: Dp = Size14dp, + animationModifier: Modifier = Size14Modifier, nav: INav, ) { var wantsToZap by remember { mutableStateOf(false) } @@ -1108,7 +1108,7 @@ fun ZapReaction( CircularProgressIndicator( progress = { animatedProgress }, - modifier = remember { Modifier.size(animationSize) }, + modifier = animationModifier, strokeWidth = 2.dp, ) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 3939c9a8c..4005fbe69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -658,7 +658,7 @@ class AccountViewModel( showErrorIfNoLnAddress = showErrorIfNoLnAddress, okHttpClient = ::okHttpClientForMoney, onError = onError, - onProgress = { onProgress(it) }, + onProgress = onProgress, onPayViaIntent = onPayViaIntent, zapType = zapType ?: defaultZapType(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 0d6e5aafe..e23281965 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -93,7 +93,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfFeedPadding import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size35Modifier -import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size40Modifier import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size55dp @@ -455,7 +454,7 @@ fun ReactionsColumn( accountViewModel = accountViewModel, iconSize = Size40dp, iconSizeModifier = Size40Modifier, - animationSize = Size35dp, + animationModifier = Size35Modifier, nav = nav, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index ecae26927..752cb9ce3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -146,6 +146,7 @@ val MaxWidthWithHorzPadding = Modifier.fillMaxWidth().padding(horizontal = 10.dp val Size5Modifier = Modifier.size(5.dp) val Size10Modifier = Modifier.size(10.dp) +val Size14Modifier = Modifier.size(14.dp) val Size15Modifier = Modifier.size(15.dp) val Size16Modifier = Modifier.size(16.dp) val Size17Modifier = Modifier.size(17.dp) From 2315051504d16e93a7d7b91c78fb47171de689c1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 7 Aug 2025 18:02:31 -0400 Subject: [PATCH 04/21] - Makes zap calculation a sync routine - Optimizes the display of the yellow zap when the zap event comes before the pay payment confirmation - Fixes animations for a new zap over an existing zap. --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../com/vitorpamplona/amethyst/model/Note.kt | 111 ++++++++++-------- .../amethyst/ui/note/PollNote.kt | 13 +- .../amethyst/ui/note/PollNoteViewModel.kt | 12 +- .../amethyst/ui/note/ReactionsRow.kt | 48 ++++++-- .../amethyst/ui/note/ZapCustomDialog.kt | 2 + .../ui/note/elements/ZapTheDevsCard.kt | 44 +++++-- .../ui/screen/loggedIn/AccountViewModel.kt | 7 +- .../dvms/DvmContentDiscoveryScreen.kt | 29 ++++- 9 files changed, 175 insertions(+), 97 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f0c81c046..ec529464a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -516,10 +516,8 @@ class Account( suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note?, - onWasZapped: () -> Unit, - ) { - zappedNote?.isZappedBy(userProfile(), this, onWasZapped) - } + afterTimeInSeconds: Long, + ): Boolean = zappedNote?.isZappedBy(userProfile(), afterTimeInSeconds, this) == true suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 03158d3ee..19c2d9b04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import coil3.util.CoilUtils.result import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -472,28 +473,28 @@ open class Note( } private suspend fun isPaidByCalculation( + zapPayments: List>, + afterTimeInSeconds: Long, account: Account, - zapEvents: List>, - onWasZappedByAuthor: () -> Unit, - ) { - if (zapEvents.isEmpty()) { - return + ): Boolean { + if (zapPayments.isEmpty()) { + return false } - var hasSentOne = false - - launchAndWaitAll(zapEvents) { next -> + return anyAsync(zapPayments) { next -> val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent if (zapResponseEvent != null) { - account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response -> - val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) - - if (!hasSentOne && result == true) { - hasSentOne = true - onWasZappedByAuthor() - } + val response = account.nip47SignerState.decryptResponse(zapResponseEvent) + if (response != null) { + response is PayInvoiceSuccessResponse && + account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) && + zapResponseEvent.createdAt > afterTimeInSeconds + } else { + false } + } else { + false } } } @@ -501,75 +502,81 @@ open class Note( private suspend fun isZappedByCalculation( option: Int?, user: User, + afterTimeInSeconds: Long, account: Account, zapEvents: Map, - onWasZappedByAuthor: () -> Unit, - ) { + ): Boolean { if (zapEvents.isEmpty()) { - return + return false } - val parallelDecrypt = mutableListOf>() + val parallelDecrypt = mutableListOf>() zapEvents.forEach { next -> val zapRequest = next.key.event as LnZapRequestEvent val zapEvent = next.value?.event as? LnZapEvent - if (!zapRequest.isPrivateZap()) { - // public events - if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { - onWasZappedByAuthor() - return - } - } else { - // private events - - // if has already decrypted - val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest) - if (privateZap != null) { - if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { - onWasZappedByAuthor() - return + if (zapEvent != null) { + if (!zapRequest.isPrivateZap()) { + // public events + if (zapRequest.pubKey == user.pubkeyHex && + zapEvent.createdAt > afterTimeInSeconds && + (option == null || option == zapEvent.zappedPollOption()) + ) { + return true } } else { - if (account.isWriteable()) { - parallelDecrypt.add(Pair(zapRequest, zapEvent)) + // private events + + // if has already decrypted + val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest) + if (privateZap != null) { + if (privateZap.pubKey == user.pubkeyHex && + zapEvent.createdAt > afterTimeInSeconds && + (option == null || option == zapEvent.zappedPollOption()) + ) { + return true + } + } else { + if (account.isWriteable()) { + parallelDecrypt.add(Pair(zapRequest, zapEvent)) + } } } } } - val result = - anyAsync(parallelDecrypt) { pair -> - val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) + if (parallelDecrypt.isEmpty()) { + return false + } - result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption()) - } - - if (result) { - onWasZappedByAuthor() + return anyAsync(parallelDecrypt) { pair -> + val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) + result?.pubKey == user.pubkeyHex && + pair.second.createdAt > afterTimeInSeconds && + (option == null || option == pair.second.zappedPollOption()) } } suspend fun isZappedBy( user: User, + afterTimeInSeconds: Long, account: Account, - onWasZappedByAuthor: () -> Unit, - ) { - isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor) + ): Boolean { + val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps) + if (first) return true if (account.userProfile() == user) { - isPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor) + return isPaidByCalculation(zapPayments.toList(), afterTimeInSeconds, account) } + return false } suspend fun isZappedBy( option: Int?, user: User, + afterTimeInSeconds: Long, account: Account, - onWasZappedByAuthor: () -> Unit, - ) { - isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor) - } + ): Boolean = isZappedByCalculation(option, user, afterTimeInSeconds, account, zaps) fun getReactionBy(user: User): String? = reactions.firstNotNullOfOrNull { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index c448e0179..f7473724a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -90,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -100,6 +102,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -517,6 +520,8 @@ fun ZapVote( } var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } + var showErrorMessageDialog by remember { mutableStateOf(null) } val context = LocalContext.current @@ -558,6 +563,7 @@ fun ZapVote( accountViewModel.zapAmountChoices().size == 1 && pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first()) ) { + zapStartingTime = TimeUtils.now() accountViewModel.zap( baseNote, accountViewModel.zapAmountChoices().first() * 1000, @@ -583,6 +589,7 @@ fun ZapVote( accountViewModel, pollViewModel, poolOption.option, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -660,9 +667,10 @@ fun ZapVote( ) } else { Spacer(Modifier.width(3.dp)) + CircularProgressIndicator( progress = { zappingProgress }, - modifier = Modifier.size(14.dp), + modifier = Size14Modifier, strokeWidth = 2.dp, ) } @@ -688,6 +696,7 @@ fun FilteredZapAmountChoicePopup( accountViewModel: AccountViewModel, pollViewModel: PollNoteViewModel, pollOption: Int, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, toUser: User?) -> Unit, @@ -717,6 +726,7 @@ fun FilteredZapAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, @@ -743,6 +753,7 @@ fun FilteredZapAmountChoicePopup( modifier = Modifier.combinedClickable( onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 369252068..9bee68081 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -103,10 +103,8 @@ class PollNoteViewModel : ViewModel() { viewModelScope.launch(Dispatchers.Default) { totalZapped = totalZapped() wasZappedByLoggedInAccount = false - account?.calculateIfNoteWasZappedByAccount(pollNote) { - wasZappedByLoggedInAccount = true - canZap.value = checkIfCanZap() - } + wasZappedByLoggedInAccount = account.calculateIfNoteWasZappedByAccount(pollNote, 0) + canZap.value = checkIfCanZap() tallies.forEach { val zappedValue = zappedPollOptionAmount(it.option) @@ -210,10 +208,8 @@ class PollNoteViewModel : ViewModel() { suspend fun isPollOptionZappedBy( option: Int, user: User, - onWasZappedByAuthor: () -> Unit, - ) { - pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor) - } + afterTimeInSeconds: Long, + ): Boolean = pollNote?.isZappedBy(option, user, afterTimeInSeconds, account) == true fun cachedIsPollOptionZappedBy( option: Int, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 76927db46..aa410cad6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -66,6 +66,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -154,11 +155,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -994,6 +997,7 @@ fun ZapReaction( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } Row( verticalAlignment = CenterVertically, @@ -1008,6 +1012,7 @@ fun ZapReaction( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, onMultipleChoices = { scope.launch { @@ -1033,6 +1038,7 @@ fun ZapReaction( baseNote = baseNote, popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -1083,6 +1089,7 @@ fun ZapReaction( if (wantsToSetCustomZap) { ZapCustomDialog( + onZapStarts = { zapStartingTime = TimeUtils.now() }, onClose = { wantsToSetCustomZap = false }, onError = { _, message, user -> scope.launch { @@ -1106,11 +1113,23 @@ fun ZapReaction( label = "ZapIconIndicator", ) - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = animationModifier, - strokeWidth = 2.dp, - ) + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = animationModifier, + strokeWidth = 2.dp, + ) + } + } + } } else { ObserveZapIcon( baseNote, @@ -1136,6 +1155,7 @@ fun zapClick( baseNote: Note, accountViewModel: AccountViewModel, context: Context, + onZapStarts: () -> Unit, onZappingProgress: (Float) -> Unit, onMultipleChoices: () -> Unit, onError: (String, String, User?) -> Unit, @@ -1162,6 +1182,7 @@ fun zapClick( R.string.login_with_a_private_key_to_be_able_to_send_zaps, ) } else if (choices.size == 1) { + onZapStarts() accountViewModel.zap( baseNote, choices.first() * 1000, @@ -1181,6 +1202,7 @@ fun zapClick( fun ObserveZapIcon( baseNote: Note, accountViewModel: AccountViewModel, + afterTimeInSeconds: Long = 0, inner: @Composable (MutableState) -> Unit, ) { val wasZappedByLoggedInUser = remember { mutableStateOf(false) } @@ -1196,10 +1218,9 @@ fun ObserveZapIcon( LaunchedEffect(key1 = zapsState) { if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) { - accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped -> - if (wasZappedByLoggedInUser.value != newWasZapped) { - wasZappedByLoggedInUser.value = newWasZapped - } + val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, afterTimeInSeconds) + if (wasZappedByLoggedInUser.value != newWasZapped) { + wasZappedByLoggedInUser.value = newWasZapped } } } @@ -1552,6 +1573,7 @@ fun ZapAmountChoicePopup( baseNote: Note, accountViewModel: AccountViewModel, popupYOffset: Dp, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, @@ -1562,7 +1584,7 @@ fun ZapAmountChoicePopup( accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices .collectAsStateWithLifecycle() - ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) + ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onZapStarts, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) } @Composable @@ -1571,6 +1593,7 @@ fun ZapAmountChoicePopup( zapAmountChoices: ImmutableList, accountViewModel: AccountViewModel, popupYOffset: Dp, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, @@ -1578,7 +1601,7 @@ fun ZapAmountChoicePopup( onPayViaIntent: (ImmutableList) -> Unit, ) { val visibilityState = rememberVisibilityState(onDismiss) - ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent) + ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onZapStarts, onChangeAmount, onError, onProgress, onPayViaIntent) } @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @@ -1589,6 +1612,7 @@ fun ZapAmountChoicePopup( accountViewModel: AccountViewModel, popupYOffset: Dp, visibilityState: MutableTransitionState, + onZapStarts: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, onProgress: (percent: Float) -> Unit, @@ -1615,6 +1639,7 @@ fun ZapAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, @@ -1641,6 +1666,7 @@ fun ZapAmountChoicePopup( modifier = Modifier.combinedClickable( onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 0ebb39184..7526b3e9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -103,6 +103,7 @@ class ZapOptionViewModel : ViewModel() { @Composable fun ZapCustomDialog( + onZapStarts: () -> Unit, onClose: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, onProgress: (percent: Float) -> Unit, @@ -172,6 +173,7 @@ fun ZapCustomDialog( ZapButton( isActive = postViewModel.canSend() && !baseNote.isDraft(), ) { + onZapStarts() accountViewModel.zap( baseNote, postViewModel.value()!! * 1000L, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt index 1826aa018..7fd70dcc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -83,12 +84,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -285,7 +288,7 @@ fun ZapDonationButton( accountViewModel: AccountViewModel, iconSize: Dp = Size35dp, iconSizeModifier: Modifier = Size20Modifier, - animationSize: Dp = 14.dp, + animationModifier: Modifier = Size14Modifier, nav: INav, ) { var wantsToZap by remember { mutableStateOf?>(null) } @@ -300,6 +303,8 @@ fun ZapDonationButton( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } + var hasZapped by remember { mutableStateOf(false) } Button( @@ -308,6 +313,7 @@ fun ZapDonationButton( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, @@ -329,6 +335,7 @@ fun ZapDonationButton( zapAmountChoices = it, popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = null zappingProgress = 0f @@ -376,17 +383,30 @@ fun ZapDonationButton( if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { Spacer(ModifierWidth3dp) - CircularProgressIndicator( - progress = - animateFloatAsState( - targetValue = zappingProgress, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, - label = "ZapIconIndicator", - ).value, - modifier = remember { Modifier.size(animationSize) }, - strokeWidth = 2.dp, - color = grayTint, + val animatedProgress by animateFloatAsState( + targetValue = zappingProgress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = "ZapIconIndicator", ) + + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = animationModifier, + strokeWidth = 2.dp, + color = grayTint, + ) + } + } + } } else { ObserveZapIcon( baseNote, @@ -423,6 +443,7 @@ fun customZapClick( baseNote: Note, accountViewModel: AccountViewModel, context: Context, + onZapStarts: () -> Unit, onZappingProgress: (Float) -> Unit, onMultipleChoices: (List) -> Unit, onError: (String, String, User?) -> Unit, @@ -452,6 +473,7 @@ fun customZapClick( val amount = choices.first() if (amount > 1100) { + onZapStarts() accountViewModel.zap( baseNote, amount * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 4005fbe69..a40262366 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -395,12 +395,11 @@ class AccountViewModel( suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note, - onWasZapped: (Boolean) -> Unit, - ) { + afterTimeInSeconds: Long, + ): Boolean = withContext(Dispatchers.IO) { - account.calculateIfNoteWasZappedByAccount(zappedNote) { onWasZapped(true) } + account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds) } - } suspend fun calculateZapAmount(zappedNote: Note): String = if (zappedNote.zapPayments.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 1aefd5a1b..d197e8029 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -97,6 +98,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -472,6 +474,7 @@ fun ZapDVMButton( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } var hasZapped by remember { mutableStateOf(false) } Button( @@ -480,6 +483,7 @@ fun ZapDVMButton( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, @@ -501,6 +505,7 @@ fun ZapDVMButton( zapAmountChoices = persistentListOf(amount / 1000), popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = null zappingProgress = 0f @@ -554,12 +559,24 @@ fun ZapDVMButton( label = "ZapIconIndicator", ) - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = remember { Modifier.size(animationSize) }, - strokeWidth = 2.dp, - color = grayTint, - ) + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = remember { Modifier.size(animationSize) }, + strokeWidth = 2.dp, + color = grayTint, + ) + } + } + } } else { ObserveZapIcon( baseNote, From d5b9d5e80b54b7c8b89f76df264ae26dfea14fc7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 11 Aug 2025 10:33:21 -0400 Subject: [PATCH 05/21] Closes subscription when finished by timeout. --- .../relay/client/accessories/NostrClientSingleDownloadExt.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt index 62869bfbb..a5abcb338 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt @@ -49,7 +49,6 @@ fun NostrClient.downloadFirstEvent( if (subId == subscriptionId) { unsubscribe(this) close(subscriptionId) - onResponse(event) } } @@ -61,6 +60,8 @@ fun NostrClient.downloadFirstEvent( GlobalScope.launch(Dispatchers.IO) { delay(30000) + + close(subscriptionId) unsubscribe(listener) } } From d0f3dde08b706cdefe81303d0c920dfbfaaa4d4c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 11 Aug 2025 10:34:10 -0400 Subject: [PATCH 06/21] Migrates to a more obvious open/close subscription model for NostrClient --- .../quartz/nip01Core/relay/client/NostrClient.kt | 4 ++-- .../relay/client/accessories/NostrClientSingleDownloadExt.kt | 2 +- .../relay/client/subscriptions/SubscriptionController.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index d741251b7..4dd823d66 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -206,7 +206,7 @@ class NostrClient( return false } - fun sendRequest( + fun openReqSubscription( subId: String = newSubId(), filters: Map>, ) { @@ -239,7 +239,7 @@ class NostrClient( } } - fun sendCount( + fun openCountSubscription( subId: String = newSubId(), filters: Map>, ) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt index a5abcb338..5d07db9ef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt @@ -56,7 +56,7 @@ fun NostrClient.downloadFirstEvent( subscribe(listener) - sendRequest(subscriptionId, filters) + openReqSubscription(subscriptionId, filters) GlobalScope.launch(Dispatchers.IO) { delay(30000) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt index dac7e7f2d..9e5c57d43 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt @@ -126,14 +126,14 @@ class SubscriptionController( // was active and is not active anymore, just close. client.close(subId) } else { - client.sendRequest(subId, updatedFilters) + client.openReqSubscription(subId, updatedFilters) } } else { if (updatedFilters == null) { // was not active and is still not active, does nothing } else { // was not active and becomes active, sends the entire filter. - client.sendRequest(subId, updatedFilters) + client.openReqSubscription(subId, updatedFilters) } } } From 77bb34eaf840c878c735aab4bc3aae175de0b207 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 08:47:51 -0400 Subject: [PATCH 07/21] Updates tor to 16KB-page files and firebase --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e4ba2464..bf3d71a48 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ composeBom = "2025.07.00" coreKtx = "1.16.0" datastore = "1.1.7" espressoCore = "3.7.0" -firebaseBom = "34.0.0" +firebaseBom = "34.1.0" fragmentKtx = "1.8.8" gms = "4.4.3" jacksonModuleKotlin = "2.19.2" @@ -43,7 +43,7 @@ rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.18.0" securityCryptoKtx = "1.1.0" spotless = "7.2.1" -torAndroid = "0.4.8.17" +torAndroid = "0.4.8.17.1" translate = "17.0.3" unifiedpush = "3.0.10" urlDetector = "0.1.23" From 7a382694494bbf9b24ca23a9b86a43fa36e5e0b1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 11:29:34 -0400 Subject: [PATCH 08/21] removing unused libs --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf3d71a48..9987bbbc9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -111,7 +111,6 @@ jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" } lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" } markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" } From 14582fe9b6af4f7b6641da62b3bcb3093a888631 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 11:29:58 -0400 Subject: [PATCH 09/21] Recalculating subject and last message when deletes happen --- .../amethyst/model/privateChats/Chatroom.kt | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 969e84bb4..40360848e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -78,14 +78,25 @@ class Chatroom : NotesGatherer { messages = messages - msg msg.removeGatherer(this) - messages - .filter { it.event?.subject() != null } - .sortedBy { it.createdAt() } - .lastOrNull() - ?.let { - subject.tryEmit(it.event?.subject()) - subjectCreatedAt = it.createdAt() - } + if (msg == lastMessage) { + lastMessage = messages.maxByOrNull { it.createdAt() ?: 0 } + } + + if (msg.event?.subject() == subject.value) { + messages + .maxByOrNull { + val noteEvent = it.event + if (noteEvent?.subject() != null) { + noteEvent.createdAt + } else { + 0 + } + }?.let { + subject.tryEmit(it.event?.subject()) + subjectCreatedAt = it.createdAt() + } + } + return true } return false From 25aaed80b0723be72db73a8bd2608afb513b7600 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 11:43:57 -0400 Subject: [PATCH 10/21] Better name for the newestMessage of a chat --- .../amethyst/model/privateChats/Chatroom.kt | 10 +++++----- .../chats/rooms/dal/ChatroomListKnownFeedFilter.kt | 2 +- .../chats/rooms/dal/ChatroomListNewFeedFilter.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 40360848e..31b3ff3db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -38,7 +38,7 @@ class Chatroom : NotesGatherer { var subject = MutableStateFlow(null) var subjectCreatedAt: Long? = null var ownerSentMessage: Boolean = false - var lastMessage: Note? = null + var newestMessage: Note? = null override fun removeNote(note: Note) { removeMessageSync(note) @@ -57,8 +57,8 @@ class Chatroom : NotesGatherer { } val createdAt = msg.createdAt() ?: 0 - if (createdAt > (lastMessage?.createdAt() ?: 0)) { - lastMessage = msg + if (createdAt > (newestMessage?.createdAt() ?: 0)) { + newestMessage = msg } val newSubject = msg.event?.subject() @@ -78,8 +78,8 @@ class Chatroom : NotesGatherer { messages = messages - msg msg.removeGatherer(this) - if (msg == lastMessage) { - lastMessage = messages.maxByOrNull { it.createdAt() ?: 0 } + if (msg == newestMessage) { + newestMessage = messages.maxByOrNull { it.createdAt() ?: 0 } } if (msg.event?.subject() == subject.value) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index debe99060..09bc8a4b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -47,7 +47,7 @@ class ChatroomListKnownFeedFilter( if ((chatroom.senderIntersects(followingKeySet) || chatList.hasSentMessagesTo(key)) && !account.isAllHidden(key.users) ) { - chatroom.lastMessage + chatroom.newestMessage } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 920c4bb1c..ffc24da83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -41,7 +41,7 @@ class ChatroomListNewFeedFilter( val privateMessages = chatList.rooms.mapNotNull { key, chatroom -> if (!chatroom.senderIntersects(followingKeySet) && !chatList.hasSentMessagesTo(key) && !account.isAllHidden(key.users)) { - chatroom.lastMessage + chatroom.newestMessage } else { null } From d605eb3b0a5623b9e6eb2cbf63d30dda69982622 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 16:37:32 -0400 Subject: [PATCH 11/21] Fixes lack of update on chats and channels due to the new indexer being decoupled form LocalCache. --- .../vitorpamplona/amethyst/model/Channel.kt | 22 ++++++ .../amethyst/model/privateChats/Chatroom.kt | 21 ++++++ .../amethyst/model/privateChats/ListChange.kt | 39 ++++++++++ .../amethyst/ui/dal/AdditiveFeedFilter.kt | 19 +++-- .../amethyst/ui/dal/ChangesFlowFilter.kt | 28 ++++++++ .../amethyst/ui/dal/FeedFilter.kt | 16 +++-- .../amethyst/ui/feeds/FeedContentState.kt | 23 +++--- .../amethyst/ui/screen/FeedView.kt | 8 ++- .../loggedIn/AccountFeedContentStates.kt | 16 ----- .../loggedIn/chats/feed/ChatFeedView.kt | 38 +++++----- .../loggedIn/chats/privateDM/ChatroomView.kt | 4 +- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 20 +++--- .../privateDM/dal/ChatroomFeedViewModel.kt | 40 ++++++++++- .../publicChannels/dal/ChannelFeedFilter.kt | 13 ++-- .../dal/ChannelFeedViewModel.kt | 4 +- .../publicChannels/ephemChat/ChannelView.kt | 12 +--- .../nip28PublicChat/ChannelView.kt | 12 +--- .../nip53LiveActivities/ChannelView.kt | 12 +--- .../send/ChannelFileUploadDialog.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 6 +- .../chats/publicChannels/send/EditFieldRow.kt | 2 +- .../screen/loggedIn/drafts/DraftListScreen.kt | 2 +- .../dvms/DvmContentDiscoveryScreen.kt | 2 +- .../ammolite/relays/BundledUpdate.kt | 72 +++++++++++++------ 24 files changed, 287 insertions(+), 146 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 153f1cd73..60350a848 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -21,11 +21,15 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.privateChats.ListChange import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import java.lang.ref.WeakReference @Stable abstract class Channel : NotesGatherer { @@ -34,6 +38,16 @@ abstract class Channel : NotesGatherer { private var relays = mapOf() + private var changesFlow: WeakReference>> = WeakReference(null) + + fun changesFlow(): MutableSharedFlow> { + val current = changesFlow.get() + if (current != null) return current + val new = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) + changesFlow = WeakReference(new) + return new + } + abstract fun toBestDisplayName(): String open fun relays(): Set = @@ -80,6 +94,8 @@ abstract class Channel : NotesGatherer { addRelay(relay) } + changesFlow.get()?.tryEmit(ListChange.Addition(note)) + flowSet?.notes?.invalidateData() } } @@ -93,6 +109,8 @@ abstract class Channel : NotesGatherer { lastNote = notes.values().sortedWith(DefaultFeedOrder).firstOrNull() } + changesFlow.get()?.tryEmit(ListChange.Deletion(note)) + flowSet?.notes?.invalidateData() } } @@ -109,6 +127,8 @@ abstract class Channel : NotesGatherer { toBeRemoved.forEach { notes.remove(it.idHex) } + changesFlow.get()?.tryEmit(ListChange.SetDeletion(toBeRemoved.toSet())) + flowSet?.notes?.invalidateData() return toBeRemoved.toSet() @@ -123,6 +143,8 @@ abstract class Channel : NotesGatherer { hidden.forEach { notes.remove(it.idHex) } + changesFlow.get()?.tryEmit(ListChange.SetDeletion(hidden)) + flowSet?.notes?.invalidateData() return hidden.toSet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 31b3ff3db..a16e5752e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -29,7 +29,10 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import java.lang.ref.WeakReference @Stable class Chatroom : NotesGatherer { @@ -40,6 +43,16 @@ class Chatroom : NotesGatherer { var ownerSentMessage: Boolean = false var newestMessage: Note? = null + private var changesFlow: WeakReference>> = WeakReference(null) + + fun changesFlow(): MutableSharedFlow> { + val current = changesFlow.get() + if (current != null) return current + val new = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) + changesFlow = WeakReference(new) + return new + } + override fun removeNote(note: Note) { removeMessageSync(note) } @@ -67,6 +80,9 @@ class Chatroom : NotesGatherer { subject.tryEmit(newSubject) subjectCreatedAt = msg.createdAt() } + + changesFlow.get()?.tryEmit(ListChange.Addition(msg)) + return true } return false @@ -97,6 +113,8 @@ class Chatroom : NotesGatherer { } } + changesFlow.get()?.tryEmit(ListChange.Deletion(msg)) + return true } return false @@ -118,6 +136,9 @@ class Chatroom : NotesGatherer { val toRemove = messages.minus(toKeep) messages = toKeep + + changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) + return toRemove } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt new file mode 100644 index 000000000..d1ae61cb4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt @@ -0,0 +1,39 @@ +/** + * 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.model.privateChats + +sealed class ListChange { + data class Addition( + val item: T, + ) : ListChange() + + data class Deletion( + val item: T, + ) : ListChange() + + data class SetAddition( + val item: Set, + ) : ListChange() + + data class SetDeletion( + val item: Set, + ) : ListChange() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt index 9ade31bea..8bbf5e91b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/AdditiveFeedFilter.kt @@ -22,11 +22,9 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.logTime -abstract class AdditiveFeedFilter : FeedFilter() { - abstract fun applyFilter(newItems: Set): Set - - abstract fun sort(items: Set): List - +abstract class AdditiveFeedFilter : + FeedFilter(), + IAdditiveFeedFilter { open fun updateListWith( oldList: List, newItems: Set, @@ -42,4 +40,15 @@ abstract class AdditiveFeedFilter : FeedFilter() { oldList } } + + fun removeNotes( + oldList: List, + deletedNotes: Set, + ): List = oldList - deletedNotes +} + +interface IAdditiveFeedFilter : IFeedFilter { + fun applyFilter(newItems: Set): Set + + fun sort(items: Set): List } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt new file mode 100644 index 000000000..53f645ded --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt @@ -0,0 +1,28 @@ +/** + * 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.dal + +import com.vitorpamplona.amethyst.model.privateChats.ListChange +import kotlinx.coroutines.flow.MutableSharedFlow + +interface ChangesFlowFilter : IAdditiveFeedFilter { + fun changesFlow(): MutableSharedFlow> +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index d6a71a6c2..c26e1935c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.logTime -abstract class FeedFilter { - fun loadTop(): List { +abstract class FeedFilter : IFeedFilter { + override fun loadTop(): List { val feed = logTime( debugMessage = { "${this.javaClass.simpleName} FeedFilter returning ${it.size} objects" }, @@ -31,13 +31,17 @@ abstract class FeedFilter { ) return feed.take(limit()) } +} - open fun limit() = 500 +interface IFeedFilter { + fun loadTop(): List + + fun limit(): Int = 500 /** Returns a string that serves as the key to invalidate the list if it changes. */ - abstract fun feedKey(): Any + fun feedKey(): Any - open fun showHiddenKey(): Boolean = false + fun showHiddenKey(): Boolean = false - abstract fun feed(): List + fun feed(): List } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index b2410ce85..f84a96c83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -20,17 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.feeds -import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter -import com.vitorpamplona.amethyst.ui.dal.FeedFilter +import com.vitorpamplona.amethyst.ui.dal.IFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists -import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.ammolite.relays.BundledUpdate +import com.vitorpamplona.ammolite.relays.BasicBundledInsert +import com.vitorpamplona.ammolite.relays.BasicBundledUpdate import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -42,7 +41,7 @@ import kotlinx.coroutines.launch @Stable class FeedContentState( - val localFilter: FeedFilter, + val localFilter: IFeedFilter, val viewModelScope: CoroutineScope, ) : InvalidatableContent { private val _feedContent = MutableStateFlow(FeedState.Loading) @@ -177,8 +176,8 @@ class FeedContentState( } } - private val bundler = BundledUpdate(250, Dispatchers.IO) - private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) + private val bundler = BasicBundledUpdate(250, Dispatchers.IO, viewModelScope) + private val bundlerInsert = BasicBundledInsert>(250, Dispatchers.IO, viewModelScope) override fun invalidateData(ignoreIfDoing: Boolean) { viewModelScope.launch(Dispatchers.IO) { @@ -202,7 +201,9 @@ class FeedContentState( } fun invalidateInsertData(newItems: Set) { - bundlerInsert.invalidateList(newItems) { refreshFromOldState(it.flatten().toSet()) } + bundlerInsert.invalidateList(newItems) { + refreshFromOldState(it.flatten().toSet()) + } } fun updateFeedWith(newNotes: Set) { @@ -216,10 +217,4 @@ class FeedContentState( invalidateData() } } - - fun destroy() { - Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") - bundlerInsert.cancel() - bundler.cancel() - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index beb4807fd..8015e2fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -28,7 +28,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedState @@ -50,7 +52,7 @@ fun RefresheableFeedView( nav: INav, ) { RefresheableBox(viewModel, enablePullRefresh) { - SaveableFeedState(viewModel, scrollStateKey) { listState -> + SaveableFeedState(viewModel.feedState, scrollStateKey) { listState -> RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead) } } @@ -58,7 +60,7 @@ fun RefresheableFeedView( @Composable fun SaveableFeedState( - viewModel: FeedViewModel, + feedContentState: FeedContentState, scrollStateKey: String? = null, content: @Composable (LazyListState) -> Unit, ) { @@ -69,7 +71,7 @@ fun SaveableFeedState( rememberLazyListState() } - WatchScrollToTop(viewModel.feedState, listState) + WatchScrollToTop(feedContentState, listState) content(listState) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 9f945f7ad..2d9bbd153 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -100,22 +100,6 @@ class AccountFeedContentStates( } fun destroy() { - homeNewThreads.destroy() - homeReplies.destroy() - - dmKnown.destroy() - dmNew.destroy() - - videoFeed.destroy() - - discoverMarketplace.destroy() - discoverFollowSets.destroy() - discoverReads.destroy() - discoverDVMs.destroy() - discoverLive.destroy() - discoverCommunities.destroy() - discoverPublicChats.destroy() - notifications.destroy() notificationSummary.destroy() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index b27f5ed3d..364283185 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -32,14 +32,13 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed -import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -47,7 +46,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @Composable fun RefreshingChatroomFeedView( - viewModel: FeedViewModel, + feedContentState: FeedContentState, accountViewModel: AccountViewModel, nav: INav, routeForLastRead: String, @@ -55,27 +54,24 @@ fun RefreshingChatroomFeedView( onWantsToEditDraft: (Note) -> Unit, avoidDraft: DraftTagState? = null, scrollStateKey: String? = null, - enablePullRefresh: Boolean = true, ) { - RefresheableBox(viewModel, enablePullRefresh) { - SaveableFeedState(viewModel, scrollStateKey) { listState -> - RenderChatFeedView( - viewModel, - accountViewModel, - listState, - nav, - routeForLastRead, - onWantsToReply, - onWantsToEditDraft, - avoidDraft, - ) - } + SaveableFeedState(feedContentState, scrollStateKey) { listState -> + RenderChatFeedView( + feedContentState, + accountViewModel, + listState, + nav, + routeForLastRead, + onWantsToReply, + onWantsToEditDraft, + avoidDraft, + ) } } @Composable fun RenderChatFeedView( - viewModel: FeedViewModel, + feed: FeedContentState, accountViewModel: AccountViewModel, listState: LazyListState, nav: INav, @@ -84,13 +80,13 @@ fun RenderChatFeedView( onWantsToEditDraft: (Note) -> Unit, avoidDraft: DraftTagState? = null, ) { - val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle() + val feedState by feed.feedContent.collectAsStateWithLifecycle() CrossfadeIfEnabled(targetState = feedState, animationSpec = tween(durationMillis = 100), accountViewModel = accountViewModel) { state -> when (state) { is FeedState.Loading -> LoadingFeed() - is FeedState.Empty -> FeedEmpty { viewModel.invalidateData() } - is FeedState.FeedError -> FeedError(state.errorMessage) { viewModel.invalidateData() } + is FeedState.Empty -> FeedEmpty { feed.invalidateData() } + is FeedState.FeedError -> FeedError(state.errorMessage) { feed.invalidateData() } is FeedState.Loaded -> ChatFeedLoaded( state, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index ec87784ec..ec40203d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -132,7 +132,7 @@ fun ChatroomViewUI( .weight(1f, true), ) { RefreshingChatroomFeedView( - viewModel = feedViewModel, + feedContentState = feedViewModel.feedState, accountViewModel = accountViewModel, nav = nav, routeForLastRead = "Room/${room.hashCode()}", @@ -152,7 +152,7 @@ fun ChatroomViewUI( accountViewModel, onSendNewMessage = { scope.launch { - feedViewModel.sendToTop() + feedViewModel.feedState.sendToTop() } }, nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index 877c0de4e..5dfd03295 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -23,28 +23,26 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey class ChatroomFeedFilter( val withUser: ChatroomKey, val account: Account, -) : AdditiveFeedFilter() { +) : AdditiveFeedFilter(), + ChangesFlowFilter { + fun chatroom() = account.chatroomList.getOrCreatePrivateChatroom(withUser) + + override fun changesFlow() = chatroom().changesFlow() + // returns the last Note of each user. override fun feedKey(): String = withUser.hashCode().toString() - override fun feed(): List { - val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser) - - return chatroom.messages - .filter { account.isAcceptable(it) } - .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) - .reversed() - } + override fun feed(): List = chatroom().messages.filter { account.isAcceptable(it) }.sortedWith(DefaultFeedOrder) override fun applyFilter(newItems: Set): Set { - val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser) - + val chatroom = chatroom() return newItems.filter { it in chatroom.messages && account.isAcceptable(it) }.toSet() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index cf158e266..0fac088f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -20,16 +20,25 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal +import android.util.Log +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.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.privateChats.ListChange +import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter +import com.vitorpamplona.amethyst.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class ChatroomFeedViewModel( val user: ChatroomKey, val account: Account, -) : FeedViewModel(ChatroomFeedFilter(user, account)) { +) : ListChangeFeedViewModel(ChatroomFeedFilter(user, account)) { class Factory( val user: ChatroomKey, val account: Account, @@ -38,3 +47,30 @@ class ChatroomFeedViewModel( override fun create(modelClass: Class): T = ChatroomFeedViewModel(user, account) as T } } + +@Stable +abstract class ListChangeFeedViewModel( + localFilter: ChangesFlowFilter, +) : ViewModel(), + InvalidatableContent { + val feedState = FeedContentState(localFilter, viewModelScope) + + override val isRefreshing = feedState.isRefreshing + + override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing) + + init { + Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}") + viewModelScope.launch(Dispatchers.Default) { + localFilter.changesFlow().collect { + Log.d("Init", "Collecting changes to: ${this@ListChangeFeedViewModel.javaClass.simpleName}") + when (it) { + is ListChange.Addition -> feedState.updateFeedWith(setOf(it.item)) + is ListChange.Deletion -> feedState.deleteFromFeed(setOf(it.item)) + is ListChange.SetAddition -> feedState.updateFeedWith(it.item) + is ListChange.SetDeletion -> feedState.deleteFromFeed(it.item) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 90d0b43cf..db66af206 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -24,21 +24,20 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter +import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder class ChannelFeedFilter( val channel: Channel, val account: Account, -) : AdditiveFeedFilter() { +) : AdditiveFeedFilter(), + ChangesFlowFilter { override fun feedKey() = channel + override fun changesFlow() = channel.changesFlow() + // returns the last Note of each user. - override fun feed(): List = - sort( - channel.notes.filterIntoSet { key, it -> - account.isAcceptable(it) - }, - ) + override fun feed(): List = sort(channel.notes.filterIntoSet { key, it -> account.isAcceptable(it) }) override fun applyFilter(newItems: Set): Set = newItems diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt index be6228770..23c1c7fb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt @@ -24,12 +24,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ListChangeFeedViewModel class ChannelFeedViewModel( val channel: Channel, val account: Account, -) : FeedViewModel(ChannelFeedFilter(channel, account)) { +) : ListChangeFeedViewModel(ChannelFeedFilter(channel, account)) { class Factory( val channel: Channel, val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index c843e8cf1..daa967c5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -41,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.C import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId -import kotlinx.coroutines.launch @Composable fun EphemeralChatChannelView( @@ -111,7 +109,7 @@ private fun ChannelView( }, ) { RefreshingChatroomFeedView( - viewModel = feedViewModel, + feedContentState = feedViewModel.feedState, accountViewModel = accountViewModel, nav = nav, routeForLastRead = "Channel/${channel.roomId.toKey()}", @@ -123,17 +121,11 @@ private fun ChannelView( Spacer(modifier = DoubleVertSpacer) - val scope = rememberCoroutineScope() - // LAST ROW EditFieldRow( newPostModel, accountViewModel, - onSendNewMessage = { - scope.launch { - feedViewModel.sendToTop() - } - }, + onSendNewMessage = feedViewModel.feedState::sendToTop, nav, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 0d441a6d7..896c357a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -41,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dataso import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import kotlinx.coroutines.launch @Composable fun PublicChatChannelView( @@ -111,7 +109,7 @@ fun ChannelView( }, ) { RefreshingChatroomFeedView( - viewModel = feedViewModel, + feedContentState = feedViewModel.feedState, accountViewModel = accountViewModel, nav = nav, routeForLastRead = "Channel/${channel.idHex}", @@ -123,17 +121,11 @@ fun ChannelView( Spacer(modifier = DoubleVertSpacer) - val scope = rememberCoroutineScope() - // LAST ROW EditFieldRow( newPostModel, accountViewModel, - onSendNewMessage = { - scope.launch { - feedViewModel.sendToTop() - } - }, + onSendNewMessage = feedViewModel.feedState::sendToTop, nav, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 3cd0cd09b..d986e092c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -42,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.C import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import kotlinx.coroutines.launch @Composable fun LiveActivityChannelView( @@ -113,7 +111,7 @@ fun LiveActivityChannelView( ) { ShowVideoStreaming(channel, accountViewModel) RefreshingChatroomFeedView( - viewModel = feedViewModel, + feedContentState = feedViewModel.feedState, accountViewModel = accountViewModel, nav = nav, routeForLastRead = "Channel/${channel.address.toValue()}", @@ -125,17 +123,11 @@ fun LiveActivityChannelView( Spacer(modifier = DoubleVertSpacer) - val scope = rememberCoroutineScope() - // LAST ROW EditFieldRow( newPostModel, accountViewModel, - onSendNewMessage = { - scope.launch { - feedViewModel.sendToTop() - } - }, + onSendNewMessage = feedViewModel.feedState::sendToTop, nav, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index ac5d4d9ea..4d416e58a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp fun ChannelFileUploadDialog( channelScreenModel: ChannelNewMessageViewModel, state: ChatFileUploadState, - onUpload: () -> Unit, + onUpload: suspend () -> Unit, onCancel: () -> Unit, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index c91abed59..a7db52ac2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -259,7 +259,7 @@ open class ChannelNewMessageViewModel : urlPreview = findUrlInMessage() } - fun sendPost(onDone: () -> Unit) { + fun sendPost(onDone: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { sendPostSync() onDone() @@ -293,7 +293,7 @@ open class ChannelNewMessageViewModel : fun upload( onError: (title: String, message: String) -> Unit, context: Context, - onceUploaded: () -> Unit, + onceUploaded: suspend () -> Unit, ) = try { uploadUnsafe(onError, context, onceUploaded) } catch (_: SignerExceptions.ReadOnlyException) { @@ -306,7 +306,7 @@ open class ChannelNewMessageViewModel : fun uploadUnsafe( onError: (title: String, message: String) -> Unit, context: Context, - onceUploaded: () -> Unit, + onceUploaded: suspend () -> Unit, ) { viewModelScope.launch(Dispatchers.Default) { val uploadState = uploadState ?: return@launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index c2f20ad7e..0a2e46255 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -57,7 +57,7 @@ import kotlinx.coroutines.FlowPreview fun EditFieldRow( channelScreenModel: ChannelNewMessageViewModel, accountViewModel: AccountViewModel, - onSendNewMessage: () -> Unit, + onSendNewMessage: suspend () -> Unit, nav: INav, ) { channelScreenModel.replyTo.value?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index ad48a8edb..88bfc3763 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -120,7 +120,7 @@ private fun RenderDraftListScreen( ) { Column(Modifier.padding(it).fillMaxHeight()) { RefresheableBox(feedViewModel) { - SaveableFeedState(feedViewModel, DRAFTS) { listState -> + SaveableFeedState(feedViewModel.feedState, DRAFTS) { listState -> RenderFeedState( viewModel = feedViewModel, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index d197e8029..50d538d3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -285,7 +285,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( nav: INav, ) { Column(Modifier.fillMaxHeight()) { - SaveableFeedState(resultFeedViewModel, null) { listState -> + SaveableFeedState(resultFeedViewModel.feedState, null) { listState -> // TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation RenderFeedState( resultFeedViewModel, diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt index 2866251b2..9c0076bd7 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/BundledUpdate.kt @@ -50,6 +50,25 @@ class BundledUpdate( val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler) + val bundledUpdate = BasicBundledUpdate(delay, dispatcher, scope) + + fun invalidate( + ignoreIfDoing: Boolean = false, + onUpdate: suspend () -> Unit, + ) = bundledUpdate.invalidate(ignoreIfDoing, onUpdate) + + fun cancel() { + scope.cancel() + } +} + +/** This class is designed to have a waiting time between two calls of invalidate */ +@Stable +class BasicBundledUpdate( + val delay: Long, + val dispatcher: CoroutineDispatcher = Dispatchers.Default, + val scope: CoroutineScope, +) { private var onlyOneInBlock = AtomicBoolean() private var invalidatesAgain = false @@ -79,10 +98,6 @@ class BundledUpdate( } } } - - fun cancel() { - scope.cancel() - } } /** This class is designed to have a waiting time between two calls of invalidate */ @@ -99,6 +114,25 @@ class BundledInsert( val scope = CoroutineScope(dispatcher + SupervisorJob() + exceptionHandler) + val bundledInsert = BasicBundledInsert(delay, dispatcher, scope) + + fun invalidateList( + newObject: T, + onUpdate: suspend (Set) -> Unit, + ) = bundledInsert.invalidateList(newObject, onUpdate) + + fun cancel() { + scope.cancel() + } +} + +/** This class is designed to have a waiting time between two calls of invalidate */ +@Stable +class BasicBundledInsert( + val delay: Long, + val dispatcher: CoroutineDispatcher = Dispatchers.Default, + val scope: CoroutineScope, +) { private var onlyOneInBlock = AtomicBoolean() private var queue = LinkedBlockingQueue() @@ -109,32 +143,30 @@ class BundledInsert( checkNotInMainThread() queue.put(newObject) + if (onlyOneInBlock.getAndSet(true)) { + // if it was true already, returns. return } scope.launch(dispatcher) { try { - val mySet = mutableSetOf() - queue.drainTo(mySet) - if (mySet.isNotEmpty()) { - onUpdate(mySet) - } + while (true) { + val batch = mutableSetOf() + queue.drainTo(batch) + if (batch.isNotEmpty()) { + onUpdate(batch) + } else { + break + } - delay(delay) - - val mySet2 = mutableSetOf() - queue.drainTo(mySet2) - if (mySet2.isNotEmpty()) { - onUpdate(mySet2) + delay(delay) } } finally { - withContext(NonCancellable) { onlyOneInBlock.set(false) } + withContext(NonCancellable) { + onlyOneInBlock.set(false) + } } } } - - fun cancel() { - scope.cancel() - } } From c14d976ad16574747458b30a4ece3fed3f9e81f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 17:15:23 -0400 Subject: [PATCH 12/21] Minor refactoring --- .../quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt index 01439d24b..b61362794 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt @@ -50,7 +50,7 @@ class ReqCmd( } @JvmStatic - fun parse(msgArray: JsonNode): com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd { + fun parse(msgArray: JsonNode): ReqCmd { val filters = mutableListOf() for (i in 2 until msgArray.size()) { @@ -60,7 +60,7 @@ class ReqCmd( } } - return com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd( + return ReqCmd( msgArray.get(1).asText(), filters, ) From 478b2d12eb07751282584e68ee3395f34665a74f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 17:16:14 -0400 Subject: [PATCH 13/21] Speeding up the parsing of relay urls --- .../nip65RelayList/tags/AdvertisedRelayInfoTag.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt index bdde75e00..20eb69ac4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt @@ -76,8 +76,11 @@ class AdvertisedRelayInfo( @JvmStatic fun parseReadNorm(tag: Array): NormalizedRelayUrl? { - ensure(match(tag)) { return null } - ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null } + ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null } + + if (tag.has(2)) { + ensure(AdvertisedRelayType.isRead(tag[2])) { return null } + } val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) @@ -88,8 +91,11 @@ class AdvertisedRelayInfo( @JvmStatic fun parseWriteNorm(tag: Array): NormalizedRelayUrl? { - ensure(match(tag)) { return null } - ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null } + ensure(tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()) { return null } + + if (tag.has(2)) { + ensure(AdvertisedRelayType.isWrite(tag[2])) { return null } + } val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) From c17d66b7e996daeee7a3a63d317018d09a157f7f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 17:28:22 -0400 Subject: [PATCH 14/21] Small optimization --- .../model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index cf90954fb..c2d557041 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -131,9 +131,9 @@ class FollowListOutboxOrProxyRelays( val flowSet: StateFlow> = flow .map { relayList -> - relayList.map { it.url }.toSet() + relayList.mapTo(mutableSetOf()) { it.url } }.onStart { - emit(flow.value.map { it.url }.toSet()) + emit(flow.value.mapTo(mutableSetOf()) { it.url }) }.flowOn(Dispatchers.Default) .stateIn( scope, From 346fb615074b4de2c04020440fafad0a23ba7b0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 17:55:24 -0400 Subject: [PATCH 15/21] Small refactoring --- .../vitorpamplona/amethyst/model/Channel.kt | 2 +- .../amethyst/model/privateChats/Chatroom.kt | 1 + .../amethyst/model/privateChats/ListChange.kt | 39 ------------------- .../amethyst/ui/dal/ChangesFlowFilter.kt | 19 ++++++++- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../screen/loggedIn/profile/ProfileScreen.kt | 14 +++---- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 7 files changed, 27 insertions(+), 51 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 60350a848..0af719319 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.privateChats.ListChange import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index a16e5752e..ed669a73a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NotesGatherer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt deleted file mode 100644 index d1ae61cb4..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ListChange.kt +++ /dev/null @@ -1,39 +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.model.privateChats - -sealed class ListChange { - data class Addition( - val item: T, - ) : ListChange() - - data class Deletion( - val item: T, - ) : ListChange() - - data class SetAddition( - val item: Set, - ) : ListChange() - - data class SetDeletion( - val item: Set, - ) : ListChange() -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt index 53f645ded..792ef828d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt @@ -20,9 +20,26 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.privateChats.ListChange import kotlinx.coroutines.flow.MutableSharedFlow interface ChangesFlowFilter : IAdditiveFeedFilter { fun changesFlow(): MutableSharedFlow> } + +sealed class ListChange { + data class Addition( + val item: T, + ) : ListChange() + + data class Deletion( + val item: T, + ) : ListChange() + + data class SetAddition( + val item: Set, + ) : ListChange() + + data class SetDeletion( + val item: Set, + ) : ListChange() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 0fac088f8..5c277bf08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -27,8 +27,8 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.privateChats.ListChange import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter +import com.vitorpamplona.amethyst.ui.dal.ListChange import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey 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 f62ec2ac3..71eb5802c 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 @@ -56,7 +56,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache @@ -97,7 +96,7 @@ 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.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import kotlinx.coroutines.Dispatchers +import com.vitorpamplona.amethyst.ui.theme.Size8dp import kotlinx.coroutines.launch @Composable @@ -112,12 +111,9 @@ fun ProfileScreen( if (userBase == null) { LaunchedEffect(userId) { - // waits to resolve. - launch(Dispatchers.IO) { - val newUserBase = LocalCache.checkGetOrCreateUser(userId) - if (newUserBase != userBase) { - userBase = newUserBase - } + val newUserBase = LocalCache.checkGetOrCreateUser(userId) + if (newUserBase != userBase) { + userBase = newUserBase } } } @@ -395,7 +391,7 @@ private fun RenderScreen( containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onBackground, selectedTabIndex = pagerState.currentPage, - edgePadding = 8.dp, + edgePadding = Size8dp, modifier = tabRowModifier, divider = { HorizontalDivider(thickness = DividerThickness) }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 752cb9ce3..704938d0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -90,6 +90,7 @@ val HalfDoubleVertSpacer = Modifier.height(7.dp) val Size0dp = 0.dp val Size5dp = 5.dp val Size6dp = 6.dp +val Size8dp = 8.dp val Size10dp = 10.dp val Size12dp = 12.dp val Size13dp = 13.dp From 22dc037bf7d75422ecd8e1646fd5d90db2f1d6a1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 18:55:47 -0400 Subject: [PATCH 16/21] Moves to disable tor if on the background --- .../amethyst/service/okhttp/DualHttpClientManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt index 4ffa9f7b7..6f67bb444 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt @@ -44,7 +44,7 @@ class DualHttpClientManager( factory.buildHttpClient(proxy, mobile, userAgent) }.stateIn( scope, - SharingStarted.Lazily, + SharingStarted.WhileSubscribed(1000), factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent), ) @@ -54,7 +54,7 @@ class DualHttpClientManager( factory.buildHttpClient(mobile, userAgent) }.stateIn( scope, - SharingStarted.Lazily, + SharingStarted.WhileSubscribed(1000), factory.buildHttpClient(isMobileDataProvider.value, userAgent), ) From 41c2097f3c935c4e06951a38378399577602d3c1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 18:55:55 -0400 Subject: [PATCH 17/21] reduces logs --- amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index d115ad178..cf4c3bf3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -46,7 +46,6 @@ import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator -import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -120,7 +119,7 @@ class Amethyst : Application() { // Authenticates with relays. val authCoordinator = AuthCoordinator(client, applicationIOScope) - val logger = if (isDebug) RelaySpeedLogger(client) else null + // val logger = if (isDebug) RelaySpeedLogger(client) else null // Coordinates all subscriptions for the Nostr Client val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope) From a034b495e8e8b3c555448b6a78453a30c00b09e4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 18:57:01 -0400 Subject: [PATCH 18/21] Removes checks --- .../main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt index 7d760d3ff..5cd75cd5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt @@ -27,7 +27,6 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import android.util.Log -import com.vitorpamplona.amethyst.service.checkNotInMainThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay @@ -55,7 +54,7 @@ class TorService( name: ComponentName, service: IBinder, ) { - launch { + launch(Dispatchers.IO) { // moved torService to a local variable, since we only need it once val torService = (service as LocalBinder).service @@ -77,7 +76,6 @@ class TorService( ) awaitClose { - checkNotInMainThread() Log.d("TorService", "Stopping Tor Service") launch { context.stopService(currentIntent) From 251e5535d0f312b5559370e1077af575b8c554cd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 12 Aug 2025 18:57:11 -0400 Subject: [PATCH 19/21] Removes hash logs --- .../amethyst/ui/components/ZoomableContentView.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 1b79f32b3..888e81e29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.components import android.content.Context import android.content.Intent -import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.fadeIn @@ -809,9 +808,6 @@ private fun verifyHash(content: MediaUrlContent): Boolean? { Amethyst.instance.diskCache.openSnapshot(content.url)?.use { snapshot -> val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey() - - Log.d("Image Hash Verification", "$hash == ${content.hash}") - return hash == content.hash } From 4c0f223673a60b3b791fb5413c17bbc1f9ab6090 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Aug 2025 19:11:29 -0400 Subject: [PATCH 20/21] Draft event refactoring --- .../vitorpamplona/amethyst/model/Account.kt | 267 +++++++----------- .../amethyst/model/AccountSettings.kt | 4 +- .../amethyst/model/LocalCache.kt | 6 +- .../com/vitorpamplona/amethyst/model/Note.kt | 5 +- .../AccountOutboxRelayState.kt | 4 +- .../metadata/FilterDraftsAndReportsFromKey.kt | 4 +- .../ui/navigation/routes/RouteMaker.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 10 +- .../ui/note/elements/DisplayReward.kt | 1 - .../nip22Comments/CommentPostViewModel.kt | 15 +- .../ui/screen/loggedIn/AccountViewModel.kt | 8 +- .../loggedIn/DecryptAndIndexProcessor.kt | 12 +- .../loggedIn/chats/feed/ChatFeedView.kt | 4 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 8 +- .../header/NewChatroomSubjectDialog.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 38 ++- .../privateDM/send/upload/ChatFileSender.kt | 27 +- .../send/ChannelNewMessageViewModel.kt | 12 +- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +- .../nip99Classifieds/NewProductViewModel.kt | 4 +- .../drafts/dal/DraftEventsFeedFilter.kt | 4 +- .../loggedIn/home/ShortNotePostViewModel.kt | 17 +- .../NewPublicMessageViewModel.kt | 17 +- .../loggedIn/threadview/ThreadFeedView.kt | 4 +- .../com/vitorpamplona/quartz/EventFactory.kt | 4 +- .../quartz/nip04Dm/messages/PrivateDmEvent.kt | 32 ++- .../quartz/nip37Drafts/DraftBuilder.kt | 6 +- .../quartz/nip37Drafts/DraftEvent.kt | 230 --------------- .../quartz/nip37Drafts/DraftEventCache.kt | 16 +- .../quartz/nip37Drafts/DraftWrapEvent.kt | 125 ++++++++ 30 files changed, 376 insertions(+), 518 deletions(-) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftWrapEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ec529464a..025062c3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState @@ -130,16 +131,13 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip04Dm.messages.reply import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags @@ -160,12 +158,10 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag -import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response @@ -217,6 +213,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Locale +import kotlin.collections.forEach @OptIn(DelicateCoroutinesApi::class) @Stable @@ -297,6 +294,7 @@ class Account( val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) // Relay settings + val homeRelays = AccountHomeRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope) val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) @@ -1141,103 +1139,42 @@ class Account( return event } - suspend fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: Set, - broadcastNotes: List, - ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) - - suspend fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: Set, - broadcastNotes: Set, - ) { - if (draftTag != null) { - val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) - val draftEvent = - DraftEvent.create( - dTag = draftTag, - innerEvent = rumor, - anchorTagArray = emptyList(), - signer = signer, - ) - draftsDecryptionCache.preload(draftEvent, rumor) - sendDraftEvent(draftEvent) - } else { - val it = signer.sign(template) - cache.justConsumeMyOwnEvent(it) - client.send(it, relayList = relayList) - - broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } } - } - } - - suspend fun signAndSendNIP04Message( - draftTag: String?, - template: EventTemplate, - relayList: Set, - ) { - if (draftTag != null) { - if (template.content.isEmpty()) { - deleteDraft(draftTag) - } else { - val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) - val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) - draftsDecryptionCache.preload(draftEvent, rumor) - sendDraftEvent(draftEvent) - } - } else { - val newEvent = signer.sign(template) - cache.justConsumeMyOwnEvent(newEvent) - client.send(newEvent, relayList) - } - } - - suspend fun signAndSendWithList( - draftTag: String?, - template: EventTemplate, - relayList: Collection, - broadcastNotes: Set, - ) { - if (draftTag != null) { - val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) - val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) - draftsDecryptionCache.preload(draftEvent, rumor) - sendDraftEvent(draftEvent) - } else { - val event = signer.sign(template) - cache.justConsumeMyOwnEvent(event) - - val relaySet = relayList.toSet() - - client.send(event, relayList = relaySet) - broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relaySet) } } - } - } - suspend fun createAndSendDraft( draftTag: String, template: EventTemplate, + broadcast: Set = emptySet(), ) { + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer) draftsDecryptionCache.preload(draftEvent, rumor) - sendDraftEvent(draftEvent) + + cache.justConsumeMyOwnEvent(draftEvent) + + val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet() + if (relayList.isNotEmpty()) { + client.send(draftEvent, relayList) + broadcast.forEach { + client.send(it, relayList.toSet()) + } + } } suspend fun deleteDraft(draftTag: String) { - val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag) - cache.getAddressableNoteIfExists(key)?.let { note -> - val noteEvent = note.event - if (noteEvent is DraftEvent) { - val deletedDraftEvent = noteEvent.createDeletedEvent(signer) - client.send(deletedDraftEvent, outboxRelays.flow.value + note.relays) - cache.justConsumeMyOwnEvent(deletedDraftEvent) + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() - delete(deletedDraftEvent, note.relays.toSet()) - } + val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) + val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft))) + + val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet() + + cache.justConsumeMyOwnEvent(deletedDraft) + cache.justConsumeMyOwnEvent(deletionEvent) + + if (relayList.isNotEmpty()) { + client.send(deletedDraft, relayList) + client.send(deletionEvent, relayList) } } @@ -1257,7 +1194,18 @@ class Account( currentSceneRelay = readingSceneRelay, ) - sendToPrivateOutboxAndLocal(signer.sign(template)) + val event = signer.sign(template) + + // updates relays that already have this replaceable. + val noteRelays = cache.getAddressableNoteIfExists(event.address())?.relays ?: emptyList() + + val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value + if (relayList.isNotEmpty()) { + client.send(event, relayList + noteRelays) + } else { + client.send(event, outboxRelays.flow.value + noteRelays) + } + cache.justConsumeMyOwnEvent(event) } suspend fun updateInteractiveStoryReadingState( @@ -1274,7 +1222,18 @@ class Account( currentSceneRelay = readingSceneRelay, ) - sendToPrivateOutboxAndLocal(signer.sign(template)) + val event = signer.sign(template) + + // updates relays that already have this replaceable. + val noteRelays = cache.getAddressableNoteIfExists(event.address())?.relays ?: emptyList() + + val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value + if (relayList.isNotEmpty()) { + client.send(event, relayList + noteRelays) + } else { + client.send(event, outboxRelays.flow.value + noteRelays) + } + cache.justConsumeMyOwnEvent(event) } fun mapEntitiesToNotes(entities: List): List = @@ -1328,7 +1287,15 @@ class Account( contentWarningReason?.let { contentWarning(contentWarningReason) } } - signAndSend(draftTag, template, relayList, quotes) + if (draftTag != null) { + createAndSendDraft(draftTag, template) + } else { + val it = signer.sign(template) + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList = relayList) + + mapEntitiesToNotes(quotes).forEach { it.event?.let { client.send(it, relayList = relayList) } } + } } suspend fun sendInteractiveStoryScene( @@ -1363,29 +1330,42 @@ class Account( contentWarningReason?.let { contentWarning(contentWarningReason) } } - signAndSend(draftTag, template, relayList, mapEntitiesToNotes(quotes).toSet()) + val broadcastNotes = mapEntitiesToNotes(quotes).toSet() + + if (draftTag != null) { + createAndSendDraft(draftTag, template) + } else { + val it = signer.sign(template) + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList = relayList) + + broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } } + } } suspend fun sendAddBounty( value: BigDecimal, bounty: Note, - draftTag: String?, ) { if (!isWriteable()) return - val event = bounty.event as? TextNoteEvent ?: return - val eventAuthor = bounty.author ?: return + val bountyEvent = bounty.event as? TextNoteEvent ?: return + val bountyAuthor = bounty.author ?: return val template = BountyAddValueEvent.build( - value, - EventHintBundle(event, bounty.relayHintUrl()), - eventAuthor.toPTag(), + amount = value, + bountyRoot = EventHintBundle(bountyEvent, bounty.relayHintUrl()), + bountyRootAuthor = bountyAuthor.toPTag(), ) - val relays = bounty.relays + outboxRelays.flow.value + val relays = (bounty.relays + outboxRelays.flow.value).toSet() - signAndSendWithList(draftTag, template, relays, setOf(bounty)) + val newEvent = signer.sign(template) + cache.justConsumeMyOwnEvent(newEvent) + + client.send(newEvent, relayList = relays) + client.send(bountyEvent, relayList = relays) } suspend fun sendEdit( @@ -1417,82 +1397,27 @@ class Account( broadcast.forEach { client.send(it, relayList) } } - suspend fun sendPrivateMessage( - message: String, - toUser: PTag, - replyingTo: Note? = null, - zapReceiver: List? = null, - contentWarningReason: String? = null, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String?, - ) { + suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate) { if (!isWriteable()) return - val encryptedContent = - signer.nip04Encrypt( - PrivateDmEvent.prepareMessageToEncrypt(message, imetas), - toUser.pubKey, - ) + val newEvent = signer.sign(eventTemplate) + val recipient = newEvent.verifiedRecipientPubKey() + val destinationRelays = recipient?.let { cache.getOrCreateUser(it).dmInboxRelays() } ?: emptyList() - val template = - PrivateDmEvent.build(toUser, encryptedContent) { - replyingTo?.let { reply(it.toEId()) } - - geohash?.let { geohash(it) } - zapRaiserAmount?.let { zapraiser(it) } - zapReceiver?.let { zapSplits(it) } - emojis?.let { emojis(it) } - contentWarningReason?.let { contentWarning(contentWarningReason) } - } - - val destinationRelays = cache.getOrCreateUser(toUser.pubKey).dmInboxRelays() - - signAndSendNIP04Message(draftTag, template, outboxRelays.flow.value + destinationRelays) + cache.justConsumeMyOwnEvent(newEvent) + client.send(newEvent, outboxRelays.flow.value + destinationRelays) } - suspend fun sendNIP17EncryptedFile(template: EventTemplate) { + suspend fun sendNip17EncryptedFile(template: EventTemplate) { if (!isWriteable()) return val wraps = NIP17Factory().createEncryptedFileNIP17(template, signer) broadcastPrivately(wraps) } - suspend fun sendNIP17PrivateMessage( - template: EventTemplate, - draftTag: String? = null, - ) { - if (draftTag != null) { - if (template.content.isEmpty()) { - deleteDraft(draftTag) - } else { - val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) - val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) - draftsDecryptionCache.preload(draftEvent, rumor) - sendDraftEvent(draftEvent) - } - } else { - val it = NIP17Factory().createMessageNIP17(template, signer) - broadcastPrivately(it) - } - } - - fun sendDraftEvent(draftEvent: DraftEvent) { - sendToPrivateOutboxAndLocal(draftEvent) - } - - fun sendToPrivateOutboxAndLocal(event: Event?) { - if (event == null) return - - val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value - if (relayList.isNotEmpty()) { - client.send(event, relayList.toSet()) - } else { - client.send(event, outboxRelays.flow.value) - } - cache.justConsumeMyOwnEvent(event) + suspend fun sendNip17PrivateMessage(template: EventTemplate) { + val events = NIP17Factory().createMessageNIP17(template, signer) + broadcastPrivately(events) } suspend fun broadcastPrivately(signedEvents: NIP17Factory.Result) { @@ -1645,7 +1570,7 @@ class Account( privateDMDecryptionCache.cachedDM(event) } else if (event is LnZapRequestEvent && event.isPrivateZap()) { privateZapsDecryptionCache.cachedPrivateZap(event)?.content - } else if (event is DraftEvent) { + } else if (event is DraftWrapEvent) { draftsDecryptionCache.preCachedDraft(event)?.content } else { event.content @@ -1669,7 +1594,7 @@ class Account( } else { event.content } - } else if (event is DraftEvent && isWriteable()) { + } else if (event is DraftWrapEvent && isWriteable()) { draftsDecryptionCache.cachedDraft(event)?.content } else { event?.content diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 120281127..47a626522 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect @@ -89,7 +89,7 @@ val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Co val DefaultSignerPermissions = listOf( Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), - Permission(CommandType.SIGN_EVENT, DraftEvent.KIND), + Permission(CommandType.SIGN_EVENT, DraftWrapEvent.KIND), Permission(CommandType.NIP04_ENCRYPT), Permission(CommandType.NIP04_DECRYPT), Permission(CommandType.NIP44_DECRYPT), 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 b46ed20e2..52ee06409 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -120,7 +120,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip40Expiration.expiration @@ -2536,7 +2536,7 @@ object LocalCache : ILocalCache { } fun consume( - event: DraftEvent, + event: DraftWrapEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified) @@ -2744,7 +2744,7 @@ object LocalCache : ILocalCache { is CommunityPostApprovalEvent -> consume(event, relay, wasVerified) is ContactListEvent -> consume(event, relay, wasVerified) is DeletionEvent -> consume(event, relay, wasVerified) - is DraftEvent -> consume(event, relay, wasVerified) + is DraftWrapEvent -> consume(event, relay, wasVerified) is EmojiPackEvent -> consume(event, relay, wasVerified) is EmojiPackSelectionEvent -> consume(event, relay, wasVerified) is EphemeralChatEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 19c2d9b04..e23666235 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import coil3.util.CoilUtils.result import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -53,7 +52,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod @@ -236,7 +235,7 @@ open class Note( open fun createdAt() = event?.createdAt - fun isDraft() = event is DraftEvent + fun isDraft() = event is DraftWrapEvent fun loadEvent( event: Event, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 58de33ab4..f3ccbdacf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -44,8 +44,8 @@ class AccountOutboxRelayState( privateStorage.flow, local.flow, broadcast.flow, - ) { nip65Inbox, privateOutBox, localRelays, broadcastRelays -> - nip65Inbox + privateOutBox + localRelays + broadcastRelays + ) { nip65Outbox, privateOutBox, localRelays, broadcastRelays -> + nip65Outbox + privateOutBox + localRelays + broadcastRelays }.flowOn(Dispatchers.Default) .stateIn( scope, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt index c0473e031..ec0908e23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt @@ -24,13 +24,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent val ReportsAndBookmarksFromKeyKinds = listOf( - DraftEvent.KIND, + DraftWrapEvent.KIND, ReportEvent.KIND, BookmarkListEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 292541708..d6df69fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent @@ -63,7 +63,7 @@ fun routeFor( noteEvent: Event, loggedIn: Account, ): Route? { - if (noteEvent is DraftEvent) { + if (noteEvent is DraftWrapEvent) { val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent) if (innerEvent is IsInPublicChatChannel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 78a86633a..776d04c7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -187,7 +187,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent @@ -552,7 +552,7 @@ fun InnerNoteWithReactions( } } - val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftEvent + val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftWrapEvent if (isNotRepost) { if (makeItShort) { @@ -568,7 +568,7 @@ fun InnerNoteWithReactions( ) } } else { - if (baseNote.event is DraftEvent) { + if (baseNote.event is DraftWrapEvent) { Spacer(modifier = DoubleVertSpacer) } } @@ -647,7 +647,7 @@ private fun RenderNoteRow( is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav) is AudioTrackEvent -> RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) is AudioHeaderEvent -> RenderAudioHeader(baseNote, ContentScale.FillWidth, accountViewModel, nav) - is DraftEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav) + is DraftWrapEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav) is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) @@ -901,7 +901,7 @@ fun ObserveDraftEvent( accountViewModel: AccountViewModel, render: @Composable (Note) -> Unit, ) { - val noteEvent by observeNoteEvent(note, accountViewModel) + val noteEvent by observeNoteEvent(note, accountViewModel) noteEvent?.let { val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index e1e060940..102cc6f4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -179,7 +179,6 @@ class AddBountyAmountViewModel : ViewModel() { myAccount.sendAddBounty( newValue, bountyInner, - draftTag = null, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 4a5f30a01..7bfefdeca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -202,7 +202,7 @@ open class CommentPostViewModel : val noteEvent = draft.event val noteAuthor = draft.author - if (noteEvent is DraftEvent && noteAuthor != null) { + if (noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -324,13 +324,14 @@ open class CommentPostViewModel : if (message.text.isBlank()) { accountViewModel.account.deleteDraft(draftTag.current) } else { - val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) - + val attachments = mutableSetOf() nip95attachments.forEach { - account.sendToPrivateOutboxAndLocal(it.first) - account.sendToPrivateOutboxAndLocal(it.second) + attachments.add(it.first) + attachments.add(it.second) } + + val template = createTemplate() ?: return + accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index a40262366..f1d17eff5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -115,7 +115,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent @@ -1510,7 +1510,7 @@ class AccountViewModel( account.deleteDraft(draftTag) } - suspend fun createTempDraftNote(noteEvent: DraftEvent): Note? = draftNoteCache.update(noteEvent) + suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent) fun createTempDraftNote( innerEvent: Event, @@ -1691,8 +1691,8 @@ class AccountViewModel( class CachedDraftNotes( val accountViewModel: AccountViewModel, - ) : GenericBaseCacheAsync(20) { - override suspend fun compute(key: DraftEvent): Note? = + ) : GenericBaseCacheAsync(20) { + override suspend fun compute(key: DraftWrapEvent): Note? = withContext(Dispatchers.IO) { val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key) if (decrypted != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index dd062d78d..e5c919f14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.IEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -74,7 +74,7 @@ class EventProcessor( when (event) { is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote) is OtsEvent -> otsHandler.add(event, eventNote, publicNote) - is DraftEvent -> draftHandler.add(event, eventNote, publicNote) + is DraftWrapEvent -> draftHandler.add(event, eventNote, publicNote) is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote) is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) @@ -98,7 +98,7 @@ class EventProcessor( when (event) { is ChatroomKeyable -> chatHandler.delete(event, note) is OtsEvent -> otsHandler.delete(event, note) - is DraftEvent -> draftHandler.delete(event, note) + is DraftWrapEvent -> draftHandler.delete(event, note) is GiftWrapEvent -> giftWrapHandler.delete(event, note) is SealedRumorEvent -> sealHandler.delete(event, note) is LnZapRequestEvent -> zapRequest.delete(event, note) @@ -129,7 +129,7 @@ class EventProcessor( val deletedDrafts = newNotes.mapNotNull { note -> val event = note.event - if (event is DraftEvent && + if (event is DraftWrapEvent && event.isDeleted() && !cache.deletionIndex.hasBeenDeleted(event) ) { @@ -193,9 +193,9 @@ class OtsEventHandler( class DraftEventHandler( private val account: Account, private val cache: LocalCache, -) : EventHandler { +) : EventHandler { override suspend fun add( - event: DraftEvent, + event: DraftWrapEvent, eventNote: Note, publicNote: Note, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 364283185..d411332a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @Composable fun RefreshingChatroomFeedView( @@ -129,7 +129,7 @@ fun ChatFeedLoaded( ) { itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item -> val noteEvent = item.event - if (avoidDraft == null || noteEvent !is DraftEvent || noteEvent.dTag() !in avoidDraft.usedDraftTags) { + if (avoidDraft == null || noteEvent !is DraftWrapEvent || noteEvent.dTag() !in avoidDraft.usedDraftTags) { ChatroomMessageCompose( baseNote = item, routeForLastRead = routeForLastRead, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index b92c80cf4..8a510adab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -162,7 +162,7 @@ fun NormalChatNote( ChatBubbleLayout( isLoggedInUser = isLoggedInUser, - isDraft = note.event is DraftEvent, + isDraft = note.event is DraftWrapEvent, innerQuote = innerQuote, isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE, hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(), @@ -289,7 +289,7 @@ private fun MessageBubbleLines( accountViewModel: AccountViewModel, nav: INav, ) { - if (baseNote.event !is DraftEvent) { + if (baseNote.event !is DraftWrapEvent) { RenderReplyRow( note = baseNote, innerQuote = innerQuote, @@ -388,7 +388,7 @@ fun NoteRow( when (note.event) { is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav) is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note, bgColor, accountViewModel, nav) - is DraftEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav) + is DraftWrapEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav) is ChatMessageEncryptedFileHeaderEvent -> RenderEncryptedFile(note, bgColor, accountViewModel, nav) else -> RenderRegularTextNote(note, canPreview, innerQuote, bgColor, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt index 15f12dc00..e8fd51ae0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt @@ -107,7 +107,7 @@ fun NewChatroomSubjectDialog( groupName.value.ifBlank { null }?.let { changeSubject(it) } } - accountViewModel.account.sendNIP17PrivateMessage(template) + accountViewModel.account.sendNip17PrivateMessage(template) } onClose() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 91e17d0f4..ec6dd1737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -79,7 +79,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits @@ -252,7 +252,7 @@ class ChatNewMessageViewModel : val noteEvent = draft.event val noteAuthor = draft.author - if (noteEvent is DraftEvent && noteAuthor != null) { + if (noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -421,7 +421,7 @@ class ChatNewMessageViewModel : } } - private suspend fun innerSendPost(dTag: String?) { + private suspend fun innerSendPost(draftTag: String?) { val room = room ?: return val urls = findURLs(message.text) @@ -468,19 +468,31 @@ class ChatNewMessageViewModel : } } - accountViewModel.account.sendNIP17PrivateMessage(template, dTag) + if (draftTag != null) { + accountViewModel.account.createAndSendDraft(draftTag, template) + } else { + accountViewModel.account.sendNip17PrivateMessage(template) + } } else { - accountViewModel.account.sendPrivateMessage( - message = message, - toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, - replyingTo = replyTo.value, - contentWarningReason = null, - imetas = usedAttachments, - draftTag = dTag, - ) + val toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() } + + val template = + PrivateDmEvent.build( + toUser = toUser, + message = message, + imetas = usedAttachments, + replyingTo = replyTo.value?.toEId(), + signer = accountViewModel.account.signer, + ) + + if (draftTag != null) { + accountViewModel.account.createAndSendDraft(draftTag, template) + } else { + accountViewModel.account.sendNip04PrivateMessage(template) + } } - if (dTag == null) { + if (draftTag == null) { ChatFileSender(room, accountViewModel.account).sendAll(uploadsWaitingToBeSent) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index 344fc1bf5..5fdd6d647 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM @@ -48,7 +49,7 @@ class ChatFileSender( contentWarningReason: String?, cipher: AESGCM, ) { - account.sendNIP17EncryptedFile( + account.sendNip17EncryptedFile( ChatMessageEncryptedFileHeaderEvent.build( url = result.url, to = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }, @@ -89,18 +90,18 @@ class ChatFileSender( val iMetaAttachments = IMetaAttachments() iMetaAttachments.add(result, caption, contentWarningReason) - account.sendPrivateMessage( - message = result.url, - toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, - replyingTo = null, - zapReceiver = null, - contentWarningReason = null, - zapRaiserAmount = null, - geohash = null, - imetas = iMetaAttachments.iMetaAttachments, - emojis = null, - draftTag = null, - ) + val toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() } + + val template = + PrivateDmEvent.build( + toUser = toUser, + message = result.url, + imetas = iMetaAttachments.iMetaAttachments, + replyingTo = null, + signer = account.signer, + ) + + account.sendNip04PrivateMessage(template) } suspend fun sendAll(uploads: List) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index a7db52ac2..f63406ba2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup @@ -191,7 +191,7 @@ open class ChannelNewMessageViewModel : val noteEvent = draft.event val noteAuthor = draft.author - if (noteEvent is DraftEvent && noteAuthor != null) { + if (noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -281,8 +281,14 @@ open class ChannelNewMessageViewModel : if (message.text.isBlank()) { account.deleteDraft(draftTag.current) } else { + val attachments = mutableSetOf() + nip95attachments.forEach { + attachments.add(it.first) + attachments.add(it.second) + } + val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) + accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 8f2e7fe07..002642d94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -86,7 +86,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @Composable fun ChatroomHeaderCompose( @@ -113,7 +113,7 @@ fun ChatroomComposeChannelOrUser( nav: INav, ) { val baseNoteEvent = baseNote.event - if (baseNoteEvent is DraftEvent) { + if (baseNoteEvent is DraftWrapEvent) { ObserveDraftEvent(baseNote, accountViewModel) { innerNote -> ChatroomEntry(innerNote, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 64b834bd7..8cb8ed970 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -78,7 +78,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -192,7 +192,7 @@ open class NewProductViewModel : val noteEvent = draft.event val noteAuthor = draft.author - if (noteEvent is DraftEvent && noteAuthor != null) { + if (noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt index 82eea5b9a..d9026aa88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedFilter.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent class DraftEventsFeedFilter( val account: Account, @@ -48,7 +48,7 @@ class DraftEventsFeedFilter( fun acceptableEvent(it: Note): Boolean { val noteEvent = it.event - return noteEvent is DraftEvent && noteEvent.pubKey == account.userProfile().pubkeyHex + return noteEvent is DraftWrapEvent && noteEvent.pubKey == account.userProfile().pubkeyHex } override fun sort(items: Set): List = items.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 19966349e..49f8b1dc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -96,7 +96,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup @@ -241,7 +241,7 @@ open class ShortNotePostViewModel : val noteEvent = draft?.event val noteAuthor = draft?.author - if (draft != null && noteEvent is DraftEvent && noteAuthor != null) { + if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -483,18 +483,17 @@ open class ShortNotePostViewModel : } suspend fun sendDraftSync() { - val accountViewModel = accountViewModel - if (message.text.isBlank()) { accountViewModel.account.deleteDraft(draftTag.current) } else { - val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) - + val attachments = mutableSetOf() nip95attachments.forEach { - account.sendToPrivateOutboxAndLocal(it.first) - account.sendToPrivateOutboxAndLocal(it.second) + attachments.add(it.first) + attachments.add(it.second) } + + val template = createTemplate() ?: return + accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 9b0a7c945..f3b8ff4fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -85,7 +85,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits @@ -235,7 +235,7 @@ class NewPublicMessageViewModel : val noteEvent = draft.event val noteAuthor = draft.author - if (noteEvent is DraftEvent && noteAuthor != null) { + if (noteEvent is DraftWrapEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -323,18 +323,17 @@ class NewPublicMessageViewModel : } suspend fun sendDraftSync() { - val accountViewModel = accountViewModel - if (message.text.isBlank()) { accountViewModel.account.deleteDraft(draftTag.current) } else { - val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) - + val broadcast = mutableSetOf() nip95attachments.forEach { - account.sendToPrivateOutboxAndLocal(it.first) - account.sendToPrivateOutboxAndLocal(it.second) + broadcast.add(it.first) + broadcast.add(it.second) } + + val template = createTemplate() ?: return + accountViewModel.account.createAndSendDraft(draftTag.current, template, broadcast) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 318672bd9..d03940fff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -208,7 +208,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent @@ -641,7 +641,7 @@ private fun FullBleedNoteCompose( RenderGitIssueEvent(baseNote, makeItShort = false, canPreview = true, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is AppDefinitionEvent) { RenderAppDefinition(baseNote, accountViewModel, nav) - } else if (noteEvent is DraftEvent) { + } else if (noteEvent is DraftWrapEvent) { RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 4477dfe7d..68c7a8588 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent @@ -216,7 +216,7 @@ class EventFactory { CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) - DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig) + DraftWrapEvent.KIND -> DraftWrapEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt index 7b7b99979..94ec1a6c6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag @@ -121,16 +122,37 @@ class PrivateDmEvent( } } - fun build( - to: PTag, - encryptedMessage: String, + suspend fun build( + toUser: PTag, + message: String, + imetas: List? = null, + replyingTo: EventReference? = null, createdAt: Long = TimeUtils.now(), + signer: NostrSigner, initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, encryptedMessage, createdAt) { + ) = eventTemplate( + kind = KIND, + description = + signer.nip04Encrypt( + prepareMessageToEncrypt(message, imetas), + toUser.pubKey, + ), + createdAt = createdAt, + ) { alt(ALT) - pTag(to) + pTag(toUser) + replyingTo?.let { reply(it) } initializer() } + + suspend fun create( + to: PTag, + message: String, + imetas: List? = null, + replyingTo: EventReference?, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + ) = signer.sign(build(to, message, imetas, replyingTo, createdAt, signer)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt index 9a6befef4..3435d2227 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt @@ -35,11 +35,11 @@ class DraftBuilder { draft: T, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): DraftEvent { + ): DraftWrapEvent { val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey) val template = - eventTemplate(DraftEvent.KIND, encryptedContent, createdAt) { - alt(DraftEvent.ALT_DESCRIPTION) + eventTemplate(DraftWrapEvent.KIND, encryptedContent, createdAt) { + alt(DraftWrapEvent.ALT_DESCRIPTION) dTag(dTag) kind(draft.kind) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt deleted file mode 100644 index b982ca9fd..000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ /dev/null @@ -1,230 +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.quartz.nip37Drafts - -import android.util.Log -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.core.JsonParseException -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent -import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class DraftEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), - EventHintProvider, - AddressHintProvider, - PubKeyHintProvider { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - - override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) - - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) - - override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) - - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - - override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - - override fun isContentEncoded() = true - - fun isDeleted() = content == "" - - fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey - - suspend fun createDeletedEvent(signer: NostrSigner): DraftEvent = signer.sign(createdAt, KIND, tags, "") - - suspend fun decryptInnerEvent(signer: NostrSigner): Event { - if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() - - val json = signer.nip44Decrypt(content, pubKey) - return try { - fromJson(json) - } catch (e: JsonParseException) { - Log.w("DraftEvent", "Unable to parse inner event of a draft: $json") - throw e - } - } - - companion object { - const val KIND = 31234 - const val ALT_DESCRIPTION = "Draft Event" - - fun createAddressTag( - pubKey: HexKey, - dTag: String, - ): String = Address.assemble(KIND, pubKey, dTag) - - @Suppress("DEPRECATION") - suspend fun create( - dTag: String, - originalNote: TorrentCommentEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tagsWithMarkers = - originalNote.tags.filter { - it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") - } - - return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: InteractiveStoryBaseEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tags = mutableListOf>() - return create(dTag, originalNote, tags, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: LiveActivitiesChatMessageEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tags = mutableListOf>() - originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) } - originalNote.replyingTo()?.let { tags.add(arrayOf("e", it, "", "reply")) } - - return create(dTag, originalNote, tags, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: ChannelMessageEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tags = mutableListOf>() - originalNote.channelId()?.let { tags.add(arrayOf("e", it)) } - - return create(dTag, originalNote, tags, signer, createdAt) - } - - @Suppress("DEPRECATION") - suspend fun create( - dTag: String, - originalNote: GitReplyEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tags = mutableListOf>() - originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) } - originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) } - - return create(dTag, originalNote, tags, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: PollNoteEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tagsWithMarkers = - originalNote.tags.filter { - it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") - } - - return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: CommentEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies() - - return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) - } - - suspend fun create( - dTag: String, - originalNote: TextNoteEvent, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tagsWithMarkers = - originalNote.tags.filter { - it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") - } - - return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) - } - - suspend fun create( - dTag: String, - innerEvent: Event, - anchorTagArray: List> = emptyList(), - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - ): DraftEvent { - val tags = mutableListOf>() - tags.add(arrayOf("d", dTag)) - tags.add(arrayOf("k", "${innerEvent.kind}")) - - if (anchorTagArray.isNotEmpty()) { - tags.addAll(anchorTagArray) - } - - val draft = - signer.sign( - createdAt = createdAt, - kind = KIND, - tags = tags.toTypedArray(), - content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey), - ) - - return draft - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt index ef14999b3..f7e57f574 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt @@ -29,8 +29,8 @@ class DraftEventCache( signer: NostrSigner, ) { private val decryptionCache = - object : LruCache(1000) { - override fun create(key: DraftEvent): DraftEventDecryptCache? = + object : LruCache(1000) { + override fun create(key: DraftWrapEvent): DraftEventDecryptCache? = if (!key.isDeleted() && key.pubKey == signer.pubKey) { DraftEventDecryptCache(signer) } else { @@ -38,23 +38,23 @@ class DraftEventCache( } } - fun delete(event: DraftEvent) = decryptionCache.remove(event) + fun delete(event: DraftWrapEvent) = decryptionCache.remove(event) fun preload( - event: DraftEvent, + event: DraftWrapEvent, result: Event, ) = decryptionCache[event]?.preload(result) - fun preCachedDraft(event: DraftEvent): Event? = decryptionCache[event]?.cached() + fun preCachedDraft(event: DraftWrapEvent): Event? = decryptionCache[event]?.cached() - suspend fun cachedDraft(event: DraftEvent) = decryptionCache[event]?.decrypt(event) + suspend fun cachedDraft(event: DraftWrapEvent) = decryptionCache[event]?.decrypt(event) } class DraftEventDecryptCache( signer: NostrSigner, -) : DecryptCache(signer) { +) : DecryptCache(signer) { override suspend fun decryptAndParse( - event: DraftEvent, + event: DraftWrapEvent, signer: NostrSigner, ): Event = event.decryptInnerEvent(signer) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftWrapEvent.kt new file mode 100644 index 000000000..bae8d9b69 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftWrapEvent.kt @@ -0,0 +1,125 @@ +/** + * 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.quartz.nip37Drafts + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.core.JsonParseException +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class DraftWrapEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + override fun isContentEncoded() = true + + fun isDeleted() = content == "" + + fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey + + suspend fun decryptInnerEvent(signer: NostrSigner): Event { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() + + val json = signer.nip44Decrypt(content, pubKey) + return try { + fromJson(json) + } catch (e: JsonParseException) { + Log.w("DraftEvent", "Unable to parse inner event of a draft: $json") + throw e + } + } + + companion object { + const val KIND = 31234 + const val ALT_DESCRIPTION = "Draft Event" + + fun createAddress( + pubKey: HexKey, + dTag: String, + ): Address = Address(KIND, pubKey, dTag) + + fun createAddressTag( + pubKey: HexKey, + dTag: String, + ): String = Address.assemble(KIND, pubKey, dTag) + + suspend fun build( + dTag: String, + draft: Event, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = signer.nip44Encrypt(draft.toJson(), signer.pubKey), + createdAt = createdAt, + ) { + alt(ALT_DESCRIPTION) + dTag(dTag) + kind(draft.kind) + + initializer() + } + + suspend fun buildDeleted( + dTag: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = "", + createdAt = createdAt, + ) { + alt(ALT_DESCRIPTION) + dTag(dTag) + initializer() + } + + suspend fun create( + dTag: String, + draft: Event, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = signer.sign(build(dTag, draft, signer, createdAt)) + + suspend fun createDeletedEvent( + dTag: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): DraftWrapEvent = signer.sign(buildDeleted(dTag, createdAt)) + } +} From f5881d42488a521d389a6f8f65c9b01aa8ec4def Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Aug 2025 19:11:54 -0400 Subject: [PATCH 21/21] avoids using broadcasting relays to build filters to download home data --- .../AccountHomeRelayState.kt | 54 +++++++++++++++++++ .../metadata/AccountMetadataEoseManager.kt | 2 +- .../privateDM/datasource/FilterNip04DMs.kt | 2 +- .../DMsFromUserFilterSubAssembler.kt | 4 +- 4 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt new file mode 100644 index 000000000..670768683 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountHomeRelayState.kt @@ -0,0 +1,54 @@ +/** + * 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.model.nip01UserMetadata + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +class AccountHomeRelayState( + nip65: Nip65RelayListState, + privateStorage: PrivateStorageRelayListState, + local: LocalRelayListState, + scope: CoroutineScope, +) { + val flow = + combine( + nip65.outboxFlow, + privateStorage.flow, + local.flow, + ) { nip65Outbox, privateOutBox, localRelays -> + nip65Outbox + privateOutBox + localRelays + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + nip65.outboxFlow.value + + privateStorage.flow.value + + local.flow.value, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index cba60b259..0bc24ccd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -39,7 +39,7 @@ class AccountMetadataEoseManager( ) : PerUserEoseManager(client, allKeys) { override fun user(key: AccountQueryState) = key.account.userProfile() - fun relayFlow(query: AccountQueryState) = query.account.outboxRelays.flow + fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow override fun updateFilter( key: AccountQueryState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index 7fac8d0bc..3083a29cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -37,7 +37,7 @@ fun filterNip04DMs( ): List? { if (group == null || group.isEmpty() || account == null) return null - val userOutboxRelays = account.outboxRelays.flow.value + val userOutboxRelays = account.homeRelays.flow.value val userInboxRelays = account.dmRelays.flow.value val groupOutboxRelays = mutableSetOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 8463cab68..f8a2a8cff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -40,7 +40,7 @@ class DMsFromUserFilterSubAssembler( key: ChatroomListState, since: SincePerRelayMap?, ): List? = - key.account.outboxRelays.flow.value.map { + key.account.homeRelays.flow.value.map { filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time) } + key.account.dmRelays.flow.value.map { @@ -58,7 +58,7 @@ class DMsFromUserFilterSubAssembler( userJobMap[user] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.outboxRelays.flow.collectLatest { + key.account.homeRelays.flow.collectLatest { invalidateFilters() } },