Draft event refactoring

This commit is contained in:
Vitor Pamplona
2025-08-13 19:11:29 -04:00
parent 251e5535d0
commit 4c0f223673
30 changed files with 376 additions and 518 deletions
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState
import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState 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.AccountOutboxRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState 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.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds 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.geohash.geohashes
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags 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.hasAnyTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.reply
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags 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.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec 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.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.Response
@@ -217,6 +213,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale import java.util.Locale
import kotlin.collections.forEach
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@Stable @Stable
@@ -297,6 +294,7 @@ class Account(
val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope)
// Relay settings // Relay settings
val homeRelays = AccountHomeRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope)
val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope) val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope)
val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope)
val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope)
@@ -1141,103 +1139,42 @@ class Account(
return event return event
} }
suspend fun <T : Event> signAndSend(
draftTag: String?,
template: EventTemplate<T>,
relayList: Set<NormalizedRelayUrl>,
broadcastNotes: List<Entity>,
) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet())
suspend fun <T : Event> signAndSend(
draftTag: String?,
template: EventTemplate<T>,
relayList: Set<NormalizedRelayUrl>,
broadcastNotes: Set<Note>,
) {
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 <T : Event> signAndSendNIP04Message(
draftTag: String?,
template: EventTemplate<T>,
relayList: Set<NormalizedRelayUrl>,
) {
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 <T : Event> signAndSendWithList(
draftTag: String?,
template: EventTemplate<T>,
relayList: Collection<NormalizedRelayUrl>,
broadcastNotes: Set<Note>,
) {
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( suspend fun createAndSendDraft(
draftTag: String, draftTag: String,
template: EventTemplate<out Event>, template: EventTemplate<out Event>,
broadcast: Set<Event> = emptySet(),
) { ) {
val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList()
val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) val rumor = RumorAssembler.assembleRumor(signer.pubKey, template)
val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer) val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer)
draftsDecryptionCache.preload(draftEvent, rumor) 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) { suspend fun deleteDraft(draftTag: String) {
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag) val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList()
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)
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, 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( suspend fun updateInteractiveStoryReadingState(
@@ -1274,7 +1222,18 @@ class Account(
currentSceneRelay = readingSceneRelay, 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<Entity>): List<Note> = fun mapEntitiesToNotes(entities: List<Entity>): List<Note> =
@@ -1328,7 +1287,15 @@ class Account(
contentWarningReason?.let { contentWarning(contentWarningReason) } 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( suspend fun sendInteractiveStoryScene(
@@ -1363,29 +1330,42 @@ class Account(
contentWarningReason?.let { contentWarning(contentWarningReason) } 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( suspend fun sendAddBounty(
value: BigDecimal, value: BigDecimal,
bounty: Note, bounty: Note,
draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
val event = bounty.event as? TextNoteEvent ?: return val bountyEvent = bounty.event as? TextNoteEvent ?: return
val eventAuthor = bounty.author ?: return val bountyAuthor = bounty.author ?: return
val template = val template =
BountyAddValueEvent.build( BountyAddValueEvent.build(
value, amount = value,
EventHintBundle(event, bounty.relayHintUrl()), bountyRoot = EventHintBundle(bountyEvent, bounty.relayHintUrl()),
eventAuthor.toPTag(), 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( suspend fun sendEdit(
@@ -1417,82 +1397,27 @@ class Account(
broadcast.forEach { client.send(it, relayList) } broadcast.forEach { client.send(it, relayList) }
} }
suspend fun sendPrivateMessage( suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) {
message: String,
toUser: PTag,
replyingTo: Note? = null,
zapReceiver: List<ZapSplitSetup>? = null,
contentWarningReason: String? = null,
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrlTag>? = null,
draftTag: String?,
) {
if (!isWriteable()) return if (!isWriteable()) return
val encryptedContent = val newEvent = signer.sign(eventTemplate)
signer.nip04Encrypt( val recipient = newEvent.verifiedRecipientPubKey()
PrivateDmEvent.prepareMessageToEncrypt(message, imetas), val destinationRelays = recipient?.let { cache.getOrCreateUser(it).dmInboxRelays() } ?: emptyList()
toUser.pubKey,
)
val template = cache.justConsumeMyOwnEvent(newEvent)
PrivateDmEvent.build(toUser, encryptedContent) { client.send(newEvent, outboxRelays.flow.value + destinationRelays)
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)
} }
suspend fun sendNIP17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) { suspend fun sendNip17EncryptedFile(template: EventTemplate<ChatMessageEncryptedFileHeaderEvent>) {
if (!isWriteable()) return if (!isWriteable()) return
val wraps = NIP17Factory().createEncryptedFileNIP17(template, signer) val wraps = NIP17Factory().createEncryptedFileNIP17(template, signer)
broadcastPrivately(wraps) broadcastPrivately(wraps)
} }
suspend fun sendNIP17PrivateMessage( suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) {
template: EventTemplate<ChatMessageEvent>, val events = NIP17Factory().createMessageNIP17(template, signer)
draftTag: String? = null, broadcastPrivately(events)
) {
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 broadcastPrivately(signedEvents: NIP17Factory.Result) { suspend fun broadcastPrivately(signedEvents: NIP17Factory.Result) {
@@ -1645,7 +1570,7 @@ class Account(
privateDMDecryptionCache.cachedDM(event) privateDMDecryptionCache.cachedDM(event)
} else if (event is LnZapRequestEvent && event.isPrivateZap()) { } else if (event is LnZapRequestEvent && event.isPrivateZap()) {
privateZapsDecryptionCache.cachedPrivateZap(event)?.content privateZapsDecryptionCache.cachedPrivateZap(event)?.content
} else if (event is DraftEvent) { } else if (event is DraftWrapEvent) {
draftsDecryptionCache.preCachedDraft(event)?.content draftsDecryptionCache.preCachedDraft(event)?.content
} else { } else {
event.content event.content
@@ -1669,7 +1594,7 @@ class Account(
} else { } else {
event.content event.content
} }
} else if (event is DraftEvent && isWriteable()) { } else if (event is DraftWrapEvent && isWriteable()) {
draftsDecryptionCache.cachedDraft(event)?.content draftsDecryptionCache.cachedDraft(event)?.content
} else { } else {
event?.content event?.content
@@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag 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.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
@@ -89,7 +89,7 @@ val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Co
val DefaultSignerPermissions = val DefaultSignerPermissions =
listOf( listOf(
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), 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_ENCRYPT),
Permission(CommandType.NIP04_DECRYPT), Permission(CommandType.NIP04_DECRYPT),
Permission(CommandType.NIP44_DECRYPT), Permission(CommandType.NIP44_DECRYPT),
@@ -120,7 +120,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent 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.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip40Expiration.expiration
@@ -2536,7 +2536,7 @@ object LocalCache : ILocalCache {
} }
fun consume( fun consume(
event: DraftEvent, event: DraftWrapEvent,
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
wasVerified: Boolean, wasVerified: Boolean,
): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified) ): Boolean = !event.isDeleted() && consumeBaseReplaceable(event, relay, wasVerified)
@@ -2744,7 +2744,7 @@ object LocalCache : ILocalCache {
is CommunityPostApprovalEvent -> consume(event, relay, wasVerified) is CommunityPostApprovalEvent -> consume(event, relay, wasVerified)
is ContactListEvent -> consume(event, relay, wasVerified) is ContactListEvent -> consume(event, relay, wasVerified)
is DeletionEvent -> 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 EmojiPackEvent -> consume(event, relay, wasVerified)
is EmojiPackSelectionEvent -> consume(event, relay, wasVerified) is EmojiPackSelectionEvent -> consume(event, relay, wasVerified)
is EphemeralChatEvent -> consume(event, relay, wasVerified) is EphemeralChatEvent -> consume(event, relay, wasVerified)
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import coil3.util.CoilUtils.result
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.service.checkNotInMainThread 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.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW 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.LnZapPaymentRequestEvent
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
@@ -236,7 +235,7 @@ open class Note(
open fun createdAt() = event?.createdAt open fun createdAt() = event?.createdAt
fun isDraft() = event is DraftEvent fun isDraft() = event is DraftWrapEvent
fun loadEvent( fun loadEvent(
event: Event, event: Event,
@@ -44,8 +44,8 @@ class AccountOutboxRelayState(
privateStorage.flow, privateStorage.flow,
local.flow, local.flow,
broadcast.flow, broadcast.flow,
) { nip65Inbox, privateOutBox, localRelays, broadcastRelays -> ) { nip65Outbox, privateOutBox, localRelays, broadcastRelays ->
nip65Inbox + privateOutBox + localRelays + broadcastRelays nip65Outbox + privateOutBox + localRelays + broadcastRelays
}.flowOn(Dispatchers.Default) }.flowOn(Dispatchers.Default)
.stateIn( .stateIn(
scope, scope,
@@ -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.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
val ReportsAndBookmarksFromKeyKinds = val ReportsAndBookmarksFromKeyKinds =
listOf( listOf(
DraftEvent.KIND, DraftWrapEvent.KIND,
ReportEvent.KIND, ReportEvent.KIND,
BookmarkListEvent.KIND, BookmarkListEvent.KIND,
) )
@@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel 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.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
@@ -63,7 +63,7 @@ fun routeFor(
noteEvent: Event, noteEvent: Event,
loggedIn: Account, loggedIn: Account,
): Route? { ): Route? {
if (noteEvent is DraftEvent) { if (noteEvent is DraftWrapEvent) {
val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent) val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent)
if (innerEvent is IsInPublicChatChannel) { if (innerEvent is IsInPublicChatChannel) {
@@ -187,7 +187,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent 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.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent 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 (isNotRepost) {
if (makeItShort) { if (makeItShort) {
@@ -568,7 +568,7 @@ fun InnerNoteWithReactions(
) )
} }
} else { } else {
if (baseNote.event is DraftEvent) { if (baseNote.event is DraftWrapEvent) {
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
} }
} }
@@ -647,7 +647,7 @@ private fun RenderNoteRow(
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav) is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
is AudioTrackEvent -> RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) is AudioTrackEvent -> RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav)
is AudioHeaderEvent -> RenderAudioHeader(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 ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
@@ -901,7 +901,7 @@ fun ObserveDraftEvent(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
render: @Composable (Note) -> Unit, render: @Composable (Note) -> Unit,
) { ) {
val noteEvent by observeNoteEvent<DraftEvent>(note, accountViewModel) val noteEvent by observeNoteEvent<DraftWrapEvent>(note, accountViewModel)
noteEvent?.let { noteEvent?.let {
val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it) val innerNote by produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = it)
@@ -179,7 +179,6 @@ class AddBountyAmountViewModel : ViewModel() {
myAccount.sendAddBounty( myAccount.sendAddBounty(
newValue, newValue,
bountyInner, bountyInner,
draftTag = null,
) )
} }
} }
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive 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.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -202,7 +202,7 @@ open class CommentPostViewModel :
val noteEvent = draft.event val noteEvent = draft.event
val noteAuthor = draft.author val noteAuthor = draft.author
if (noteEvent is DraftEvent && noteAuthor != null) { if (noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() val oldTag = (draft.event as? AddressableEvent)?.dTag()
@@ -324,13 +324,14 @@ open class CommentPostViewModel :
if (message.text.isBlank()) { if (message.text.isBlank()) {
accountViewModel.account.deleteDraft(draftTag.current) accountViewModel.account.deleteDraft(draftTag.current)
} else { } else {
val template = createTemplate() ?: return val attachments = mutableSetOf<Event>()
accountViewModel.account.createAndSendDraft(draftTag.current, template)
nip95attachments.forEach { nip95attachments.forEach {
account.sendToPrivateOutboxAndLocal(it.first) attachments.add(it.first)
account.sendToPrivateOutboxAndLocal(it.second) attachments.add(it.second)
} }
val template = createTemplate() ?: return
accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
} }
} }
@@ -115,7 +115,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel 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.Nip47WalletConnect
import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
@@ -1510,7 +1510,7 @@ class AccountViewModel(
account.deleteDraft(draftTag) account.deleteDraft(draftTag)
} }
suspend fun createTempDraftNote(noteEvent: DraftEvent): Note? = draftNoteCache.update(noteEvent) suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent)
fun createTempDraftNote( fun createTempDraftNote(
innerEvent: Event, innerEvent: Event,
@@ -1691,8 +1691,8 @@ class AccountViewModel(
class CachedDraftNotes( class CachedDraftNotes(
val accountViewModel: AccountViewModel, val accountViewModel: AccountViewModel,
) : GenericBaseCacheAsync<DraftEvent, Note>(20) { ) : GenericBaseCacheAsync<DraftWrapEvent, Note>(20) {
override suspend fun compute(key: DraftEvent): Note? = override suspend fun compute(key: DraftWrapEvent): Note? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key) val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key)
if (decrypted != null) { if (decrypted != null) {
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.IEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent 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.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -74,7 +74,7 @@ class EventProcessor(
when (event) { when (event) {
is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote) is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote)
is OtsEvent -> otsHandler.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 GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote)
is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote)
is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote)
@@ -98,7 +98,7 @@ class EventProcessor(
when (event) { when (event) {
is ChatroomKeyable -> chatHandler.delete(event, note) is ChatroomKeyable -> chatHandler.delete(event, note)
is OtsEvent -> otsHandler.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 GiftWrapEvent -> giftWrapHandler.delete(event, note)
is SealedRumorEvent -> sealHandler.delete(event, note) is SealedRumorEvent -> sealHandler.delete(event, note)
is LnZapRequestEvent -> zapRequest.delete(event, note) is LnZapRequestEvent -> zapRequest.delete(event, note)
@@ -129,7 +129,7 @@ class EventProcessor(
val deletedDrafts = val deletedDrafts =
newNotes.mapNotNull { note -> newNotes.mapNotNull { note ->
val event = note.event val event = note.event
if (event is DraftEvent && if (event is DraftWrapEvent &&
event.isDeleted() && event.isDeleted() &&
!cache.deletionIndex.hasBeenDeleted(event) !cache.deletionIndex.hasBeenDeleted(event)
) { ) {
@@ -193,9 +193,9 @@ class OtsEventHandler(
class DraftEventHandler( class DraftEventHandler(
private val account: Account, private val account: Account,
private val cache: LocalCache, private val cache: LocalCache,
) : EventHandler<DraftEvent> { ) : EventHandler<DraftWrapEvent> {
override suspend fun add( override suspend fun add(
event: DraftEvent, event: DraftWrapEvent,
eventNote: Note, eventNote: Note,
publicNote: Note, publicNote: Note,
) { ) {
@@ -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.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
@Composable @Composable
fun RefreshingChatroomFeedView( fun RefreshingChatroomFeedView(
@@ -129,7 +129,7 @@ fun ChatFeedLoaded(
) { ) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item -> itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item ->
val noteEvent = item.event 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( ChatroomMessageCompose(
baseNote = item, baseNote = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent 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.splits.hasZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -162,7 +162,7 @@ fun NormalChatNote(
ChatBubbleLayout( ChatBubbleLayout(
isLoggedInUser = isLoggedInUser, isLoggedInUser = isLoggedInUser,
isDraft = note.event is DraftEvent, isDraft = note.event is DraftWrapEvent,
innerQuote = innerQuote, innerQuote = innerQuote,
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE, isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(), hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
@@ -289,7 +289,7 @@ private fun MessageBubbleLines(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
if (baseNote.event !is DraftEvent) { if (baseNote.event !is DraftWrapEvent) {
RenderReplyRow( RenderReplyRow(
note = baseNote, note = baseNote,
innerQuote = innerQuote, innerQuote = innerQuote,
@@ -388,7 +388,7 @@ fun NoteRow(
when (note.event) { when (note.event) {
is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav) is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav)
is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(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) is ChatMessageEncryptedFileHeaderEvent -> RenderEncryptedFile(note, bgColor, accountViewModel, nav)
else -> RenderRegularTextNote(note, canPreview, innerQuote, bgColor, accountViewModel, nav) else -> RenderRegularTextNote(note, canPreview, innerQuote, bgColor, accountViewModel, nav)
} }
@@ -107,7 +107,7 @@ fun NewChatroomSubjectDialog(
groupName.value.ifBlank { null }?.let { changeSubject(it) } groupName.value.ifBlank { null }?.let { changeSubject(it) }
} }
accountViewModel.account.sendNIP17PrivateMessage(template) accountViewModel.account.sendNip17PrivateMessage(template)
} }
onClose() onClose()
@@ -79,7 +79,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive 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.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
@@ -252,7 +252,7 @@ class ChatNewMessageViewModel :
val noteEvent = draft.event val noteEvent = draft.event
val noteAuthor = draft.author val noteAuthor = draft.author
if (noteEvent is DraftEvent && noteAuthor != null) { if (noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() 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 room = room ?: return
val urls = findURLs(message.text) 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 { } else {
accountViewModel.account.sendPrivateMessage( val toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
message = message,
toUser = room.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, val template =
replyingTo = replyTo.value, PrivateDmEvent.build(
contentWarningReason = null, toUser = toUser,
imetas = usedAttachments, message = message,
draftTag = dTag, 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) ChatFileSender(room, accountViewModel.account).sendAll(uploadsWaitingToBeSent)
} }
} }
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments 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.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
@@ -48,7 +49,7 @@ class ChatFileSender(
contentWarningReason: String?, contentWarningReason: String?,
cipher: AESGCM, cipher: AESGCM,
) { ) {
account.sendNIP17EncryptedFile( account.sendNip17EncryptedFile(
ChatMessageEncryptedFileHeaderEvent.build( ChatMessageEncryptedFileHeaderEvent.build(
url = result.url, url = result.url,
to = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() }, to = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() },
@@ -89,18 +90,18 @@ class ChatFileSender(
val iMetaAttachments = IMetaAttachments() val iMetaAttachments = IMetaAttachments()
iMetaAttachments.add(result, caption, contentWarningReason) iMetaAttachments.add(result, caption, contentWarningReason)
account.sendPrivateMessage( val toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }
message = result.url,
toUser = chatroom.users.first().let { LocalCache.getOrCreateUser(it).toPTag() }, val template =
replyingTo = null, PrivateDmEvent.build(
zapReceiver = null, toUser = toUser,
contentWarningReason = null, message = result.url,
zapRaiserAmount = null, imetas = iMetaAttachments.iMetaAttachments,
geohash = null, replyingTo = null,
imetas = iMetaAttachments.iMetaAttachments, signer = account.signer,
emojis = null, )
draftTag = null,
) account.sendNip04PrivateMessage(template)
} }
suspend fun sendAll(uploads: List<SuccessfulUploads>) { suspend fun sendAll(uploads: List<SuccessfulUploads>) {
@@ -82,7 +82,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive 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.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
@@ -191,7 +191,7 @@ open class ChannelNewMessageViewModel :
val noteEvent = draft.event val noteEvent = draft.event
val noteAuthor = draft.author val noteAuthor = draft.author
if (noteEvent is DraftEvent && noteAuthor != null) { if (noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() val oldTag = (draft.event as? AddressableEvent)?.dTag()
@@ -281,8 +281,14 @@ open class ChannelNewMessageViewModel :
if (message.text.isBlank()) { if (message.text.isBlank()) {
account.deleteDraft(draftTag.current) account.deleteDraft(draftTag.current)
} else { } else {
val attachments = mutableSetOf<Event>()
nip95attachments.forEach {
attachments.add(it.first)
attachments.add(it.second)
}
val template = createTemplate() ?: return val template = createTemplate() ?: return
accountViewModel.account.createAndSendDraft(draftTag.current, template) accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
} }
} }
@@ -86,7 +86,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
@Composable @Composable
fun ChatroomHeaderCompose( fun ChatroomHeaderCompose(
@@ -113,7 +113,7 @@ fun ChatroomComposeChannelOrUser(
nav: INav, nav: INav,
) { ) {
val baseNoteEvent = baseNote.event val baseNoteEvent = baseNote.event
if (baseNoteEvent is DraftEvent) { if (baseNoteEvent is DraftWrapEvent) {
ObserveDraftEvent(baseNote, accountViewModel) { innerNote -> ObserveDraftEvent(baseNote, accountViewModel) { innerNote ->
ChatroomEntry(innerNote, accountViewModel, nav) ChatroomEntry(innerNote, accountViewModel, nav)
} }
@@ -78,7 +78,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive 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.splits.zapSplits
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -192,7 +192,7 @@ open class NewProductViewModel :
val noteEvent = draft.event val noteEvent = draft.event
val noteAuthor = draft.author val noteAuthor = draft.author
if (noteEvent is DraftEvent && noteAuthor != null) { if (noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() val oldTag = (draft.event as? AddressableEvent)?.dTag()
@@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
class DraftEventsFeedFilter( class DraftEventsFeedFilter(
val account: Account, val account: Account,
@@ -48,7 +48,7 @@ class DraftEventsFeedFilter(
fun acceptableEvent(it: Note): Boolean { fun acceptableEvent(it: Note): Boolean {
val noteEvent = it.event 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<Note>): List<Note> = items.sortedWith(DefaultFeedOrder) override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
@@ -96,7 +96,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW 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.ZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
@@ -241,7 +241,7 @@ open class ShortNotePostViewModel :
val noteEvent = draft?.event val noteEvent = draft?.event
val noteAuthor = draft?.author val noteAuthor = draft?.author
if (draft != null && noteEvent is DraftEvent && noteAuthor != null) { if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() val oldTag = (draft.event as? AddressableEvent)?.dTag()
@@ -483,18 +483,17 @@ open class ShortNotePostViewModel :
} }
suspend fun sendDraftSync() { suspend fun sendDraftSync() {
val accountViewModel = accountViewModel
if (message.text.isBlank()) { if (message.text.isBlank()) {
accountViewModel.account.deleteDraft(draftTag.current) accountViewModel.account.deleteDraft(draftTag.current)
} else { } else {
val template = createTemplate() ?: return val attachments = mutableSetOf<Event>()
accountViewModel.account.createAndSendDraft(draftTag.current, template)
nip95attachments.forEach { nip95attachments.forEach {
account.sendToPrivateOutboxAndLocal(it.first) attachments.add(it.first)
account.sendToPrivateOutboxAndLocal(it.second) attachments.add(it.second)
} }
val template = createTemplate() ?: return
accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments)
} }
} }
@@ -85,7 +85,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive 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.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
@@ -235,7 +235,7 @@ class NewPublicMessageViewModel :
val noteEvent = draft.event val noteEvent = draft.event
val noteAuthor = draft.author val noteAuthor = draft.author
if (noteEvent is DraftEvent && noteAuthor != null) { if (noteEvent is DraftWrapEvent && noteAuthor != null) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag() val oldTag = (draft.event as? AddressableEvent)?.dTag()
@@ -323,18 +323,17 @@ class NewPublicMessageViewModel :
} }
suspend fun sendDraftSync() { suspend fun sendDraftSync() {
val accountViewModel = accountViewModel
if (message.text.isBlank()) { if (message.text.isBlank()) {
accountViewModel.account.deleteDraft(draftTag.current) accountViewModel.account.deleteDraft(draftTag.current)
} else { } else {
val template = createTemplate() ?: return val broadcast = mutableSetOf<Event>()
accountViewModel.account.createAndSendDraft(draftTag.current, template)
nip95attachments.forEach { nip95attachments.forEach {
account.sendToPrivateOutboxAndLocal(it.first) broadcast.add(it.first)
account.sendToPrivateOutboxAndLocal(it.second) broadcast.add(it.second)
} }
val template = createTemplate() ?: return
accountViewModel.account.createAndSendDraft(draftTag.current, template, broadcast)
} }
} }
@@ -208,7 +208,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent 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.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent 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) RenderGitIssueEvent(baseNote, makeItShort = false, canPreview = true, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is AppDefinitionEvent) { } else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav) RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is DraftEvent) { } else if (noteEvent is DraftWrapEvent) {
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav) RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) { } else if (noteEvent is HighlightEvent) {
RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav) RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav)
@@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent 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.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -216,7 +216,7 @@ class EventFactory {
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.KIND -> DeletionEvent(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) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig) EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate 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.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
@@ -121,16 +122,37 @@ class PrivateDmEvent(
} }
} }
fun build( suspend fun build(
to: PTag, toUser: PTag,
encryptedMessage: String, message: String,
imetas: List<IMetaTag>? = null,
replyingTo: EventReference? = null,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
signer: NostrSigner,
initializer: TagArrayBuilder<PrivateDmEvent>.() -> Unit = {}, initializer: TagArrayBuilder<PrivateDmEvent>.() -> Unit = {},
) = eventTemplate(KIND, encryptedMessage, createdAt) { ) = eventTemplate(
kind = KIND,
description =
signer.nip04Encrypt(
prepareMessageToEncrypt(message, imetas),
toUser.pubKey,
),
createdAt = createdAt,
) {
alt(ALT) alt(ALT)
pTag(to) pTag(toUser)
replyingTo?.let { reply(it) }
initializer() initializer()
} }
suspend fun create(
to: PTag,
message: String,
imetas: List<IMetaTag>? = null,
replyingTo: EventReference?,
createdAt: Long = TimeUtils.now(),
signer: NostrSigner,
) = signer.sign(build(to, message, imetas, replyingTo, createdAt, signer))
} }
} }
@@ -35,11 +35,11 @@ class DraftBuilder {
draft: T, draft: T,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
): DraftEvent { ): DraftWrapEvent {
val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey) val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey)
val template = val template =
eventTemplate<DraftEvent>(DraftEvent.KIND, encryptedContent, createdAt) { eventTemplate<DraftWrapEvent>(DraftWrapEvent.KIND, encryptedContent, createdAt) {
alt(DraftEvent.ALT_DESCRIPTION) alt(DraftWrapEvent.ALT_DESCRIPTION)
dTag(dTag) dTag(dTag)
kind(draft.kind) kind(draft.kind)
@@ -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<Array<String>>,
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<Array<String>>()
return create(dTag, originalNote, tags, signer, createdAt)
}
suspend fun create(
dTag: String,
originalNote: LiveActivitiesChatMessageEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
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<Array<String>>()
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<Array<String>>()
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<Array<String>> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): DraftEvent {
val tags = mutableListOf<Array<String>>()
tags.add(arrayOf("d", dTag))
tags.add(arrayOf("k", "${innerEvent.kind}"))
if (anchorTagArray.isNotEmpty()) {
tags.addAll(anchorTagArray)
}
val draft =
signer.sign<DraftEvent>(
createdAt = createdAt,
kind = KIND,
tags = tags.toTypedArray(),
content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey),
)
return draft
}
}
}
@@ -29,8 +29,8 @@ class DraftEventCache(
signer: NostrSigner, signer: NostrSigner,
) { ) {
private val decryptionCache = private val decryptionCache =
object : LruCache<DraftEvent, DraftEventDecryptCache>(1000) { object : LruCache<DraftWrapEvent, DraftEventDecryptCache>(1000) {
override fun create(key: DraftEvent): DraftEventDecryptCache? = override fun create(key: DraftWrapEvent): DraftEventDecryptCache? =
if (!key.isDeleted() && key.pubKey == signer.pubKey) { if (!key.isDeleted() && key.pubKey == signer.pubKey) {
DraftEventDecryptCache(signer) DraftEventDecryptCache(signer)
} else { } else {
@@ -38,23 +38,23 @@ class DraftEventCache(
} }
} }
fun delete(event: DraftEvent) = decryptionCache.remove(event) fun delete(event: DraftWrapEvent) = decryptionCache.remove(event)
fun preload( fun preload(
event: DraftEvent, event: DraftWrapEvent,
result: Event, result: Event,
) = decryptionCache[event]?.preload(result) ) = 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( class DraftEventDecryptCache(
signer: NostrSigner, signer: NostrSigner,
) : DecryptCache<DraftEvent, Event>(signer) { ) : DecryptCache<DraftWrapEvent, Event>(signer) {
override suspend fun decryptAndParse( override suspend fun decryptAndParse(
event: DraftEvent, event: DraftWrapEvent,
signer: NostrSigner, signer: NostrSigner,
): Event = event.decryptInnerEvent(signer) ): Event = event.decryptInnerEvent(signer)
} }
@@ -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<Array<String>>,
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<DraftWrapEvent>.() -> 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<DraftWrapEvent>.() -> 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))
}
}