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.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<BaseVoiceEvent>() ?: 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<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) {
toastManager.toast(
R.string.failed_to_upload_media_no_details,
@@ -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<BaseVoiceEvent>()
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<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 =
@@ -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<BaseVoiceEvent>()
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<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) {
accountViewModel.account.settings.changeDefaultFileServer(server)