Merge pull request #1660 from davotoula/1654-voice-reply-to-text-note-as-kind-1
voice reply to text note as kind 1
This commit is contained in:
@@ -100,6 +100,13 @@ fun RenderTextEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is an audio-only event (content is just an audio URL with waveform IMeta)
|
||||||
|
val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() }
|
||||||
|
if (isAudioOnly) {
|
||||||
|
RenderAudioFromIMeta(note, accountViewModel, nav)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
LoadDecryptedContent(
|
LoadDecryptedContent(
|
||||||
note,
|
note,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size75Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size75Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||||
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
|
||||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -107,6 +110,32 @@ fun VoiceHeader(
|
|||||||
?.let { WaveformData(it) }
|
?.let { WaveformData(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderAudioWithWaveform(
|
||||||
|
mediaUrl = media,
|
||||||
|
title = noteEvent.subject(),
|
||||||
|
mimeType = null,
|
||||||
|
waveform = waveform,
|
||||||
|
note = note,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared composable for rendering audio with waveform visualization.
|
||||||
|
* Used by both VoiceHeader (for BaseVoiceEvent) and RenderAudioFromIMeta (for other events with audio IMeta).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderAudioWithWaveform(
|
||||||
|
mediaUrl: String,
|
||||||
|
title: String?,
|
||||||
|
mimeType: String?,
|
||||||
|
waveform: WaveformData?,
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event ?: return
|
||||||
val callbackUri = remember(note) { note.toNostrUri() }
|
val callbackUri = remember(note) { note.toNostrUri() }
|
||||||
|
|
||||||
Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
@@ -114,14 +143,14 @@ fun VoiceHeader(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
GetMediaItem(
|
GetMediaItem(
|
||||||
videoUri = media,
|
videoUri = mediaUrl,
|
||||||
title = noteEvent.subject(),
|
title = title,
|
||||||
artworkUri = null,
|
artworkUri = null,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
callbackUri = callbackUri,
|
callbackUri = callbackUri,
|
||||||
mimeType = null,
|
mimeType = mimeType,
|
||||||
aspectRatio = null,
|
aspectRatio = null,
|
||||||
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media),
|
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(mediaUrl),
|
||||||
keepPlaying = false,
|
keepPlaying = false,
|
||||||
waveformData = waveform,
|
waveformData = waveform,
|
||||||
) { mediaItem ->
|
) { mediaItem ->
|
||||||
@@ -168,7 +197,7 @@ fun RenderVoicePlayer(
|
|||||||
factory = { context: Context ->
|
factory = { context: Context ->
|
||||||
PlayerView(context).apply {
|
PlayerView(context).apply {
|
||||||
player = controllerState.controller
|
player = controllerState.controller
|
||||||
// if we alrady know the size of the frame, this forces the player to stay in the size
|
// if we already know the size of the frame, this forces the player to stay in the size
|
||||||
layoutParams =
|
layoutParams =
|
||||||
FrameLayout.LayoutParams(
|
FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
@@ -263,3 +292,54 @@ fun PlayPauseButton(controllerState: MediaControllerState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts AudioMeta from an event's IMeta tags if it has audio content with waveform.
|
||||||
|
* Returns the first audio IMeta that has a waveform, or null if none found.
|
||||||
|
*/
|
||||||
|
fun Event.getAudioMetaWithWaveform(): AudioMeta? {
|
||||||
|
val audioMetas = imetas().map { AudioMeta.parse(it) }
|
||||||
|
return audioMetas.firstOrNull { meta ->
|
||||||
|
meta.waveform != null &&
|
||||||
|
(meta.mimeType == null || meta.mimeType?.startsWith("audio/") == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the event content is primarily an audio attachment (content is just the audio URL).
|
||||||
|
*/
|
||||||
|
fun Event.isAudioOnlyContent(): Boolean {
|
||||||
|
val audioMeta = getAudioMetaWithWaveform() ?: return false
|
||||||
|
return content.trim() == audioMeta.url
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders audio with waveform for any event type that has audio IMeta attachment.
|
||||||
|
* This allows KIND 1 (TextNoteEvent) and KIND 1111 (CommentEvent) with voice
|
||||||
|
* attachments to display the same waveform UI as VoiceEvent.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderAudioFromIMeta(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event ?: return
|
||||||
|
|
||||||
|
val audioMeta = remember(noteEvent) { noteEvent.getAudioMetaWithWaveform() } ?: return
|
||||||
|
|
||||||
|
val waveform =
|
||||||
|
remember(audioMeta) {
|
||||||
|
audioMeta.waveform?.let { WaveformData(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderAudioWithWaveform(
|
||||||
|
mediaUrl = audioMeta.url,
|
||||||
|
title = null,
|
||||||
|
mimeType = audioMeta.mimeType,
|
||||||
|
waveform = waveform,
|
||||||
|
note = note,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+32
-60
@@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -67,12 +66,8 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
|||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||||
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
|
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||||
import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager
|
import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||||
@@ -107,6 +102,7 @@ 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.tags.MarkedETag
|
||||||
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,7 +130,6 @@ 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.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
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
@@ -390,7 +385,7 @@ class AccountViewModel(
|
|||||||
note.flow().author(),
|
note.flow().author(),
|
||||||
note.flow().metadata.stateFlow,
|
note.flow().metadata.stateFlow,
|
||||||
note.flow().reports.stateFlow,
|
note.flow().reports.stateFlow,
|
||||||
) { hiddenUsers, followingUsers, autor, metadata, reports ->
|
) { hiddenUsers, followingUsers, _, metadata, _ ->
|
||||||
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
|
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors))
|
||||||
}.onStart {
|
}.onStart {
|
||||||
emit(
|
emit(
|
||||||
@@ -998,6 +993,32 @@ class AccountViewModel(
|
|||||||
|
|
||||||
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
|
fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes author and relay hints in MarkedETag list by looking up notes from cache.
|
||||||
|
* This ensures reply tags have proper author pubkeys and relay hints for threading.
|
||||||
|
*/
|
||||||
|
fun fixReplyTagHints(tags: List<MarkedETag>) {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
val note = getNoteIfExists(tag.eventId)
|
||||||
|
val cachedAuthor = note?.author?.pubkeyHex
|
||||||
|
val cachedRelay = note?.relayHintUrl()
|
||||||
|
|
||||||
|
// Fix author if missing or different from cached
|
||||||
|
if (tag.author.isNullOrBlank() && cachedAuthor != null) {
|
||||||
|
tag.author = cachedAuthor
|
||||||
|
} else if (cachedAuthor != null && tag.author != cachedAuthor) {
|
||||||
|
tag.author = cachedAuthor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix relay hint if missing or different from cached
|
||||||
|
if (tag.relay == null && cachedRelay != null) {
|
||||||
|
tag.relay = cachedRelay
|
||||||
|
} else if (cachedRelay != null && tag.relay != cachedRelay) {
|
||||||
|
tag.relay = cachedRelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address)
|
override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address)
|
||||||
|
|
||||||
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key)
|
||||||
@@ -1011,11 +1032,11 @@ class AccountViewModel(
|
|||||||
LocalCache.findLatestModificationForNote(note)
|
LocalCache.findLatestModificationForNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key)
|
fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key)
|
||||||
|
|
||||||
fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key)
|
fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key)
|
||||||
|
|
||||||
fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key)
|
fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel = LocalCache.getOrCreateEphemeralChannel(key)
|
||||||
|
|
||||||
fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex)
|
fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex)
|
||||||
|
|
||||||
@@ -1148,53 +1169,6 @@ class AccountViewModel(
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendVoiceReply(
|
|
||||||
note: Note,
|
|
||||||
recording: RecordingResult,
|
|
||||||
context: Context,
|
|
||||||
) {
|
|
||||||
if (isWriteable()) {
|
|
||||||
val hint = note.toEventHint<BaseVoiceEvent>() ?: return
|
|
||||||
|
|
||||||
launchSigner {
|
|
||||||
val uploader = UploadOrchestrator()
|
|
||||||
val result =
|
|
||||||
uploader.upload(
|
|
||||||
uri = recording.file.toUri(),
|
|
||||||
mimeType = recording.mimeType,
|
|
||||||
alt = null,
|
|
||||||
contentWarningReason = null,
|
|
||||||
compressionQuality = CompressorQuality.UNCOMPRESSED,
|
|
||||||
server = account.settings.defaultFileServer,
|
|
||||||
account = account,
|
|
||||||
context = context,
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
} else if (result is UploadingState.Error) {
|
|
||||||
toastManager.toast(
|
|
||||||
R.string.failed_to_upload_media_no_details,
|
|
||||||
result.errorResource,
|
|
||||||
*result.params,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toastManager.toast(
|
|
||||||
R.string.read_only_user,
|
|
||||||
R.string.login_with_a_private_key_to_be_able_to_reply,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadThumb(
|
fun loadThumb(
|
||||||
context: Context,
|
context: Context,
|
||||||
thumbUri: String,
|
thumbUri: String,
|
||||||
@@ -1422,7 +1396,7 @@ class AccountViewModel(
|
|||||||
// First check if we have an actual response from the DVM in LocalCache
|
// First check if we have an actual response from the DVM in LocalCache
|
||||||
val response =
|
val response =
|
||||||
LocalCache.notes.maxOrNullOf(
|
LocalCache.notes.maxOrNullOf(
|
||||||
filter = { key, note ->
|
filter = { _, note ->
|
||||||
val noteEvent = note.event
|
val noteEvent = note.event
|
||||||
noteEvent is NIP90ContentDiscoveryResponseEvent &&
|
noteEvent is NIP90ContentDiscoveryResponseEvent &&
|
||||||
noteEvent.pubKey == pubkeyHex &&
|
noteEvent.pubKey == pubkeyHex &&
|
||||||
@@ -1531,8 +1505,6 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account)
|
|
||||||
|
|
||||||
fun convertAccounts(loggedInAccounts: List<AccountInfo>?): Set<HexKey> =
|
fun convertAccounts(loggedInAccounts: List<AccountInfo>?): Set<HexKey> =
|
||||||
loggedInAccounts
|
loggedInAccounts
|
||||||
?.mapNotNull {
|
?.mapNotNull {
|
||||||
|
|||||||
+33
-6
@@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
|||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
import com.vitorpamplona.quartz.nip01Core.tags.people.pTags
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
@@ -542,18 +543,44 @@ 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)
|
||||||
|
accountViewModel.fixReplyTagHints(tags)
|
||||||
|
markedETags(tags)
|
||||||
|
notify(replyingTo.toPTag())
|
||||||
|
}
|
||||||
|
pTags?.let { userList ->
|
||||||
|
val tags =
|
||||||
|
userList.map {
|
||||||
|
val tag = it.toPTag()
|
||||||
|
if (tag.relayHint == null) {
|
||||||
|
tag.copy(relayHint = LocalCache.relayHints.hintsForKey(it.pubkeyHex).firstOrNull())
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify(tags)
|
||||||
|
}
|
||||||
|
// Add audio as IMeta attachment
|
||||||
|
add(audioMeta.toIMetaArray())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
|
|||||||
+29
-7
@@ -37,6 +37,11 @@ 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.tags.people.toPTag
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.tags.markedETags
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.tags.notify
|
||||||
|
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 +205,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 +214,30 @@ 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<TextNoteEvent>()
|
||||||
|
if (textHint == null) {
|
||||||
|
accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val template =
|
||||||
|
TextNoteEvent.build(audioMeta.url) {
|
||||||
|
val tags = prepareETagsAsReplyTo(textHint, null)
|
||||||
|
accountViewModel.fixReplyTagHints(tags)
|
||||||
|
markedETags(tags)
|
||||||
|
notify(textHint.toPTag())
|
||||||
|
// 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user