From 58b97e069506f20ac342cb5ff32b41a079c74e62 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 18:47:19 +0100 Subject: [PATCH] =?UTF-8?q?Voice=20reply=20to=20VoiceEvent/VoiceReplyEvent?= =?UTF-8?q?=20(KIND=201222/1244)=20=E2=86=92=20Creates=20VoiceReplyEvent?= =?UTF-8?q?=20(KIND=201244)=20Voice=20reply=20to=20TextNoteEvent=20(KIND?= =?UTF-8?q?=201)=20=E2=86=92=20Creates=20TextNoteEvent=20(KIND=201)=20with?= =?UTF-8?q?=20audio=20IMeta=20attachment=20and=20proper=20reply=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/loggedIn/AccountViewModel.kt | 49 +++++++++++++++---- .../loggedIn/home/ShortNotePostViewModel.kt | 43 +++++++++++++--- .../loggedIn/home/VoiceReplyViewModel.kt | 36 +++++++++++--- 3 files changed, 105 insertions(+), 23 deletions(-) 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 9c983e475..085653a95 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 @@ -107,6 +107,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -134,6 +137,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log @@ -1154,8 +1158,6 @@ class AccountViewModel( context: Context, ) { if (isWriteable()) { - val hint = note.toEventHint() ?: return - launchSigner { val uploader = UploadOrchestrator() val result = @@ -1171,14 +1173,41 @@ class AccountViewModel( ) if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - account.sendVoiceReplyMessage( - result.result.url, - result.result.fileHeader.mimeType ?: recording.mimeType, - result.result.fileHeader.hash, - recording.duration, - recording.amplitudes, - hint, - ) + val audioMeta = + AudioMeta( + url = result.result.url, + mimeType = result.result.fileHeader.mimeType ?: recording.mimeType, + hash = result.result.fileHeader.hash, + duration = recording.duration, + waveform = recording.amplitudes, + ) + + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + account.sendVoiceReplyMessage( + result.result.url, + result.result.fileHeader.mimeType ?: recording.mimeType, + result.result.fileHeader.hash, + recording.duration, + recording.amplitudes, + voiceHint, + ) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + account.signAndComputeBroadcast(template) + } } else if (result is UploadingState.Error) { toastManager.toast( R.string.failed_to_upload_media_no_details, 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 cd479b134..8495a3083 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 @@ -542,18 +542,49 @@ open class ShortNotePostViewModel : voiceMetadata?.let { audioMeta -> // Only create voice reply if original note is also a voice message val originalVoiceHint = originalNote?.toEventHint() - return if (originalVoiceHint != null) { - // Create voice reply event - VoiceReplyEvent.build( + if (originalVoiceHint != null) { + // Create voice reply event (KIND 1244) + return VoiceReplyEvent.build( voiceMessage = audioMeta, replyingTo = originalVoiceHint, ) - } else { - // Create root voice event (no reply or original is not a voice message) - VoiceEvent.build( + } + // If no original note, create a standalone voice event (KIND 1222) + if (originalNote == null) { + return VoiceEvent.build( voiceMessage = audioMeta, ) } + // Otherwise, original note exists but is not a voice message + // Create a TextNoteEvent (KIND 1) with audio as IMeta attachment + return TextNoteEvent.build(audioMeta.url) { + val replyingTo = originalNote?.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + tags.forEach { + val note = accountViewModel.getNoteIfExists(it.eventId) + val ourAuthor = note?.author?.pubkeyHex + val ourHint = note?.relayHintUrl() + if (it.author == null || it.author?.isBlank() == true) { + it.author = ourAuthor + } else { + if (ourAuthor != null && it.author != ourAuthor) { + it.author = ourAuthor + } + } + if (it.relay == null) { + it.relay = ourHint + } else { + if (ourHint != null && it.relay != ourHint) { + it.relay = ourHint + } + } + } + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } } val tagger = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index faa5d677a..097aac737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -37,6 +37,10 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent @@ -200,12 +204,6 @@ class VoiceReplyViewModel : ViewModel() { is UploadingState.Finished -> { when (val orchestratorResult = result.result) { is UploadOrchestrator.OrchestratorResult.ServerResult -> { - val hint = note.toEventHint() - if (hint == null) { - accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) - return - } - val audioMeta = AudioMeta( url = orchestratorResult.url, @@ -215,7 +213,31 @@ class VoiceReplyViewModel : ViewModel() { waveform = recording.amplitudes, ) - accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, hint)) + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, voiceHint)) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val textHint = note.toEventHint() + if (textHint == null) { + accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) + return + } + + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + accountViewModel.account.signAndComputeBroadcast(template) + } if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server)