refactor: voice recorder slots, shared poll/live helpers and the Marmot icon picker

- RecordVoiceButton and VoiceMessagePreview become NotePlatform slots (the
  Android recorder and MediaPlayer preview stay in the app);
  MAX_VOICE_RECORD_SECONDS moves beside them. ShortNotePostScreen reads
  saved accounts from the host instead of LocalPreferences.
- LiveStatusIndicator probes streams and relays through isVideoOnline.
- PollDeadlinePicker uses LocalClock and rememberIs24HourClock.
- MarmotGroupIconEditor picks through GallerySelectSingle; the icon uploader
  takes a MediaUri and drops its temp copy through the new
  MediaUploader.discardTempFile, with uploadMarmotGroupIcon beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-30 12:20:32 +00:00
parent 9d9f2dcfae
commit 5b9bd82a14
17 changed files with 140 additions and 72 deletions
@@ -612,4 +612,12 @@ class AndroidMediaUploader(
val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null
}
override fun discardTempFile(uri: MediaUri) {
try {
uri.path?.let { path -> File(path).takeIf { it.exists() }?.delete() }
} catch (e: Exception) {
Log.w("AndroidMediaUploader", "Failed to delete temp file", e)
}
}
}
@@ -44,8 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
const val MAX_VOICE_RECORD_SECONDS = 600
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun RecordAudioBox(
@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.resources.record_a_message
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description
import com.vitorpamplona.amethyst.commons.resources.remove
import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly
import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatSecondsToTime
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
@@ -189,6 +189,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ZapRail
import com.vitorpamplona.amethyst.commons.ui.note.ZapRailIcon
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomSheet
import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.commons.ui.note.types.EditState
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog
@@ -229,7 +230,6 @@ import com.vitorpamplona.amethyst.model.zap.CashuRailStatus
import com.vitorpamplona.amethyst.model.zap.RailCapability
import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver
import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note.creators.polls
import android.text.format.DateFormat
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -45,7 +44,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
@@ -57,13 +55,14 @@ import com.vitorpamplona.amethyst.commons.resources.confirm
import com.vitorpamplona.amethyst.commons.resources.next
import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time
import com.vitorpamplona.amethyst.commons.resources.poll_closing_in
import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock
import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate
import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.quartz.utils.TimeUtils
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset
import com.vitorpamplona.quartz.utils.currentTimeMillis
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -72,26 +71,28 @@ fun PollDeadlinePicker(model: ShortNotePostViewModel) {
var showTimePicker by remember { mutableStateOf(false) }
// Get current time details
val currentTime = Instant.ofEpochMilli(model.closedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime()
// The deadline as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
val localSeconds = model.closedAt + LocalClock.utcOffsetSeconds(model.closedAt)
val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY)
val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year
val datePickerState =
rememberDatePickerState(
initialSelectedDateMillis = model.closedAt * 1000,
yearRange = currentTime.year..2050,
yearRange = currentYear..2050,
selectableDates =
object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
// Only allow today and future dates
return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer
return utcTimeMillis >= currentTimeMillis() - 86400000 // minus 24h buffer
}
},
)
val context = LocalContext.current
val timePickerState =
rememberTimePickerState(
initialHour = currentTime.hour,
initialMinute = currentTime.minute,
is24Hour = DateFormat.is24HourFormat(context),
initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
is24Hour = rememberIs24HourClock(),
)
OutlinedCard(
@@ -148,10 +149,8 @@ fun PollDeadlinePicker(model: ShortNotePostViewModel) {
(timePickerState.minute * TimeUtils.ONE_MINUTE)
} ?: TimeUtils.oneDayAhead()
// Get the offset from UTC for the current instant in the local time zone
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
model.closedAt = datetimeLocalTimeZone - offset.totalSeconds
// Subtract this zone's current offset from UTC
model.closedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
showTimePicker = false
},
@@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import com.vitorpamplona.amethyst.commons.audio.RecordingResult
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
@@ -45,13 +46,17 @@ import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
import com.vitorpamplona.amethyst.ui.note.creators.location.DEFAULT_MAP_ZOOM
import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
import com.vitorpamplona.quartz.podcasts.PodcastAudio
import kotlinx.collections.immutable.ImmutableList
import okio.Path
import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingRoomEvent as AppRenderMeetingRoomEvent
import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingRoomPresence as AppRenderMeetingRoomPresence
import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingSpaceEvent as AppRenderMeetingSpaceEvent
import com.vitorpamplona.amethyst.service.playback.composable.VideoView as AppVideoView
import com.vitorpamplona.amethyst.ui.actions.EditPostView as AppEditPostView
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton as AppRecordVoiceButton
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview as AppVoiceMessagePreview
import com.vitorpamplona.amethyst.ui.components.GifVideoView as AppGifVideoView
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview as AppLoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.ReusableZapButton as AppReusableZapButton
@@ -622,6 +627,22 @@ object AndroidNotePlatform : NotePlatform {
accountViewModel = accountViewModel,
)
@Composable
override fun RecordVoiceButton(
onVoiceTaken: (RecordingResult) -> Unit,
maxDurationSeconds: Int?,
) = AppRecordVoiceButton(onVoiceTaken, maxDurationSeconds)
@Composable
override fun VoiceMessagePreview(
voiceMetadata: AudioMeta,
localFile: Path?,
onRemove: () -> Unit,
onReRecord: ((RecordingResult) -> Unit)?,
isUploading: Boolean,
modifier: Modifier,
) = AppVoiceMessagePreview(voiceMetadata, localFile, onRemove, onReRecord, isUploading, modifier)
@Composable
override fun QuickZapAmountRow(
note: Note,
@@ -21,24 +21,20 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.failed_to_save_the_video
import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery
import com.vitorpamplona.amethyst.commons.service.isVideoOnline
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUploader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -56,19 +52,6 @@ fun AccountViewModel.urlPreview(
suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean = httpClientBuilder.isVideoOnline(videoUrl)
/**
* Encrypt + upload a picked image as a group avatar (canonical `marmot-group-image-v1` scheme).
* The returned handle is later passed to [AccountViewModel.updateMarmotGroupMetadata] as
* `MarmotGroupIconChange.Set` to commit it into the group's metadata. Uploading is separated from
* the metadata commit so the (slow) Blossom upload can show its own progress before the commit is
* signed.
*/
suspend fun AccountViewModel.uploadMarmotGroupIcon(
uri: Uri,
mimeType: String?,
uploader: MediaUploader,
): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader)
fun AccountViewModel.saveMediaToGallery(
videoUri: String?,
mimeType: String?,
@@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.RandomInstance
@@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -20,9 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -33,10 +30,13 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupImage
import com.vitorpamplona.amethyst.commons.resources.Res
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo
import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon
import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupIconUrl
@@ -73,11 +74,13 @@ fun MarmotGroupIconEditor(
onPick: (SelectedMedia) -> Unit,
onRemove: () -> Unit,
) {
val resolver = LocalContext.current.contentResolver
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) onPick(SelectedMedia(uri, resolver.getType(uri)))
var showPicker by remember { mutableStateOf(false) }
if (showPicker) {
GallerySelectSingle(imagesOnly = true) { media ->
showPicker = false
media?.let(onPick)
}
}
val model =
when {
@@ -100,7 +103,7 @@ fun MarmotGroupIconEditor(
Modifier
.size(96.dp)
.clip(CircleShape)
.let { if (enabled) it.clickable { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } else it },
.let { if (enabled) it.clickable { showPicker = true } else it },
loadProfilePicture = LocalDisplaySettings.current.showProfilePictures,
loadRobohash = LocalDisplaySettings.current.loadRobohash,
)
@@ -111,7 +114,7 @@ fun MarmotGroupIconEditor(
) {
TextButton(
enabled = enabled,
onClick = { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) },
onClick = { showPicker = true },
) {
Text(stringRes(if (hasIcon) Res.string.marmot_change_photo else Res.string.marmot_add_photo))
}
@@ -20,22 +20,21 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send
import android.net.Uri
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageCipher
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageEncryption
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.Log
import java.io.File
/**
* Encrypts a picked image with the MIP-01 v2 scheme (see [MarmotGroupImageEncryption])
@@ -50,7 +49,7 @@ class MarmotGroupIconUploader(
val account: Account,
) {
suspend fun upload(
uri: Uri,
uri: MediaUri,
mimeType: String?,
server: ServerName,
uploader: MediaUploader,
@@ -105,13 +104,7 @@ class MarmotGroupIconUploader(
} finally {
// Delete the intermediate compressed temp file (compress returns the original
// URI unchanged when it skips compression, so only delete a distinct temp).
if (compressed.uri != uri) {
try {
compressed.uri.path?.let { path -> File(path).takeIf { it.exists() }?.delete() }
} catch (e: Exception) {
Log.w("MarmotGroupIconUploader", "Failed to delete temp icon file", e)
}
}
if (compressed.uri != uri) uploader.discardTempFile(compressed.uri)
}
}
@@ -119,3 +112,15 @@ class MarmotGroupIconUploader(
private const val DEFAULT_MIME = "image/jpeg"
}
}
/**
* Encrypts and uploads a picked image as a group avatar (canonical `marmot-group-image-v1`
* scheme). The returned handle is later committed through
* [AccountViewModel.updateMarmotGroupMetadata] as `MarmotGroupIconChange.Set`; uploading is
* separate from that commit so the slow Blossom upload can show its own progress first.
*/
suspend fun AccountViewModel.uploadMarmotGroupIcon(
uri: MediaUri,
mimeType: String?,
uploader: MediaUploader,
): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader)
@@ -140,6 +140,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.commons.ui.note.platform.RecordVoiceButton
import com.vitorpamplona.amethyst.commons.ui.note.platform.VoiceMessagePreview
import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel
@@ -159,11 +162,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField
import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker
import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour
@@ -536,10 +536,9 @@ private fun NewPostScreenBody(
val alwaysOnEnabled by accountViewModel.account.settings.alwaysOnNotificationService
.collectAsStateWithLifecycle()
val savedAccounts by com.vitorpamplona.amethyst.LocalPreferences
.accountsFlow()
.collectAsStateWithLifecycle()
val hasMultipleAccounts = (savedAccounts?.size ?: 0) > 1
val savedAccounts by accountViewModel.host.savedAccounts
.collectAsStateWithLifecycle(emptySet())
val hasMultipleAccounts = savedAccounts.size > 1
postViewModel.scheduledForSec?.let { current ->
Row(
verticalAlignment = CenterVertically,
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.note.platform.VoiceMessagePreview
import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -32,9 +32,8 @@ import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
import com.vitorpamplona.amethyst.commons.service.isVideoOnline
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.checkVideoIsOnline
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
@@ -97,12 +96,12 @@ private suspend fun checkChannelIsOnline(
// Check if streaming URL is online, fall back to relay check
val streamingUrl = channel.info?.streaming()
if (!streamingUrl.isNullOrBlank()) {
accountViewModel.checkVideoIsOnline(streamingUrl)
accountViewModel.httpClientBuilder.isVideoOnline(streamingUrl)
} else {
// Check relay connection
val relayUrl = channel.relayHintUrl()
if (relayUrl != null) {
OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo)
accountViewModel.httpClientBuilder.isVideoOnline(relayUrl.url)
} else {
false
}
@@ -113,7 +112,7 @@ private suspend fun checkChannelIsOnline(
is EphemeralChatChannel -> {
// Check relay connection for ephemeral chat
val relayUrl = channel.roomId.relayUrl
OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo)
accountViewModel.httpClientBuilder.isVideoOnline(relayUrl.url)
}
else -> {
@@ -89,6 +89,9 @@ interface MediaUploader {
/** The file name the picker reported for [uri] (Android's `DISPLAY_NAME`), if any. */
fun displayName(uri: MediaUri): String? = uri.lastPathSegmentOrNull()
/** Deletes a temporary copy [compressIfNeeded] wrote, once the caller is done with it. */
fun discardTempFile(uri: MediaUri) = Unit
}
/** No media pipeline on this front end yet: every upload fails with a clear message. */
@@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import com.vitorpamplona.amethyst.commons.audio.RecordingResult
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
@@ -44,8 +45,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.EditState
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
import com.vitorpamplona.quartz.podcasts.PodcastAudio
import kotlinx.collections.immutable.ImmutableList
import okio.Path
/**
* The platform halves of the shared note renderer: what a note card needs that only the front end
@@ -439,6 +442,30 @@ interface NotePlatform {
nav: INav,
) {}
/**
* The microphone button: records a voice message (up to [maxDurationSeconds]) and hands the
* finished recording to [onVoiceTaken].
*/
@Composable
fun RecordVoiceButton(
onVoiceTaken: (RecordingResult) -> Unit,
maxDurationSeconds: Int?,
) {}
/**
* Plays back a voice message before it is sent: [localFile] while it is only on the device,
* else [voiceMetadata]'s URL. [onReRecord] replaces it with a new recording.
*/
@Composable
fun VoiceMessagePreview(
voiceMetadata: AudioMeta,
localFile: Path?,
onRemove: () -> Unit,
onReRecord: ((RecordingResult) -> Unit)?,
isUploading: Boolean,
modifier: Modifier,
) {}
/** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */
@Composable
fun EditPostView(
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import com.vitorpamplona.amethyst.commons.audio.RecordingResult
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
@@ -47,9 +48,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
import com.vitorpamplona.quartz.podcasts.PodcastAudio
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import okio.Path
// Call-site shims for [NotePlatform]: the same names and defaults the app's own composables have,
// so shared note code reads as it did and only its imports point here.
@@ -568,6 +571,25 @@ fun QuickZapAmountRow(
nav: INav,
) = LocalNotePlatform.current.QuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav)
/** The longest voice message the recorder allows, in seconds. */
const val MAX_VOICE_RECORD_SECONDS = 600
@Composable
fun RecordVoiceButton(
onVoiceTaken: (RecordingResult) -> Unit,
maxDurationSeconds: Int? = null,
) = LocalNotePlatform.current.RecordVoiceButton(onVoiceTaken, maxDurationSeconds)
@Composable
fun VoiceMessagePreview(
voiceMetadata: AudioMeta,
localFile: Path? = null,
onRemove: () -> Unit,
onReRecord: ((RecordingResult) -> Unit)? = null,
isUploading: Boolean = false,
modifier: Modifier = Modifier,
) = LocalNotePlatform.current.VoiceMessagePreview(voiceMetadata, localFile, onRemove, onReRecord, isUploading, modifier)
@Composable
fun EditPostView(
onClose: () -> Unit,