Voice reply to VoiceEvent/VoiceReplyEvent (KIND 1222/1244) → Creates VoiceReplyEvent (KIND 1244)

Voice reply to TextNoteEvent (KIND 1) → Creates TextNoteEvent (KIND 1) with audio IMeta attachment and proper reply tags
This commit is contained in:
davotoula
2026-01-10 18:47:19 +01:00
parent d24fe03bdd
commit 58b97e0695
3 changed files with 105 additions and 23 deletions
@@ -107,6 +107,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent 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.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent 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.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
@@ -1154,8 +1158,6 @@ class AccountViewModel(
context: Context, context: Context,
) { ) {
if (isWriteable()) { if (isWriteable()) {
val hint = note.toEventHint<BaseVoiceEvent>() ?: return
launchSigner { launchSigner {
val uploader = UploadOrchestrator() val uploader = UploadOrchestrator()
val result = val result =
@@ -1171,14 +1173,41 @@ class AccountViewModel(
) )
if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
account.sendVoiceReplyMessage( val audioMeta =
result.result.url, AudioMeta(
result.result.fileHeader.mimeType ?: recording.mimeType, url = result.result.url,
result.result.fileHeader.hash, mimeType = result.result.fileHeader.mimeType ?: recording.mimeType,
recording.duration, hash = result.result.fileHeader.hash,
recording.amplitudes, duration = recording.duration,
hint, waveform = recording.amplitudes,
) )
// Check if replying to a voice event
val voiceHint = note.toEventHint<BaseVoiceEvent>()
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<TextNoteEvent>()
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) { } else if (result is UploadingState.Error) {
toastManager.toast( toastManager.toast(
R.string.failed_to_upload_media_no_details, R.string.failed_to_upload_media_no_details,
@@ -542,18 +542,49 @@ open class ShortNotePostViewModel :
voiceMetadata?.let { audioMeta -> voiceMetadata?.let { audioMeta ->
// Only create voice reply if original note is also a voice message // Only create voice reply if original note is also a voice message
val originalVoiceHint = originalNote?.toEventHint<BaseVoiceEvent>() val originalVoiceHint = originalNote?.toEventHint<BaseVoiceEvent>()
return if (originalVoiceHint != null) { if (originalVoiceHint != null) {
// Create voice reply event // Create voice reply event (KIND 1244)
VoiceReplyEvent.build( return VoiceReplyEvent.build(
voiceMessage = audioMeta, voiceMessage = audioMeta,
replyingTo = originalVoiceHint, replyingTo = originalVoiceHint,
) )
} else { }
// Create root voice event (no reply or original is not a voice message) // If no original note, create a standalone voice event (KIND 1222)
VoiceEvent.build( if (originalNote == null) {
return VoiceEvent.build(
voiceMessage = audioMeta, 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<TextNoteEvent>()
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 = val tagger =
@@ -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.actions.uploads.RecordingResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes 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.AudioMeta
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
@@ -200,12 +204,6 @@ class VoiceReplyViewModel : ViewModel() {
is UploadingState.Finished -> { is UploadingState.Finished -> {
when (val orchestratorResult = result.result) { when (val orchestratorResult = result.result) {
is UploadOrchestrator.OrchestratorResult.ServerResult -> { is UploadOrchestrator.OrchestratorResult.ServerResult -> {
val hint = note.toEventHint<BaseVoiceEvent>()
if (hint == null) {
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
return
}
val audioMeta = val audioMeta =
AudioMeta( AudioMeta(
url = orchestratorResult.url, url = orchestratorResult.url,
@@ -215,7 +213,31 @@ class VoiceReplyViewModel : ViewModel() {
waveform = recording.amplitudes, waveform = recording.amplitudes,
) )
accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, hint)) // Check if replying to a voice event
val voiceHint = note.toEventHint<BaseVoiceEvent>()
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<Event>()
if (textHint == null) {
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
return
}
val template =
TextNoteEvent.build(audioMeta.url) {
val replyingTo = note.toEventHint<TextNoteEvent>()
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) { if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server) accountViewModel.account.settings.changeDefaultFileServer(server)