mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
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:
+8
@@ -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(
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+15
-16
@@ -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
|
||||
},
|
||||
|
||||
+21
@@ -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,
|
||||
|
||||
-17
@@ -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?,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+13
-10
@@ -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))
|
||||
}
|
||||
|
||||
+16
-11
@@ -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)
|
||||
|
||||
+6
-7
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+4
-5
@@ -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 -> {
|
||||
|
||||
+3
@@ -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. */
|
||||
|
||||
+27
@@ -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(
|
||||
|
||||
+22
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user