From 4c0f223673a60b3b791fb5413c17bbc1f9ab6090 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Aug 2025 19:11:29 -0400 Subject: [PATCH] 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)) + } +}