From a27acd9dc602eac26f940c47cb5f2e87de2f5b2f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 20 Aug 2025 21:00:09 -0400 Subject: [PATCH 01/48] YakBak in PictureInPicture mode New audio player UI for YakBak --- .../playback/composable/GetVideoController.kt | 18 +- .../playback/composable/RenderVideoPlayer.kt | 11 +- .../playback/composable/VideoViewInner.kt | 17 +- .../composable/mediaitem/GetMediaItem.kt | 5 + .../composable/mediaitem/MediaItemData.kt | 3 + .../service/playback/pip/IntentExtras.kt | 6 + .../service/playback/pip/PipVideoView.kt | 15 +- .../playback/playerPool/MediaSessionPool.kt | 10 +- .../playback/service/PlaybackService.kt | 7 +- .../playback/service/PlaybackServiceClient.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/types/VoiceTrack.kt | 207 ++++++++++++++++-- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 5 + amethyst/src/main/res/values/strings.xml | 2 + 15 files changed, 261 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt index f264bc9f53..e929a44c54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt @@ -67,10 +67,11 @@ fun GetVideoController( scope.launch { Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}") PlaybackServiceClient.prepareController( - controllerId, - mediaItem.src.videoUri, - mediaItem.src.proxyPort, - context, + mediaControllerState = controllerId, + videoUri = mediaItem.src.videoUri, + proxyPort = mediaItem.src.proxyPort, + keepPlaying = mediaItem.src.keepPlaying, + context = context, ) { controllerId -> scope.launch(Dispatchers.Main) { // checks if the player is still active after requesting to load @@ -174,10 +175,11 @@ fun GetVideoController( scope.launch(Dispatchers.Main) { Log.d("PlaybackService", "Preparing Video from Resume ${controllerId.id} ${mediaItem.src.videoUri} ") PlaybackServiceClient.prepareController( - controllerId, - mediaItem.src.videoUri, - mediaItem.src.proxyPort, - context, + mediaControllerState = controllerId, + videoUri = mediaItem.src.videoUri, + proxyPort = mediaItem.src.proxyPort, + keepPlaying = mediaItem.src.keepPlaying, + context = context, ) { controllerId -> scope.launch(Dispatchers.Main) { // checks if the player is still active after requesting to load diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt index 1a9e667062..bf5eb3450f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable import android.content.Context import android.view.View +import android.widget.FrameLayout import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable @@ -49,7 +50,6 @@ fun RenderVideoPlayer( thumbData: VideoThumb?, showControls: Boolean = true, contentScale: ContentScale, - waveform: WaveformData? = null, borderModifier: Modifier, videoModifier: Modifier, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, @@ -64,6 +64,13 @@ fun RenderVideoPlayer( factory = { context: Context -> PlayerView(context).apply { player = controllerState.controller + // if we alrady know the size of the frame, this forces the player to stay in the size + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS) setBackgroundColor(Color.Transparent.toArgb()) setShutterBackgroundColor(Color.Transparent.toArgb()) @@ -101,7 +108,7 @@ fun RenderVideoPlayer( }, ) - waveform?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) } + mediaItem.src.waveformData?.let { Waveform(it, controllerState, Modifier.align(Alignment.Center)) } if (showControls) { RenderControlButtons( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 03aca3d3d9..0614fef0d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -55,14 +55,16 @@ fun VideoViewInner( val muted = remember(videoUri) { DEFAULT_MUTED_SETTING.value } GetMediaItem( - videoUri, - title, - artworkUri, - authorName, - nostrUriCallback, - mimeType, - aspectRatio, + videoUri = videoUri, + title = title, + artworkUri = artworkUri, + authorName = authorName, + callbackUri = nostrUriCallback, + mimeType = mimeType, + aspectRatio = aspectRatio, proxyPort = accountViewModel.proxyPortFor(videoUri), + keepPlaying = true, + waveformData = waveform, ) { mediaItem -> GetVideoController( mediaItem = mediaItem, @@ -76,7 +78,6 @@ fun VideoViewInner( thumbData = thumb, showControls = showControls, contentScale = contentScale, - waveform = waveform, borderModifier = borderModifier, videoModifier = videoModifier, onControllerVisibilityChanged = onControllerVisibilityChanged, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt index ab10d943c0..b88f35ef41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.compose.produceCachedState +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData val mediaItemCache = MediaItemCache() @@ -37,6 +38,8 @@ fun GetMediaItem( mimeType: String? = null, aspectRatio: Float? = null, proxyPort: Int? = null, + keepPlaying: Boolean = false, + waveformData: WaveformData? = null, inner: @Composable (LoadedMediaItem) -> Unit, ) { val data = @@ -50,6 +53,8 @@ fun GetMediaItem( mimeType = mimeType, aspectRatio = aspectRatio, proxyPort = proxyPort, + keepPlaying = keepPlaying, + waveformData = waveformData, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt index d4b664ff20..8b3166db2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem import androidx.compose.runtime.Immutable import androidx.media3.common.MediaItem +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData @Immutable data class MediaItemData( @@ -33,6 +34,8 @@ data class MediaItemData( val mimeType: String? = null, val aspectRatio: Float? = null, val proxyPort: Int? = null, + val keepPlaying: Boolean = true, + val waveformData: WaveformData? = null, ) @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt index a4ec77431f..5470724558 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt @@ -20,8 +20,10 @@ */ package com.vitorpamplona.amethyst.service.playback.pip +import android.R.attr.data import android.graphics.Rect import android.os.Bundle +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.quartz.utils.ensure @@ -58,6 +60,8 @@ class IntentExtras { mimeType = intent.getString("mimeType"), aspectRatio = if (ratio > 0) ratio else null, proxyPort = if (port > 0) port else null, + keepPlaying = intent.getBoolean("keepPlaying", true), + waveformData = intent.getFloatArray("wavefrontData")?.toList()?.let { WaveformData(it) }, ) } @@ -74,6 +78,8 @@ class IntentExtras { data.mimeType?.let { putString("mimeType", it) } data.aspectRatio?.let { putFloat("aspectRatio", it) } data.proxyPort?.let { putInt("proxyPort", it) } + data.keepPlaying.let { putBoolean("keepPlaying", it) } + data.waveformData?.let { putFloatArray("wavefrontData", it.wave.toFloatArray()) } bounds?.let { putInt("boundLeft", it.left) } bounds?.let { putInt("boundRight", it.right) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt index 168e03897e..bc2df36be8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -42,8 +43,11 @@ import androidx.media3.ui.PlayerView import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.ui.components.getActivity +import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier @Composable fun PipVideo() { @@ -69,7 +73,7 @@ fun PipVideo() { videoData?.let { GetMediaItem(it) { mediaItem -> GetVideoController(mediaItem, false) { controller -> - PipVideo(controller) + PipVideo(controller, it.waveformData) } } } @@ -77,7 +81,10 @@ fun PipVideo() { @OptIn(UnstableApi::class) @Composable -fun PipVideo(controller: MediaControllerState) { +fun PipVideo( + controller: MediaControllerState, + waveformData: WaveformData?, +) { DisposableEffect(controller) { BackgroundMedia.switchKeepPlaying(controller) onDispose { @@ -117,5 +124,9 @@ fun PipVideo(controller: MediaControllerState) { } }, ) + + Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) { + waveformData?.let { Waveform(it, controller, Modifier) } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 48bfad6b34..b299a3acc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -56,7 +56,7 @@ class SessionListener( class MediaSessionPool( val exoPlayerPool: ExoPlayerPool, val okHttpClient: OkHttpClient, - val reset: (MediaSession) -> Unit, + val reset: (MediaSession, Boolean) -> Unit, ) { val globalCallback = MediaSessionCallback(this) var lastCleanup = TimeUtils.now() @@ -65,7 +65,7 @@ class MediaSessionPool( private val playingMap = mutableMapOf() private val cache = - object : LruCache(SimultaneousPlaybackCalculator.max()) { // up to 10 videos in the screen at the same time + object : LruCache(10) { // up to 10 videos in the screen at the same time override fun entryRemoved( evicted: Boolean, key: String?, @@ -87,6 +87,7 @@ class MediaSessionPool( @OptIn(UnstableApi::class) fun newSession( id: String, + keepPlaying: Boolean, context: Context, ): MediaSession { val mediaSession = @@ -107,7 +108,7 @@ class MediaSessionPool( mediaSession.player.addListener(listener) - reset(mediaSession) + reset(mediaSession, keepPlaying) cache.put(mediaSession.id, SessionListener(mediaSession, listener)) @@ -167,6 +168,7 @@ class MediaSessionPool( fun getSession( id: String, + keepPlaying: Boolean, context: Context, ): MediaSession { val existingSession = playingMap.get(id) ?: cache.get(id) @@ -174,7 +176,7 @@ class MediaSessionPool( return existingSession.session } - return newSession(id, context) + return newSession(id, keepPlaying, context) } fun playingContent() = playingMap.values diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index 0232d843c4..eb1c693f86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -44,9 +44,9 @@ class PlaybackService : MediaSessionService() { MediaSessionPool( ExoPlayerPool(ExoPlayerBuilder(okHttp)), okHttpClient = okHttp, - reset = { session -> + reset = { session, keepPlaying -> (session.player as ExoPlayer).apply { - repeatMode = Player.REPEAT_MODE_ONE + repeatMode = if (keepPlaying) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT volume = 0f } @@ -145,7 +145,8 @@ class PlaybackService : MediaSessionService() { override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { val id = controllerInfo.connectionHints.getString("id") ?: return null val proxyPort = controllerInfo.connectionHints.getInt("proxyPort") + val keepPlaying = controllerInfo.connectionHints.getBoolean("keepPlaying", true) val manager = lazyPool(proxyPort) - return manager.getSession(id, applicationContext) + return manager.getSession(id, keepPlaying, applicationContext) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt index 1018b3813c..1bd5bf6dd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackServiceClient.kt @@ -60,6 +60,7 @@ object PlaybackServiceClient { mediaControllerState: MediaControllerState, videoUri: String, proxyPort: Int? = 0, + keepPlaying: Boolean = true, context: Context, onReady: (MediaControllerState) -> Unit, ) { @@ -73,6 +74,7 @@ object PlaybackServiceClient { // link the id with the client's id to make sure it can return the // same session on background media. putString("id", mediaControllerState.id) + putBoolean("keepPlaying", keepPlaying) proxyPort?.let { putInt("proxyPort", it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 1a99a0cb0b..b5c921998a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -798,7 +798,7 @@ private fun RenderNoteRow( is VideoNormalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is VideoShortEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is PictureEvent -> PictureDisplay(baseNote, true, ContentScale.FillWidth, PaddingValues(vertical = 5.dp), backgroundColor, accountViewModel, nav) - is BaseVoiceEvent -> RenderVoiceTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) + is BaseVoiceEvent -> RenderVoiceTrack(baseNote, accountViewModel, nav) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) is CommunityPostApprovalEvent -> { RenderPostApproval( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index a0ace2304a..e77a7734f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -20,22 +20,56 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.OptIn +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PauseCircleOutline +import androidx.compose.material.icons.filled.PlayCircleOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.layout.ContentScale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.PlayerView +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController +import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.amethyst.service.playback.composable.controls.RenderControlButtons +import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem +import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform +import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia.isPlaying import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp +import com.vitorpamplona.amethyst.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.ui.theme.Size75Modifier +import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier +import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @@ -43,23 +77,28 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @Composable fun RenderVoiceTrack( note: Note, - contentScale: ContentScale, accountViewModel: AccountViewModel, nav: INav, ) { val noteEvent = note.event as? BaseVoiceEvent ?: return - VoiceHeader(noteEvent, note, contentScale, accountViewModel, nav) + VoiceHeader(noteEvent, note, accountViewModel, nav) } @Composable fun VoiceHeader( noteEvent: BaseVoiceEvent, note: Note, - contentScale: ContentScale, accountViewModel: AccountViewModel, nav: INav, ) { + val media = + remember(noteEvent) { + noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url + } + + if (media == null) return + val waveform = remember(noteEvent) { noteEvent @@ -68,32 +107,40 @@ fun VoiceHeader( ?.waveform ?.let { WaveformData(it) } } - val media = - remember(noteEvent) { - noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url - } - if (media == null) return + val callbackUri = remember(note) { note.toNostrUri() } - Column(modifier = Modifier.fillMaxWidth().padding(top = 5.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { Row( verticalAlignment = Alignment.CenterVertically, ) { - VideoView( + GetMediaItem( videoUri = media, - mimeType = null, - waveform = waveform, title = noteEvent.subject(), + artworkUri = null, authorName = note.author?.toBestDisplayName(), - roundedCorner = true, - contentScale = contentScale, - accountViewModel = accountViewModel, - nostrUriCallback = note.toNostrUri(), - ) + callbackUri = callbackUri, + mimeType = null, + aspectRatio = null, + proxyPort = accountViewModel.proxyPortFor(media), + keepPlaying = false, + waveformData = waveform, + ) { mediaItem -> + GetVideoController( + mediaItem = mediaItem, + muted = false, + ) { controller -> + RenderVoicePlayer( + mediaItem = mediaItem, + controllerState = controller, + waveform = waveform, + borderModifier = MaterialTheme.colorScheme.imageModifier, + accountViewModel = accountViewModel, + ) + } + } } - val callbackUri = remember(note) { note.toNostrUri() } - if (noteEvent.hasHashtags()) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) @@ -101,3 +148,119 @@ fun VoiceHeader( } } } + +@Composable +@OptIn(UnstableApi::class) +fun RenderVoicePlayer( + mediaItem: LoadedMediaItem, + controllerState: MediaControllerState, + waveform: WaveformData? = null, + borderModifier: Modifier, + accountViewModel: AccountViewModel, +) { + val controllerVisible = remember(controllerState) { mutableStateOf(false) } + + Box(modifier = borderModifier) { + AndroidView( + modifier = + Modifier + .fillMaxWidth() + .height(100.dp), + factory = { context: Context -> + PlayerView(context).apply { + player = controllerState.controller + // if we alrady know the size of the frame, this forces the player to stay in the size + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + + setBackgroundColor(Color.Transparent.toArgb()) + setShutterBackgroundColor(Color.Transparent.toArgb()) + + controllerAutoShow = false + useController = true + hideController() + + setControllerVisibilityListener( + PlayerView.ControllerVisibilityListener { visible -> + controllerVisible.value = visible == View.VISIBLE + }, + ) + } + }, + ) + + Row(VoiceHeightModifier, verticalAlignment = Alignment.CenterVertically) { + PlayPauseButton(controllerState) + waveform?.let { Waveform(it, controllerState, Modifier) } + } + + RenderControlButtons( + mediaItem.src, + controllerState, + controllerVisible, + Modifier.align(Alignment.TopEnd), + accountViewModel, + ) + } +} + +@Composable +fun PlayPauseButton(controllerState: MediaControllerState) { + val controller = controllerState.controller + var isPlaying by remember(controller) { + mutableStateOf(controllerState.isPlaying()) + } + + if (controller != null) { + val view = LocalView.current + // Keeps the screen on while playing and viewing videos. + DisposableEffect(key1 = controller, key2 = view) { + val listener = + object : Player.Listener { + override fun onIsPlayingChanged(playing: Boolean) { + // doesn't consider the mutex because the screen can turn off if the video + // being played in the mutex is not visible. + if (view.keepScreenOn != playing) { + view.keepScreenOn = playing + } + isPlaying = playing + } + + fun destroy() { + if (view.keepScreenOn) { + view.keepScreenOn = false + } + isPlaying = false + } + } + + controller.addListener(listener) + onDispose { + controller.removeListener(listener) + listener.destroy() + } + } + + // Play/Pause Button + IconButton( + onClick = { + if (controller.isPlaying) { + controller.pause() + } else { + controller.play() + } + }, + modifier = Size75Modifier, + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.PauseCircleOutline else Icons.Default.PlayCircleOutline, + contentDescription = if (isPlaying) stringResource(R.string.pause) else stringResource(R.string.play), + modifier = Size50Modifier, + tint = Color.White, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index d03940fff4..d4d89fd7c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -552,7 +552,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is PictureEvent) { PictureDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, PaddingValues(vertical = Size5dp), backgroundColor, accountViewModel = accountViewModel, nav) } else if (noteEvent is BaseVoiceEvent) { - VoiceHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav) + VoiceHeader(noteEvent, baseNote, accountViewModel, nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 71568c03c8..affd665ebb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -165,6 +165,7 @@ val Size39Modifier = Modifier.size(39.dp) val Size40Modifier = Modifier.size(40.dp) val Size50Modifier = Modifier.size(50.dp) val Size55Modifier = Modifier.size(55.dp) +val Size75Modifier = Modifier.size(75.dp) val TinyBorders = Modifier.padding(2.dp) val NoSoTinyBorders = Modifier.padding(start = 5.dp, end = 5.dp, top = 2.dp, bottom = 2.dp) @@ -351,3 +352,7 @@ val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp) val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20)) + +val MaxWidthPaddingTop5dp = Modifier.fillMaxWidth().padding(top = 5.dp) + +val VoiceHeightModifier = Modifier.fillMaxWidth().height(100.dp) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index fb6d27b6fb..671fd077ff 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1229,4 +1229,6 @@ Don\'t Translate From Languages shown here will not be translated. Select a language to remove it and have it translated again. + Pause + Play From e1a5b9c07932239b0795ef4e848e656f5559c313 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 20 Aug 2025 21:26:59 -0400 Subject: [PATCH 02/48] Waits for the player to load and sets the current position of the audio --- .../service/playback/composable/wavefront/Waveform.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index c0ef0c8659..2b0c6bd472 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -82,6 +82,12 @@ fun Waveform( pollCurrentDuration(it).collect { value -> waveformProgress.floatValue = value } } } + + LaunchedEffect(Unit) { + delay(500) + val position = mediaControllerState.controller?.let { it.currentPosition / it.duration.toFloat() } ?: 0f + waveformProgress.floatValue = position + } } private fun pollCurrentDuration(controller: MediaController) = From 4e13c5ab9cc2f9320b419b856585a12f424402e2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 08:49:25 -0400 Subject: [PATCH 03/48] Improves timeouts to download and upload files --- .../amethyst/service/okhttp/OkHttpClientFactory.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index bf87bdbcbb..9489f8a065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -73,13 +73,12 @@ class OkHttpClientFactory( userAgent: String, ): OkHttpClient { val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds - val duration = Duration.ofSeconds(seconds.toLong()) return rootClient .newBuilder() .proxy(proxy) - .readTimeout(duration) - .connectTimeout(duration) - .writeTimeout(duration) + .connectTimeout(Duration.ofSeconds(seconds.toLong())) + .readTimeout(Duration.ofSeconds(seconds.toLong() * 3)) + .writeTimeout(Duration.ofSeconds(seconds.toLong() * 3)) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) .addNetworkInterceptor(logging) .addNetworkInterceptor(keyDecryptor) From fbb50f414723f25a526063ffa2913e223867f224 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 11:08:23 -0400 Subject: [PATCH 04/48] version 1.0.0 --- amethyst/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 82e5ae67be..4f167aa9a8 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,9 +44,9 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 418 - versionName = generateVersionName("0.94.3") - buildConfigField "String", "RELEASE_NOTES_ID", "\"fd42b23b9ef792059b1c1a89555443abbb11578f4b3c8430b452559eec7325f3\"" + versionCode = 419 + versionName = generateVersionName("1.00.0") + buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From bcdf6e5ff798b96325a9913ed6123c0c99ed99d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 12:17:31 -0400 Subject: [PATCH 05/48] Sends only play files to the zap store. --- zapstore.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/zapstore.yaml b/zapstore.yaml index 47dabbb2ef..fb1e369983 100644 --- a/zapstore.yaml +++ b/zapstore.yaml @@ -6,11 +6,6 @@ amethyst: builder: npub142gywvjkq0dv6nupggyn2euhx4nduwc7yz5f24ah9rpmunr2s39se3xrj0 repository: https://github.com/vitorpamplona/amethyst artifacts: - - amethyst-fdroid-arm64-v8a-v%v.apk - - amethyst-fdroid-armeabi-v7a-v%v.apk - - amethyst-fdroid-universal-v%v.apk - - amethyst-fdroid-x86-v%v.apk - - amethyst-fdroid-x86_64-v%v.apk - amethyst-googleplay-arm64-v8a-v%v.apk - amethyst-googleplay-armeabi-v7a-v%v.apk - amethyst-googleplay-universal-v%v.apk @@ -19,3 +14,4 @@ amethyst: + From 1d1ee8670e197ade4897a171fe98b8ef3f696998 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 12:20:16 -0400 Subject: [PATCH 06/48] Fixes app name --- .../vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt | 4 ++-- amethyst/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 907f5bf9a5..ee6102a1e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -536,7 +536,7 @@ fun authenticate( fun keyguardPrompt() { val intent = keyguardManager.createConfirmDeviceCredentialIntent( - stringRes(context, R.string.app_name_release), + stringRes(context, R.string.app_name), title, ) @@ -556,7 +556,7 @@ fun authenticate( val promptInfo = BiometricPrompt.PromptInfo .Builder() - .setTitle(stringRes(context, R.string.app_name_release)) + .setTitle(stringRes(context, R.string.app_name)) .setSubtitle(title) .setAllowedAuthenticators(authenticators) .build() diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 671fd077ff..e89766e8e1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Amethyst + Amethyst Amy Debug Amy Benchmark Point to the QR Code From b699b30954e8d1e976ef459603474586c5f7086e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 12:21:48 -0400 Subject: [PATCH 07/48] Version 1.00.1 to fix app name --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 4f167aa9a8..fedc6208e4 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 419 - versionName = generateVersionName("1.00.0") + versionCode = 420 + versionName = generateVersionName("1.00.1") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From a33ab5c1e9343502f104a2346bca8b5a26dd4a65 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 21 Aug 2025 16:22:29 +0000 Subject: [PATCH 08/48] New Crowdin translations by GitHub Action --- .../src/main/res/values-fr-rFR/strings.xml | 74 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 2 + .../src/main/res/values-zh-rCN/strings.xml | 2 + 3 files changed, 78 insertions(+) diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index 9487c1c913..f4a15147b9 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -51,6 +51,13 @@ Connectez-vous avec une clé privée pour ne plus Suivre Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir masquer un mot ou une phrase Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir afficher un mot ou une phrase + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir modifier les paramètres + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir téléverser + Vous utilisez une clé publique et les clés publiques sont en lecture seule. Connectez-vous avec une clé privée pour pouvoir signer des événements + Déchiffrement non autorisé + Le signataire n\'a pas autorisé le décryptage nécessaire pour effectuer cette opération. Activez les décryptions NIP-44 dans votre application signataire et réessayez + Signataire introuvable + L\'application de signature a-t-elle été désinstallée ? Vérifiez si le signataire est installé et a ce compte. Déconnectez-vous et reconnectez-vous de l\'application signataire a changé. Zaps Nombre de vues Boost @@ -71,6 +78,8 @@ Erreur d\'analyse du message d\'erreur " Abonnements" " Abonnés" + "%1$s abonnements" + "%1$s abonnés" Profil Filtres de sécurité Déconnexion @@ -96,6 +105,7 @@ Mon groupe spectaculaire Url de l\'image Description + Description introuvable "à propos…" Quoi de neuf ? Rédiger un message… @@ -137,6 +147,10 @@ Vidéo enregistrée dans la galerie vidéo du téléphone Impossible d\'enregistrer la vidéo Charger une image + Prendre une photo + Enregistrer un message + Enregistrer un message + Cliquez et maintenez pour enregistrer un message Chargement… L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "répondre ici" @@ -155,6 +169,7 @@ Galerie "Suivis" "Signalements" + "%1$s rapports" Plus d\'options " Relais" Site Web @@ -213,6 +228,13 @@ Ne plus suivre Canal créé "Les informations du canal ont été remplacées par" + Discussion éphémère + Relais de discussion + Relais de discussions + Les relais de discussions sont des groupes de discussion contrôlés par leur relais d\'origine. + Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. + Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères + et, par conséquent, les messages de discussion disparaissent avec le temps Chat public Métadonnées de tchat public Les conversations publiques sont visibles pour tout le monde sur Nostr et n\'importe qui @@ -221,6 +243,8 @@ Relais Insérez entre 1 et 3 relais qui hébergent ce groupe. Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer. + Relais payant + Force Tor lors de la connexion messages reçus Retirer Auto @@ -265,6 +289,7 @@ Erreur "Créé par %1$s" "Image de badge pour %1$s" + Image du badge de récompense Vous avez reçu un nouveau Badge Le Badge a été décerné à Copie du contenu de la note dans le presse-papiers @@ -290,6 +315,7 @@ Ne plus suivre Suivre Supprimer de la galerie + Supprimer ce média de votre galerie. Demande de Suppression Amethyst demandera que votre note soit supprimée des relais auxquels vous êtes actuellement connecté. Il n\'y a aucune garantie que votre note sera définitivement supprimée de ces relais, ou d\'autres relais où elle peut être stockée. Bloquer @@ -411,6 +437,7 @@ Non Liste des Abonnés Tous les abonnements + Suivre via Proxy Autour de moi Général Liste en Sourdine @@ -485,9 +512,18 @@ Toujours cacher les contenus sensibles Toujours afficher les contenus sensibles Toujours afficher les avertissements de contenu + Masquer + Afficher + Avertir Recommendations: Filtrer le spam des inconnus Avertir lorsque les messages ont été rapportés par vos abonnés + Filtrer les spams + Masque les messages d\'inconnus qui étaient exactement les mêmes depuis 5 fois ou plus + Avertir sur les rapports + Affiche un message d\'avertissement lorsque des messages ont été rapportés 5 fois ou plus par vos abonnements + Afficher le contenu sensible + Affiche un message d\'avertissement lorsque l\'auteur du message l\'a marqué comme sensible Nouveau symbole de réaction Aucun type de réaction sélectionné. Appuyer longuement pour changer Collecte de Zaps @@ -539,7 +575,12 @@ Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ? Tags suivis Relais + Packs à suivre + Lectures + Algorithmes de flux Place de Marché + Diffusions en direct + Communautés Salons Messages approuvés Ce groupe n\'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une @@ -547,6 +588,7 @@ Contenu sensible Ajouter un avertissement de contenu sensible avant de montrer ce contenu Préférences de l\'application + Préférences utilisateur Paramètres Toujours Wifi uniquement @@ -591,6 +633,8 @@ Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l\'emplacement actuel Post exclusif à la localisation Seuls les abonnés à cette localisation le verront. Vos abonnés généraux ne le verront pas. + Message à hashtag-exclusif + Seuls les abonnés du hashtag le verront. Vos abonnés généraux ne le verront pas. Chargement de la localisation Pas d\'autorisations de localisation Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C\'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant @@ -599,6 +643,7 @@ Activer Public Nouveau Groupe Public ou Privé + Relais Privé À Sujet @@ -664,6 +709,7 @@ Portefeuille %1$s Erreur lors de l\'ouverture de l\'application de signature L\'application signataire est introuvable. Vérifiez que l\'application n\'a pas été désinstallée + Requête de signature rejetée Demande de signature rejetée Assurez-vous que l\'application signataire a autorisé cette transaction Aucun portefeuille trouvé pour payer une facture lightning (Erreur : %1$s). Veuillez installer un portefeuille Lightning pour utiliser les zaps @@ -791,6 +837,8 @@ Nouveau Message Nouveaux Shorts : images ou vidéos Nouvelle Note Communautaire + Nouveau produit + Nouveau message géo-exclusif Ouvrir toutes les réactions à ce message Fermer toutes les réactions à ce message Répondre @@ -845,12 +893,29 @@ Insérez entre 1 et 3 relais pour stocker des événements que personne d\'autre ne peut voir, comme vos Brouillons et/ou paramètres d\'application. Idéalement, ces relais sont soit locaux soit nécessitent une authentification avant de télécharger le contenu de chaque utilisateur. Relais généraux Amethyst utilise ces relais pour télécharger des messages pour vous. + Relais connectés + Liste actuelle des relais utilisés Relais recommandés Ajoutez les relais suivants à votre liste de relais généraux afin de recevoir les messages des utilisateurs listés. Relais de recherche Liste des relais à utiliser pour la recherche et l\'étiquetage des utilisateurs. L\'étiquetage et la recherche ne fonctionneront pas si aucune option n\'est disponible. Relais locaux Liste des relais qui sont en cours d\'exécution sur cet appareil. + Relais de confiance + Relais de confiance + Relais de confiance ne nécessitant pas une connexion Tor pour + Relais Proxy + Relais Proxy + Relais agrégateurs que l\'application doit utiliser pour télécharger vos flux, tels que filter.nostr.wine. Cela remplace le modèle de boîte d\'envoi et permettra à l\'application de se connecter uniquement aux relais de vos listes. + Relais de diffusion + Relais de diffusion + Relais qui se spécialisent dans la publication de vos notes à tous les autres relais, comme sendit.nosflare.com. Amethyst ajoutera ce relais à tous vos nouveaux événements + Relais indexeur + Relais indexeur + Relais qui se spécialisent dans l\'hébergement des métadonnées et des listes de relais de chacun, comme purplepag.es. Amethyst utilisera ces relais pour trouver des utilisateurs qui ne sont pas dans vos listes. + Relais bloqués + Relais bloqués + Amethyst ne se connectera jamais à ces relais Zap les Devs ! Votre don nous aide à faire la différence. Chaque sat compte ! Faire un don maintenant @@ -940,4 +1005,13 @@ Sélectionnez une liste pour filtrer le fil Se déconnecter au verrouillage de l\'appareil Message privé + Message public + Relais de chat + Le relais auquel tous les utilisateurs de ce chat se connectent + Partager l\'image… + Hashtag de recherche : #%1$s + Ne pas traduire depuis + Les langues affichées ici ne seront pas traduites. Sélectionnez une langue pour la supprimer et l\'avoir traduite à nouveau. + Pause + Lecture diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 88760ed372..19efe1c365 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1012,4 +1012,6 @@ विषयसूचक खोज : #%1$s अनुवाद ना करें यहाँ प्रस्तुत भाषाओं का अनुवाद नहीं होगा। भाषा चयन करें हटाने के लिए जिससे उसका अनुवाद पुनः होने लगेगा। + विराम + चलाएँ diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 2ff9f4e653..6744461652 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -1012,4 +1012,6 @@ 搜索话题标签:#%1$s 不要翻译自 此处显示的语言不会被翻译。请选择一种语言来移除它并重新翻译它。 + 暂停 + 播放 From 704a08e7bb8434f9996912c0b13bb74778fe8658 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 13:51:10 -0400 Subject: [PATCH 09/48] Fixes follow/unfollow from hashtags and geohashes --- .../reqCommand/user/UserObservers.kt | 41 ++++++++----------- .../screen/loggedIn/geohash/GeoHashScreen.kt | 2 +- .../screen/loggedIn/hashtag/HashtagScreen.kt | 2 +- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 6b354e41da..812fc9e84a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -429,56 +429,47 @@ fun observeUserIsFollowing( return flow.collectAsStateWithLifecycle(user1.isFollowing(user2)) } +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable fun observeUserIsFollowingHashtag( - user: User, hashtag: String, accountViewModel: AccountViewModel, ): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - // Subscribe in the LocalCache for changes that arrive in the device val flow = - remember(user) { - user - .flow() - .follows.stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.isFollowingHashtag(hashtag) + remember(accountViewModel) { + accountViewModel.account.hashtagList.flow + .mapLatest { hashtags -> + hashtag in hashtags + }.onStart { + emit(hashtag in accountViewModel.account.hashtagList.flow.value) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } - return flow.collectAsStateWithLifecycle(user.isFollowingHashtag(hashtag)) + return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.hashtagList.flow.value) } +@SuppressLint("StateFlowValueCalledInComposition") @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable fun observeUserIsFollowingGeohash( - user: User, geohash: String, accountViewModel: AccountViewModel, ): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - // Subscribe in the LocalCache for changes that arrive in the device val flow = - remember(user) { - user - .flow() - .follows.stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.isFollowingGeohash(geohash) + remember(accountViewModel) { + accountViewModel.account.geohashList.flow + .mapLatest { geohashes -> + geohash in geohashes + }.onStart { + emit(geohash in accountViewModel.account.geohashList.flow.value) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } - return flow.collectAsStateWithLifecycle(user.isFollowingGeohash(geohash)) + return flow.collectAsStateWithLifecycle(geohash in accountViewModel.account.geohashList.flow.value) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 443f99376e..cb623710bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -132,7 +132,7 @@ fun GeoHashActionOptions( tag: String, accountViewModel: AccountViewModel, ) { - val isFollowingTag by observeUserIsFollowingGeohash(accountViewModel.userProfile(), tag, accountViewModel) + val isFollowingTag by observeUserIsFollowingGeohash(tag, accountViewModel) if (isFollowingTag) { UnfollowButton { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index d8fbb9dc2c..f6c07d8576 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -145,7 +145,7 @@ fun HashtagActionOptions( tag: String, accountViewModel: AccountViewModel, ) { - val isFollowingTag by observeUserIsFollowingHashtag(accountViewModel.userProfile(), tag, accountViewModel) + val isFollowingTag by observeUserIsFollowingHashtag(tag, accountViewModel) if (isFollowingTag) { UnfollowButton { From 1ef9902ece5887b3244a355999d4d7dd98e063d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 13:51:33 -0400 Subject: [PATCH 10/48] Migrates top nav list to hashtag, geohash and community lists --- .../amethyst/ui/screen/FollowListState.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index a563316d3c..9f7d4becb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -66,7 +66,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest @Stable class FollowListState( @@ -160,12 +159,12 @@ class FollowListState( @OptIn(ExperimentalCoroutinesApi::class) val liveKind3FollowsFlow: Flow> = - account.kind3FollowList.flow.transformLatest { + combineTransform(account.kind3FollowList.flow, account.hashtagList.flow, account.geohashList.flow, account.communityList.flow) { kind3List, hashtagList, geotagList, communityList -> checkNotInMainThread() val communities = - it.communities.mapNotNull { - LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote -> + communityList.mapNotNull { + LocalCache.getOrCreateAddressableNote(it.address).let { communityNote -> TagFeedDefinition( "Community/${communityNote.idHex}", CommunityName(communityNote), @@ -178,7 +177,7 @@ class FollowListState( } val hashtags = - it.hashtags.map { + hashtagList.map { TagFeedDefinition( "Hashtag/$it", HashtagName(it), @@ -190,7 +189,7 @@ class FollowListState( } val geotags = - it.geotags.map { + geotagList.map { TagFeedDefinition( "Geohash/$it", GeoHashName(it), From 816de5bf031ee18911e70992a3d1f001471419e3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 14:06:47 -0400 Subject: [PATCH 11/48] Fixes community join/leave issue --- .../SingleCommunityTopNavPerRelayFilter.kt | 5 ++++- .../amethyst/ui/note/types/CommunityHeader.kt | 20 +++++------------- .../FilterCommunitiesByAllCommunities.kt | 2 +- .../FilterCommunitiesByCommunity.kt | 21 +++++++++++-------- .../subassemblies/FilterCommunitiesGlobal.kt | 5 ++--- 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt index 596af8a4c5..508678a251 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -22,9 +22,12 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @Immutable class SingleCommunityTopNavPerRelayFilter( val community: String, val authors: Set?, -) : IFeedTopNavPerRelayFilter +) : IFeedTopNavPerRelayFilter { + val communityAddress = Address.parse(community) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 60346dd4a1..6278738351 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -54,7 +54,6 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture @@ -87,8 +86,6 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.util.Locale @Composable @@ -362,10 +359,10 @@ fun WatchAddressableNoteFollows( accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit, ) { - val state by accountViewModel.account.kind3FollowList.flow + val state by accountViewModel.account.communityList.flowSet .collectAsStateWithLifecycle() - onFollowChanges(state.communities.contains(note.idHex)) + onFollowChanges(state.contains(note.idHex)) } @Composable @@ -376,16 +373,9 @@ fun JoinCommunityButton( ) { Button( modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.follow(note) - } - }, + onClick = { accountViewModel.follow(note) }, shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), contentPadding = ButtonPadding, ) { Text(text = stringRes(R.string.join), color = Color.White) @@ -402,7 +392,7 @@ fun LeaveCommunityButton( Button( modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.unfollow(note) } }, + onClick = { accountViewModel.unfollow(note) }, shape = ButtonBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index 7c2112abf1..b6157d74c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -42,7 +42,7 @@ fun filterCommunitiesAllCommunities( filter = Filter( kinds = CommunityPostApprovalEvent.KIND_LIST, - ids = communityList, + tags = mapOf("a" to communityList), limit = 300, since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index fe3872fd80..40d6bf4575 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -25,11 +25,12 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent fun filterClassifiedsByCommunity( relay: NormalizedRelayUrl, - community: String, + community: Address, authors: Set?, since: Long? = null, ): List { @@ -40,9 +41,9 @@ fun filterClassifiedsByCommunity( relay = relay, filter = Filter( - authors = authors, + authors = listOf(community.pubKeyHex), kinds = listOf(CommunityDefinitionEvent.KIND), - ids = listOf(community), + tags = mapOf("d" to listOf(community.dTag)), limit = 300, since = since, ), @@ -59,11 +60,13 @@ fun filterCommunitiesByCommunity( return communitySet.set .mapNotNull { - filterClassifiedsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) + it.value.communityAddress?.let { address -> + filterClassifiedsByCommunity( + relay = it.key, + community = address, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 6798e81f6b..334aeb33fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils fun filterCommunitiesGlobal( @@ -42,7 +41,7 @@ fun filterCommunitiesGlobal( relay = it.key, filter = Filter( - kinds = listOf(CommunityDefinitionEvent.KIND), + kinds = CommunityPostApprovalEvent.KIND_LIST, limit = 100, since = since, ), @@ -51,7 +50,7 @@ fun filterCommunitiesGlobal( relay = it.key, filter = Filter( - kinds = listOf(CommunityPostApprovalEvent.KIND), + kinds = CommunityPostApprovalEvent.KIND_LIST, limit = 100, since = since ?: TimeUtils.oneWeekAgo(), ), From fd26ae5543f6a7f2aea98fb4e6848d0cf90ceb66 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 14:11:56 -0400 Subject: [PATCH 12/48] Fixes branch name --- amethyst/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index fedc6208e4..98340f6305 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -21,7 +21,7 @@ def getCurrentBranch() { def generateVersionName(String baseVersion) { def currentBranch = getCurrentBranch() - if (currentBranch == "main" || currentBranch == "master") { + if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown") { return baseVersion } else { // Clean branch name for version (replace special characters) From 4a7a09c43a37e0fa3a30039f0653d278e92e5415 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 14:20:55 -0400 Subject: [PATCH 13/48] Tries to avoid crash when lacking google services in the play version. --- .../notifications/PushNotificationUtils.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index be36c33f9d..3973f07587 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.notifications +import android.util.Log import com.google.firebase.messaging.FirebaseMessaging import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.service.retryIfException @@ -35,12 +36,16 @@ object PushNotificationUtils { accounts: List, okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { - val token = FirebaseMessaging.getInstance().token.await() - if (hasInit?.equals(accounts) == true && lastToken == token) { - return@with - } + try { + val token = FirebaseMessaging.getInstance().token.await() + if (hasInit?.equals(accounts) == true && lastToken == token) { + return@with + } - registerToken(token, accounts, okHttpClient) + registerToken(token, accounts, okHttpClient) + } catch (e: Exception) { + Log.e("PushNotificationUtils", "Failed to get Firebase token", e) + } } suspend fun checkAndInit( From fe141d1ee356c357c03d19582176a3636a905ab2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 14:29:34 -0400 Subject: [PATCH 14/48] Fixes crash when typing a new post without permissions to do a draft post. --- .../com/vitorpamplona/amethyst/model/Account.kt | 4 ++++ .../ui/note/nip22Comments/CommentPostViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 ---- .../privateDM/send/ChatNewMessageViewModel.kt | 6 ++++-- .../send/ChannelNewMessageViewModel.kt | 8 +++++--- .../nip99Classifieds/NewProductViewModel.kt | 15 +++++++++------ .../loggedIn/home/ShortNotePostViewModel.kt | 6 ++++-- .../publicMessages/NewPublicMessageViewModel.kt | 6 ++++-- 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b77838951f..e955381987 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1145,6 +1145,8 @@ class Account( template: EventTemplate, broadcast: Set = emptySet(), ) { + if (!isWriteable()) return + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) @@ -1163,6 +1165,8 @@ class Account( } suspend fun deleteDraft(draftTag: String) { + if (!isWriteable()) return + val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 7bfefdeca2..d9016fb111 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -317,7 +317,7 @@ open class CommentPostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.account.deleteDraft(version) } suspend fun sendDraftSync() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fa3fa2a02b..272ae9f474 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1506,10 +1506,6 @@ class AccountViewModel( fun dataSources() = app.sources - suspend fun deleteDraft(draftTag: String) { - account.deleteDraft(draftTag) - } - suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent) fun createTempDraftNote( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index ec6dd17374..dcb0205167 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -112,7 +112,9 @@ class ChatNewMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -349,7 +351,7 @@ class ChatNewMessageViewModel : val version = draftTag.current innerSendPost(null) cancel() - accountViewModel.deleteDraft(version) + accountViewModel.account.deleteDraft(version) } suspend fun sendDraftSync() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 61fb907dd1..71499c5fa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -106,7 +106,9 @@ open class ChannelNewMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -260,7 +262,7 @@ open class ChannelNewMessageViewModel : } fun sendPost(onDone: suspend () -> Unit) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { sendPostSync() onDone() } @@ -274,7 +276,7 @@ open class ChannelNewMessageViewModel : cancel() accountViewModel.account.signAndSendPrivately(template, channelRelays) - accountViewModel.deleteDraft(version) + accountViewModel.account.deleteDraft(version) } suspend fun sendDraftSync() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 8cb8ed9707..d55c8fe797 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -104,20 +104,22 @@ open class NewProductViewModel : IZapRaiser { val draftTag = DraftTagState() + var accountViewModel: AccountViewModel? = null + var account: Account? = null + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel?.runIOCatching { + sendDraftSync() + } } } } } - var accountViewModel: AccountViewModel? = null - var account: Account? = null - var productImages by mutableStateOf>(emptyList()) val iMetaDescription = IMetaAttachments() @@ -288,14 +290,15 @@ open class NewProductViewModel : } suspend fun sendPostSync() { + val accountViewModel = accountViewModel ?: return val template = createTemplate() ?: return - accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( + accountViewModel.account.signAndSendPrivatelyOrBroadcast( template, relayList = { relayList }, ) - accountViewModel?.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraft(draftTag.current) cancel() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 4d38c1aa50..387bf43515 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -146,7 +146,9 @@ open class ShortNotePostViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -481,7 +483,7 @@ open class ShortNotePostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.account.deleteDraft(version) } suspend fun sendDraftSync() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index f3b8ff4fed..c0a1cd99ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -127,7 +127,9 @@ class NewPublicMessageViewModel : draftTag.versions.collectLatest { // don't save the first if (it > 0) { - sendDraftSync() + accountViewModel.runIOCatching { + sendDraftSync() + } } } } @@ -319,7 +321,7 @@ class NewPublicMessageViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.deleteDraft(version) + accountViewModel.account.deleteDraft(version) } suspend fun sendDraftSync() { From 897e9d84cb3253e0429df14d28baca3d00723d9b Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 21 Aug 2025 18:31:11 +0000 Subject: [PATCH 15/48] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 2 ++ amethyst/src/main/res/values-de-rDE/strings.xml | 2 ++ amethyst/src/main/res/values-sv-rSE/strings.xml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 567ef893e1..f6e4f8aac0 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1010,4 +1010,6 @@ Vyhledávání hashtag: #%1$s Nepřekládat z Zde zobrazené jazyky nebudou přeloženy. Vyberte jazyk, který chcete odstranit a nechat je znovu přeložit. + Pozastavit + Hrát diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index d9aea16725..5d7816ae6a 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1015,4 +1015,6 @@ anz der Bedingungen ist erforderlich Suche Hashtag: #%1$s Nicht übersetzen von Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen. + Pausen + Abspielen diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 671dd309aa..39165fb5a4 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1009,4 +1009,6 @@ Sök hashtag: #%1$s Översätt inte från Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. + Pausa + Spela From 75da18a30b9260adc8ec471f15f9b0c1dfd3db6f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 14:32:48 -0400 Subject: [PATCH 16/48] Version 1.00.2 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 98340f6305..b4358454bd 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 420 - versionName = generateVersionName("1.00.1") + versionCode = 421 + versionName = generateVersionName("1.00.2") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 965db8b91b14fe7a0d5000114df90d743cd33116 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 15:13:53 -0400 Subject: [PATCH 17/48] trying to remove HEAD from the release build name --- amethyst/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index b4358454bd..c39fd6aada 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -21,7 +21,7 @@ def getCurrentBranch() { def generateVersionName(String baseVersion) { def currentBranch = getCurrentBranch() - if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown") { + if (currentBranch == "main" || currentBranch == "master" || currentBranch == "unknown" || currentBranch == "HEAD") { return baseVersion } else { // Clean branch name for version (replace special characters) From d810aaa2d034dc51627fff273d0da3616e95bf2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 17:10:10 -0400 Subject: [PATCH 18/48] streamlines function calls on AccountViewModel --- .../ui/screen/loggedIn/AccountViewModel.kt | 99 +++++-------------- 1 file changed, 26 insertions(+), 73 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 272ae9f474..e45ac1efee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -667,9 +667,7 @@ class AccountViewModel( note: Note, type: ReportType, content: String = "", - ) { - runIOCatching { account.report(note, type, content) } - } + ) = runIOCatching { account.report(note, type, content) } fun report( user: User, @@ -681,17 +679,11 @@ class AccountViewModel( } } - fun boost(note: Note) { - runIOCatching { account.boost(note) } - } + fun boost(note: Note) = runIOCatching { account.boost(note) } - fun removeEmojiPack(emojiPack: Note) { - runIOCatching { account.removeEmojiPack(emojiPack) } - } + fun removeEmojiPack(emojiPack: Note) = runIOCatching { account.removeEmojiPack(emojiPack) } - fun addEmojiPack(emojiPack: Note) { - runIOCatching { account.addEmojiPack(emojiPack) } - } + fun addEmojiPack(emojiPack: Note) = runIOCatching { account.addEmojiPack(emojiPack) } fun addMediaToGallery( hex: String, @@ -701,13 +693,9 @@ class AccountViewModel( dim: DimensionTag?, hash: String?, mimeType: String?, - ) { - runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } - } + ) = runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } - fun removeFromMediaGallery(note: Note) { - runIOCatching { account.removeFromGallery(note) } - } + fun removeFromMediaGallery(note: Note) = runIOCatching { account.removeFromGallery(note) } fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex)) @@ -736,13 +724,9 @@ class AccountViewModel( } } - fun delete(notes: List) { - runIOCatching { account.delete(notes) } - } + fun delete(notes: List) = runIOCatching { account.delete(notes) } - fun delete(note: Note) { - runIOCatching { account.delete(note) } - } + fun delete(note: Note) = runIOCatching { account.delete(note) } fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note) @@ -790,61 +774,33 @@ class AccountViewModel( } } - fun follow(community: AddressableNote) { - runIOCatching { account.follow(community) } - } + fun follow(community: AddressableNote) = runIOCatching { account.follow(community) } - fun follow(channel: PublicChatChannel) { - runIOCatching { account.follow(channel) } - } + fun follow(channel: PublicChatChannel) = runIOCatching { account.follow(channel) } - fun follow(channel: EphemeralChatChannel) { - runIOCatching { account.follow(channel) } - } + fun follow(channel: EphemeralChatChannel) = runIOCatching { account.follow(channel) } - fun unfollow(community: AddressableNote) { - runIOCatching { account.unfollow(community) } - } + fun unfollow(community: AddressableNote) = runIOCatching { account.unfollow(community) } - fun unfollow(channel: PublicChatChannel) { - runIOCatching { account.unfollow(channel) } - } + fun unfollow(channel: PublicChatChannel) = runIOCatching { account.unfollow(channel) } - fun unfollow(channel: EphemeralChatChannel) { - runIOCatching { account.unfollow(channel) } - } + fun unfollow(channel: EphemeralChatChannel) = runIOCatching { account.unfollow(channel) } - fun follow(user: User) { - runIOCatching { account.follow(user) } - } + fun follow(user: User) = runIOCatching { account.follow(user) } - fun unfollow(user: User) { - runIOCatching { account.unfollow(user) } - } + fun unfollow(user: User) = runIOCatching { account.unfollow(user) } - fun followGeohash(tag: String) { - runIOCatching { account.followGeohash(tag) } - } + fun followGeohash(tag: String) = runIOCatching { account.followGeohash(tag) } - fun unfollowGeohash(tag: String) { - runIOCatching { account.unfollowGeohash(tag) } - } + fun unfollowGeohash(tag: String) = runIOCatching { account.unfollowGeohash(tag) } - fun followHashtag(tag: String) { - runIOCatching { account.followHashtag(tag) } - } + fun followHashtag(tag: String) = runIOCatching { account.followHashtag(tag) } - fun unfollowHashtag(tag: String) { - runIOCatching { account.unfollowHashtag(tag) } - } + fun unfollowHashtag(tag: String) = runIOCatching { account.unfollowHashtag(tag) } - fun showWord(word: String) { - runIOCatching { account.showWord(word) } - } + fun showWord(word: String) = runIOCatching { account.showWord(word) } - fun hideWord(word: String) { - runIOCatching { account.hideWord(word) } - } + fun hideWord(word: String) = runIOCatching { account.hideWord(word) } fun isLoggedUser(pubkeyHex: HexKey?): Boolean = account.signer.pubKey == pubkeyHex @@ -927,17 +883,14 @@ class AccountViewModel( fun updateStatus( address: Address, newStatus: String, - ) { - runIOCatching { - account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) - } + ) = runIOCatching { + account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) } - fun deleteStatus(address: Address) { + fun deleteStatus(address: Address) = runIOCatching { account.deleteStatus(LocalCache.getOrCreateAddressableNote(address)) } - } fun urlPreview( url: String, @@ -1578,7 +1531,7 @@ class AccountViewModel( root: InteractiveStoryBaseEvent, readingScene: InteractiveStoryBaseEvent, ) { - viewModelScope.launch(Dispatchers.IO) { + runIOCatching { val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl() val readingState = getInteractiveStoryReadingState(root.addressTag()) From d0b3c9b01910e4468fd213942bf150009b2e9a9b Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 21 Aug 2025 21:12:11 +0000 Subject: [PATCH 19/48] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pt-rBR/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 40dd5406e6..d8812ec585 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1010,4 +1010,6 @@ Pesquisar hashtag: #%1$s Não Traduzir de Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. + Pausar + Reproduzir From 693a75b9ec0b44bbe6716e3ec2159a4d720e62e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 17:18:47 -0400 Subject: [PATCH 20/48] Don't try to decrypt appData unless it is a writeable account --- .../nip78AppSpecific/AppSpecificState.kt | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index c6b203ba1e..92f8d62365 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -64,40 +64,42 @@ class AppSpecificState( } init { - settings.backupAppSpecificData?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) - try { - val decrypted = signer.decrypt(event.content, event.pubKey) - val syncedSettings = JsonMapper.mapper.readValue(decrypted) - settings.syncedSettings.updateFrom(syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e) + if (settings.isWriteable()) { + settings.backupAppSpecificData?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + LocalCache.justConsumeMyOwnEvent(event) + try { + val decrypted = signer.decrypt(event.content, event.pubKey) + val syncedSettings = JsonMapper.mapper.readValue(decrypted) + settings.syncedSettings.updateFrom(syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e) + } } } - } - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") - getAppSpecificDataFlow().collect { - try { - Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") - (it.note.event as? AppSpecificDataEvent)?.let { - val decrypted = signer.decrypt(it.content, it.pubKey) - try { - val syncedSettings = JsonMapper.mapper.readValue(decrypted) - settings.updateAppSpecificData(it, syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") + getAppSpecificDataFlow().collect { + try { + Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") + (it.note.event as? AppSpecificDataEvent)?.let { + val decrypted = signer.decrypt(it.content, it.pubKey) + try { + val syncedSettings = JsonMapper.mapper.readValue(decrypted) + settings.updateAppSpecificData(it, syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + } } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decrypting latestAppSpecificData from Preferences", e) } - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decrypting latestAppSpecificData from Preferences", e) } } } From 499316687f7f46d5650b22f6e4fbab02f411111c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 17:19:22 -0400 Subject: [PATCH 21/48] Avoids parsing bad NIP-28 objects --- .../quartz/nip28PublicChat/admin/ChannelCreateEvent.kt | 2 +- .../quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index c9dac836ce..b43d0e8ab7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -60,7 +60,7 @@ class ChannelCreateEvent( val newInfo = try { - if (isEncrypted()) { + if (content.isEmpty() || !content.startsWith("{") || isEncrypted()) { ChannelDataNorm() } else { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index 526214f20b..eabbdb7542 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -66,7 +66,7 @@ class ChannelMetadataEvent( val newInfo = try { - if (isEncrypted()) { + if (content.isEmpty() || !content.startsWith("{") || isEncrypted()) { ChannelDataNorm() } else { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() From 1f2a153f47dd66cc728bb2faec4d8d5f988a8c3b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 20:11:44 -0400 Subject: [PATCH 22/48] Fixes lack of feed updates for those that didn't follow any communities --- .../amethyst/model/nip65RelayList/OutboxRelaySetState.kt | 7 ++++++- .../amethyst/model/topNavFeeds/CommunityRelayLoader.kt | 9 +++++++-- .../amethyst/model/topNavFeeds/OutboxRelayLoader.kt | 9 +++++++-- .../allFollows/AllFollowsByOutboxTopNavFilter.kt | 2 ++ .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt index 8f37e8cf34..3d93db9873 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt @@ -70,7 +70,12 @@ class OutboxRelaySetState( usersToLoad .transformLatest { followList -> val flows: List> = allRelayListFlows(followList) - val relayListFlows = combineAllFlows(flows) + val relayListFlows = + if (flows.isEmpty()) { + MutableStateFlow(emptySet()) + } else { + combineAllFlows(flows) + } emitAll(relayListFlows) }.onStart { emit( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index d4f4e44fea..16cf45525b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine class CommunityRelayLoader { @@ -81,8 +82,12 @@ class CommunityRelayLoader { ?.stateFlow } - return combine(noteMetadataFlows) { communityNotes -> - transformation(communitiesPerRelay(communityNotes, cache)) + return if (noteMetadataFlows.isEmpty()) { + MutableStateFlow(transformation(emptyMap())) + } else { + combine(noteMetadataFlows) { communityNotes -> + transformation(communitiesPerRelay(communityNotes, cache)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 26bee7d8e6..692ae4cb4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine class OutboxRelayLoader { @@ -86,8 +87,12 @@ class OutboxRelayLoader { note.flow().metadata.stateFlow } - return combine(noteMetadataFlows) { outboxRelays -> - transformation(authorsPerRelay(outboxRelays, cache)) + return if (noteMetadataFlows.isEmpty()) { + MutableStateFlow(transformation(emptyMap())) + } else { + combine(noteMetadataFlows) { outboxRelays -> + transformation(authorsPerRelay(outboxRelays, cache)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index 38c4911e50..c3fd5f88bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -86,6 +86,7 @@ class AllFollowsByOutboxTopNavFilter( } else { MutableStateFlow(emptyMap()) } + val communitiesPerRelay = if (communities != null) { CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it } @@ -116,6 +117,7 @@ class AllFollowsByOutboxTopNavFilter( } else { emptyMap() } + val communitiesPerRelay = if (communities != null) { CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index e45ac1efee..5815003018 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -222,7 +222,7 @@ class AccountViewModel( } } - if (flows != null) { + if (!flows.isNullOrEmpty()) { combine(flows) { it.any { it } } From 273186ef357bc664512f40105a6e441b8ca25fb8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 21 Aug 2025 20:13:52 -0400 Subject: [PATCH 23/48] version 1.00.3 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index c39fd6aada..3741b5081e 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 421 - versionName = generateVersionName("1.00.2") + versionCode = 422 + versionName = generateVersionName("1.00.3") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 692f77dcd1ad9fe7a9297d8de1fd6364b3f5e03f Mon Sep 17 00:00:00 2001 From: Neil Rizen <33267501+newrizen@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:56:00 -0300 Subject: [PATCH 24/48] Update pt_rbr strings.xml More compact translation, with corrected spelling (there is no "automaticamente-traduzido" with dash in Portuguese, so I changed it to 'autotraduzido' and moved the dash away to use as a ':' ) and visually more pleasing. --- amethyst/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index d8812ec585..ec6ee7a20c 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -247,8 +247,8 @@ Forçar uso do Tor ao conectar postagens recebidas Remover - Automaticamente - traduzido de + Autotraduzido + de para Mostrar em %1$s primeiro Chat público sobre %1$s From 3df7129f00ffcc3405779525f4abb3185f12713d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 12:41:03 -0400 Subject: [PATCH 25/48] Deletes unused state class --- .../nip65RelayList/OutboxRelaySetState.kt | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt deleted file mode 100644 index 3d93db9873..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.model.nip65RelayList - -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest - -class OutboxRelaySetState( - usersToLoad: MutableStateFlow>, - val cache: LocalCache, - scope: CoroutineScope, -) { - fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.Companion.createAddress(pubkey) - - fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey)) - - fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow - - fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent - - fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - - fun combineAllFlows(flows: List>): Flow> = - combine(flows) { relayListNotes: Array -> - relayListNotes.mapNotNull { - (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() - } - }.map { - it.flatten().toSet() - } - - @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> = - usersToLoad - .transformLatest { followList -> - val flows: List> = allRelayListFlows(followList) - val relayListFlows = - if (flows.isEmpty()) { - MutableStateFlow(emptySet()) - } else { - combineAllFlows(flows) - } - emitAll(relayListFlows) - }.onStart { - emit( - usersToLoad.value - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm() - }.flatten() - .toSet(), - ) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Companion.Eagerly, - emptySet(), - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val flowSet: StateFlow> = - flow - .map { relayList -> - relayList.map { it.url }.toSet() - }.onStart { - emit( - usersToLoad.value - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten() - .toSet(), - ) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Companion.Eagerly, - emptySet(), - ) -} From e0ffa6d4fb9b871cd41c55a175a81cd41a73dda6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 12:42:40 -0400 Subject: [PATCH 26/48] In case a user does not have an outbox list, defaults to all hints seen for that user. --- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 14 ++++++++++++-- .../model/topNavFeeds/OutboxRelayLoader.kt | 6 ++++-- .../user/loaders/FilterUserMetadataForKey.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../chats/privateDM/datasource/FilterNip04DMs.kt | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 4f6fe71288..13df157b6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.ifEmpty class FollowsPerOutboxRelay( kind3Follows: FollowListState, @@ -68,7 +70,11 @@ class FollowsPerOutboxRelay( mapOfSet { relayListNotes.forEach { noteState -> noteState.note.author?.pubkeyHex?.let { authorHex -> - (noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay -> + val outboxRelayList = + getNIP65RelayList(authorHex)?.writeRelaysNorm() + ?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays + outboxRelayList.forEach { relay -> add(relay, authorHex) } } @@ -85,7 +91,11 @@ class FollowsPerOutboxRelay( emit( mapOfSet { kind3Follows.flow.value.authors.map { authorHex -> - getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + val outboxRelayList = + getNIP65RelayList(authorHex)?.writeRelaysNorm() + ?: LocalCache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays + outboxRelayList.forEach { relay -> add(relay, authorHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 692ae4cb4a..2d992980e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.topNavFeeds import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -50,8 +51,9 @@ class OutboxRelayLoader { if (authorHex != null) { val relays = - (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } - ?: cache.relayHints.hintsForKey(authorHex) + (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() + ?: cache.relayHints.hintsForKey(authorHex).ifEmpty { null } + ?: Constants.eventFinderRelays relays.forEach { add(it, authorHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index d147ef3fff..b419e86a58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -52,7 +52,7 @@ fun filterFindUserMetadataForKey( mapOfSet { authors.forEach { key -> val relays = - key.authorRelayList()?.writeRelaysNorm()?.ifEmpty { null } + key.authorRelayList()?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null } ?: (key.relaysBeingUsed.keys + defaultRelays).toList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index ebcd0f627a..75b68e1d60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -50,7 +50,7 @@ fun filterUserMetadataForKey( val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + authorHomeRelayEvent?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index 3083a29cd7..9f4c0fa908 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -48,7 +48,7 @@ fun filterNip04DMs( val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) val outbox = - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + authorHomeRelayEvent?.writeRelaysNorm() ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) From f9ea1270fa3f1a2c3e145f967aeabed088de7f6f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 13:35:46 -0400 Subject: [PATCH 27/48] creates a separate okhttp for relays --- .../java/com/vitorpamplona/amethyst/Amethyst.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 9101a08fbf..fe960b6d1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -82,7 +82,7 @@ class Amethyst : Application() { // Service that will run at all times to receive events from Pokey val pokeyReceiver = PokeyReceiver() - // creates okHttpClients based on the conditions of the connection and tor status + // manages all the other connections separately from relays. val okHttpClients = DualHttpClientManager( userAgent = appAgent, @@ -92,12 +92,22 @@ class Amethyst : Application() { scope = applicationIOScope, ) + // manages all relay connections + val okHttpClientForRelays = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + val torProxySettingsAnchor = ProxySettingsAnchor() // Connects the NostrClient class with okHttp val websocketBuilder = OkHttpWebSocket.Builder { url -> - okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url)) + okHttpClientForRelays.getHttpClient(torProxySettingsAnchor.useProxy(url)) } // Caches all events in Memory From fa80912db2db9cf27b3f8be4625c71feafa2df0e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 13:50:28 -0400 Subject: [PATCH 28/48] Uses a separate okhttp for DM relays --- .../java/com/vitorpamplona/amethyst/Amethyst.kt | 16 +++++++++++++++- .../service/okhttp/ProxySettingsAnchor.kt | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index fe960b6d1d..70e36a3f52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -102,12 +102,26 @@ class Amethyst : Application() { scope = applicationIOScope, ) + // manages all relay connections + val okHttpClientForRelaysForDms = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + val torProxySettingsAnchor = ProxySettingsAnchor() // Connects the NostrClient class with okHttp val websocketBuilder = OkHttpWebSocket.Builder { url -> - okHttpClientForRelays.getHttpClient(torProxySettingsAnchor.useProxy(url)) + if (torProxySettingsAnchor.isDM(url)) { + okHttpClientForRelaysForDms.getHttpClient(torProxySettingsAnchor.useProxy(url)) + } else { + okHttpClientForRelays.getHttpClient(torProxySettingsAnchor.useProxy(url)) + } } // Caches all events in Memory diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt index 37af51d960..b97d329a28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt @@ -38,5 +38,6 @@ class ProxySettingsAnchor { ), ) + var isDM: (NormalizedRelayUrl) -> Boolean = { it in flow.value.value.dmRelayList } var useProxy: (NormalizedRelayUrl) -> Boolean = { flow.value.value.useTor(it) } } From 0d3c60b9c29f8c600c2f8443cc3ba555fbbd37ec Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 16:11:30 -0400 Subject: [PATCH 29/48] Fixes the disappearance of drafts. --- .../loggedIn/AccountFeedContentStates.kt | 5 ++ .../screen/loggedIn/drafts/DraftListScreen.kt | 56 ++++--------------- .../drafts/dal/DraftEventsFeedViewModel.kt | 39 ------------- 3 files changed, 17 insertions(+), 83 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 2d9bbd153a..962a1009d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivitie import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DiscoverNIP89FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.DiscoverMarketplaceFeedFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter @@ -68,6 +69,8 @@ class AccountFeedContentStates( val feedListOptions = FollowListState(accountViewModel.account, accountViewModel.viewModelScope) + val drafts = FeedContentState(DraftEventsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + suspend fun init() { notificationSummary.initializeSuspend() feedListOptions.initializeSuspend() @@ -97,6 +100,8 @@ class AccountFeedContentStates( notificationSummary.invalidateInsertData(newNotes) feedListOptions.updateFeedWith(newNotes) + + drafts.updateFeedWith(newNotes) } fun destroy() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 88bfc37633..95179f429f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -38,32 +38,27 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer +import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS +import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -74,42 +69,16 @@ fun DraftListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val draftFeedViewModel: DraftEventsFeedViewModel = - viewModel( - key = "NostrDraftEventsFeedViewModel", - factory = DraftEventsFeedViewModel.Factory(accountViewModel.account), - ) - - RenderDraftListScreen(draftFeedViewModel, accountViewModel, nav) + RenderDraftListScreen(accountViewModel.feedStates.drafts, accountViewModel, nav) } @Composable private fun RenderDraftListScreen( - feedViewModel: DraftEventsFeedViewModel, + feedState: FeedContentState, accountViewModel: AccountViewModel, nav: INav, ) { - val lifeCycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(feedViewModel) { - feedViewModel.invalidateData() - } - - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("DraftList Start") - feedViewModel.invalidateData() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("DraftList Stop") - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } - } + WatchLifecycleAndUpdateModel(feedState) DisappearingScaffold( isInvertedLayout = false, @@ -119,15 +88,15 @@ private fun RenderDraftListScreen( accountViewModel = accountViewModel, ) { Column(Modifier.padding(it).fillMaxHeight()) { - RefresheableBox(feedViewModel) { - SaveableFeedState(feedViewModel.feedState, DRAFTS) { listState -> - RenderFeedState( - viewModel = feedViewModel, + RefresheableBox(feedState) { + SaveableFeedState(feedState, DRAFTS) { listState -> + RenderFeedContentState( + feedContentState = feedState, accountViewModel = accountViewModel, listState = listState, nav = nav, routeForLastRead = null, - onLoaded = { DraftFeedLoaded(it, listState, null, accountViewModel, nav) }, + onLoaded = { DraftFeedLoaded(it, listState, accountViewModel, nav) }, ) } } @@ -140,7 +109,6 @@ private fun RenderDraftListScreen( private fun DraftFeedLoaded( loaded: FeedState.Loaded, listState: LazyListState, - routeForLastRead: String?, accountViewModel: AccountViewModel, nav: INav, ) { @@ -207,7 +175,7 @@ private fun DraftFeedLoaded( NoteCompose( item, modifier = MaterialTheme.colorScheme.maxWidthWithBackground, - routeForLastRead = routeForLastRead, + routeForLastRead = null, isBoostedNote = false, isHiddenFeed = items.showHidden, quotesLeft = 3, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt deleted file mode 100644 index a0c753bfb7..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/dal/DraftEventsFeedViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal - -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.FeedViewModel - -@Stable -class DraftEventsFeedViewModel( - val account: Account, -) : FeedViewModel(DraftEventsFeedFilter(account)) { - class Factory( - val account: Account, - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = DraftEventsFeedViewModel(account) as T - } -} From c1d8aac0301cd1116465d3116f1eb8e365016449 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 16:29:49 -0400 Subject: [PATCH 30/48] Fixes https://github.com/vitorpamplona/amethyst/issues/1422 --- .../vitorpamplona/amethyst/model/Account.kt | 29 ++++++++++++++++--- .../nip22Comments/CommentPostViewModel.kt | 8 +++-- .../nip22Comments/GenericCommentPostScreen.kt | 8 ++--- .../privateDM/send/ChatNewMessageViewModel.kt | 10 ++++--- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +-- .../send/ChannelNewMessageViewModel.kt | 8 +++-- .../nip99Classifieds/NewProductScreen.kt | 15 ++++------ .../nip99Classifieds/NewProductViewModel.kt | 17 +++++------ .../loggedIn/home/ShortNotePostScreen.kt | 4 +-- .../loggedIn/home/ShortNotePostViewModel.kt | 8 +++-- .../publicMessages/NewPublicMessageScreen.kt | 5 +--- .../NewPublicMessageViewModel.kt | 8 +++-- 12 files changed, 71 insertions(+), 53 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e955381987..3502f25a1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -212,6 +212,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Locale +import kotlin.coroutines.cancellation.CancellationException @OptIn(DelicateCoroutinesApi::class) @Stable @@ -1140,7 +1141,19 @@ class Account( return event } - suspend fun createAndSendDraft( + suspend fun createAndSendDraftIgnoreErrors( + draftTag: String, + template: EventTemplate, + broadcast: Set = emptySet(), + ) { + try { + createAndSendDraftInner(draftTag, template, broadcast) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + suspend fun createAndSendDraftInner( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), @@ -1164,7 +1177,15 @@ class Account( } } - suspend fun deleteDraft(draftTag: String) { + suspend fun deleteDraftIgnoreErrors(draftTag: String) { + try { + deleteDraftInner(draftTag) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + suspend fun deleteDraftInner(draftTag: String) { if (!isWriteable()) return val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() @@ -1293,7 +1314,7 @@ class Account( } if (draftTag != null) { - createAndSendDraft(draftTag, template) + createAndSendDraftIgnoreErrors(draftTag, template) } else { val it = signer.sign(template) cache.justConsumeMyOwnEvent(it) @@ -1338,7 +1359,7 @@ class Account( val broadcastNotes = mapEntitiesToNotes(quotes).toSet() if (draftTag != null) { - createAndSendDraft(draftTag, template) + createAndSendDraftIgnoreErrors(draftTag, template) } else { val it = signer.sign(template) cache.justConsumeMyOwnEvent(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index d9016fb111..6c05d97144 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -317,12 +317,14 @@ open class CommentPostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.account.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -331,7 +333,7 @@ open class CommentPostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index d5874e8246..d4b8eb9585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -86,7 +85,6 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable @@ -144,16 +142,16 @@ fun GenericCommentPostScreen( onCancel = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendDraftSync() - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendPostSync() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index dcb0205167..056787bbb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -351,12 +351,14 @@ class ChatNewMessageViewModel : val version = draftTag.current innerSendPost(null) cancel() - accountViewModel.account.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - account.deleteDraft(draftTag.current) + account.deleteDraftIgnoreErrors(draftTag.current) } else { innerSendPost(draftTag.current) } @@ -471,7 +473,7 @@ class ChatNewMessageViewModel : } if (draftTag != null) { - accountViewModel.account.createAndSendDraft(draftTag, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip17PrivateMessage(template) } @@ -488,7 +490,7 @@ class ChatNewMessageViewModel : ) if (draftTag != null) { - accountViewModel.account.createAndSendDraft(draftTag, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip04PrivateMessage(template) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 94f18b6a64..cde640df8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -175,10 +175,9 @@ fun NewGroupDMScreen( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - delay(100) - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -188,7 +187,6 @@ fun NewGroupDMScreen( postViewModel.room?.let { nav.nav(routeToMessage(it, null, null, null, accountViewModel)) } - postViewModel.cancel() } nav.popBack() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 71499c5fa2..bc775bac9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -276,12 +276,14 @@ open class ChannelNewMessageViewModel : cancel() accountViewModel.account.signAndSendPrivately(template, channelRelays) - accountViewModel.account.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - account.deleteDraft(draftTag.current) + account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -290,7 +292,7 @@ open class ChannelNewMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 9d0ec55162..bd30e46660 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -142,22 +142,19 @@ fun NewProductScreen( titleRes = R.string.new_product, isActive = postViewModel::canPost, onCancel = { - try { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() - } - } catch (e: SignerExceptions.ReadOnlyException) { - // do nothing. + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.runIOCatching { + postViewModel.sendDraftSync() + postViewModel.cancel() } + nav.popBack() }, onPost = { try { accountViewModel.viewModelScope.launch(Dispatchers.IO) { postViewModel.sendPostSync() nav.popBack() - postViewModel.cancel() } } catch (e: SignerExceptions.ReadOnlyException) { accountViewModel.toastManager.toast( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index d55c8fe797..265f6d544d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -293,24 +293,23 @@ open class NewProductViewModel : val accountViewModel = accountViewModel ?: return val template = createTemplate() ?: return - accountViewModel.account.signAndSendPrivatelyOrBroadcast( - template, - relayList = { relayList }, - ) - - accountViewModel.account.deleteDraft(draftTag.current) - + val version = draftTag.current cancel() + + accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList }) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { val accountViewModel = accountViewModel ?: return if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index c9821ec971..d8aa344efb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -98,7 +98,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @@ -174,7 +173,6 @@ private fun NewPostScreenInner( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendPostSync() - delay(100) nav.popBack() } }, @@ -183,9 +181,9 @@ private fun NewPostScreenInner( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - nav.popBack() postViewModel.cancel() } + nav.popBack() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 387bf43515..5b975b6bfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -483,12 +483,14 @@ open class ShortNotePostViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.account.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val attachments = mutableSetOf() nip95attachments.forEach { @@ -497,7 +499,7 @@ open class ShortNotePostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, attachments) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index dd280e7852..bd99422ba6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -138,10 +138,9 @@ fun NewPublicMessageScreen( // function when the postViewModel is released accountViewModel.runIOCatching { postViewModel.sendDraftSync() - delay(100) - nav.popBack() postViewModel.cancel() } + nav.popBack() }, onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -149,9 +148,7 @@ fun NewPublicMessageScreen( accountViewModel.runIOCatching { postViewModel.sendPostSync() nav.popBack() - postViewModel.cancel() } - nav.popBack() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index c0a1cd99ff..3c42d85e1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -321,12 +321,14 @@ class NewPublicMessageViewModel : cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel.account.deleteDraft(version) + accountViewModel.viewModelScope.launch { + accountViewModel.account.deleteDraftIgnoreErrors(version) + } } suspend fun sendDraftSync() { if (message.text.isBlank()) { - accountViewModel.account.deleteDraft(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { val broadcast = mutableSetOf() nip95attachments.forEach { @@ -335,7 +337,7 @@ class NewPublicMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraft(draftTag.current, template, broadcast) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) } } From 0f59befb0340bed6ccdc2633727d36b0975e28dd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 16:29:56 -0400 Subject: [PATCH 31/48] Version 1.00.4 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 3741b5081e..cbeda8e3ac 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 422 - versionName = generateVersionName("1.00.3") + versionCode = 423 + versionName = generateVersionName("1.00.4") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 5ed0bf48a66bf92ced7e7ddf5127b24855253dae Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 22 Aug 2025 20:31:42 +0000 Subject: [PATCH 32/48] New Crowdin translations by GitHub Action --- .../src/main/res/values-hu-rHU/strings.xml | 2 + .../src/main/res/values-nl-rNL/strings.xml | 2 + .../src/main/res/values-zh-rCN/strings.xml | 54 +++++++++---------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c87d142d55..fb63217b7d 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1012,4 +1012,6 @@ Hashtag keresése: #%1$s Innentől NE fordítsa le Az itt látható nyelvek nem lesznek lefordítva. Az eltávolításához és az újbóli fordításhoz válasszon ki egy nyelvet. + Szüneteltetés + Lejátszás diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index d4e9229b7a..50d666ced4 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -1012,4 +1012,6 @@ Zoek hashtag: #%1$s Niet vertalen van Talen die hier getoond worden worden worden niet vertaald. Selecteer een taal om te verwijderen en het opnieuw te laten vertalen. + Pauze + Afspelen diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6744461652..b598945356 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -51,13 +51,13 @@ 你正在使用公钥,公钥是只读的。使用私钥登录以便能够取消关注 你正在使用公钥,公钥是只读的。使用私钥登录以便能够隐藏单词或句子 你正在使用公钥,公钥是只读的。使用私钥登录以便能够显示单词或句子 - 你正在使用公钥,公钥是只读的。要更改设置请用私钥登录 - 你正在使用公钥,公钥是只读的。要上传请使用私钥登录 - 你正在使用公钥,公钥是只读的。要报名参加活动请使用私钥登录 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能更改设置。 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能上传。 + 您正在使用公钥以只读方式登录,需要使用私钥登录才能签署事件。 未授权的解密 - 签名人没有授权进行此操作所需的解密。在签名人应用中激活 NIP-44 解密并重试 - 未找到签名人 - 签名人应用被卸载了吗?检查是否安装了签名人以及是否签名人有该账户。注销并再次登录,签名人应用已更改。 + 签名器没有授权解密操作,请在签名器中授予 NIP-44 解密权限并重试。 + 未找到签名器 + 签名器被卸载?请检查是否已经安装了签名器以及其中是否存在该账户。变更签名器需要注销后重新登录。 打闪 浏览次数 提升 @@ -150,7 +150,7 @@ 拍照 录制消息 录制消息 - 单击并按住来录制消息 + 点按以录制消息 上传中… 用户尚未设置闪电地址以接收聪 "🔏在此回复… " @@ -231,10 +231,10 @@ 临时聊天 中继聊天 中继聊天 - 中继聊天是由主中继控制的聊天群。 - 它们对 Nostr 上的所有人可见,任何人均可加入它们。 - 它们对围绕特定话题的开放社区很好。这些群中的一些是临时存在的 - 因而,聊天消息会随着时间消失 + 中继聊天是由聊天所属中继控制的聊天群组。 + Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 + 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, + 因此聊天信息会随着时间的推移而消失。 公共聊天 公开聊天元数据 公开聊天对所有 Nostr 用户都是可见的,任何人都可以: @@ -244,7 +244,7 @@ 使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。 付费中继 - 连接时强制使用 Tor + 强制使用 Tor 进行连接 收到文章 移除 自动 @@ -289,7 +289,7 @@ 错误 "由 %1$s 创建" "颁发给 %1$s 的徽章图片" - \"徽章奖品图片 + \"徽章图片 你收到了新的徽章奖励 徽章奖励授予 文本已复制到剪贴板 @@ -577,7 +577,7 @@ 中继器 关注包 次浏览 - Feed算法 + 动态源算法 市场 直播 社区 @@ -633,8 +633,8 @@ 将你所在位置的地理位置添加到帖子。公众会知道你在当前位置的5公里之内(3英里) 位置限定帖子 只有处于同一地理位置的关注者才能看到贴文。其他追随者无法看到。 - 话题标签专属帖子 - 只有话题标签的关注者才会看到它。您的一般关注者不会看到它。 + 话题限定贴文 + 只有关注了话题的人才能看到贴文,您的普通关注者也无法看到。 加载位置中 没有位置信息权限 在显示你的内容之前添加敏感的内容警告。针对任何 NSFW 内容或一些人可能觉得有冒犯性或令人不安的内容。 @@ -709,7 +709,7 @@ 钱包 %1$s 打开签名应用时出错 找不到签名应用。检查应用是否已被卸载 - 签名请求被拒绝了 + 签名请求已被拒绝 签名请求被拒绝了 请确保签名应用程序已授权此交易 找不到支付闪电发票的钱包(错误:%1$s)。请安装闪电钱包来使用打闪 @@ -838,7 +838,7 @@ 新短篇媒体:图像或视频 新社区笔记 新产品 - 新建地理位置专属帖子 + 新建地理位置限定帖文 展开对此帖子的所有回应 收起对此帖子的所有回应 回复 @@ -903,19 +903,19 @@ 在该设备中运行的中继列表。 可信中继 可信中继 - 你信任的无需 Tor 连接的中继 + 无需使用 Tor 进行连接的可信中继 代理中继 代理中继 - 聚合器中继下载 feed 必须使用的应用的流量,类似 filter.nostr.wine。这替代 outbox 模型,让应用只连接到列表中的中继。 + 聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。 广播中继 广播中继 - 专门推送便笩到所有其他中继的中继,类似 sendit.nosflare.com。Amethyst 会将这个中继添加到所有你所做的新事件 + 推送你的贴文到其他中继的中继(例如:sendit.nosflare.com),应用会把这个中继作为提示嵌入到你发送的所有事件的 JSON 结构中。 索引器中继 索引器中继 - 专门托管每个人的元数据和中继列表的中继,类似 purplepag.es。Amethyst将使用这些中继来查找不在列表中的用户。 - 屏蔽的中继 - 屏蔽的中继 - Amethyst 永远不会连接到这些中继 + 专门保存每个人的账户元数据的中继(例如:purplepag.es),应用会使用这些中继来查找不在你列表中的账户元数据。 + 中继黑名单 + 中继黑名单 + 应用永远不会连接的中继 打闪开发人员! 你的捐赠帮助我们做出不同的贡献。每个聪都很重要! 立即捐款 @@ -1010,8 +1010,8 @@ 此聊天所有用户都连接到的中继 分享图片… 搜索话题标签:#%1$s - 不要翻译自 - 此处显示的语言不会被翻译。请选择一种语言来移除它并重新翻译它。 + 不要翻译 + 此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。 暂停 播放 From c9e5646df29b55b62bfe4000f269367590b837dc Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 22 Aug 2025 20:46:42 +0000 Subject: [PATCH 33/48] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index ec6ee7a20c..d8812ec585 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -247,8 +247,8 @@ Forçar uso do Tor ao conectar postagens recebidas Remover - Autotraduzido - de + Automaticamente + traduzido de para Mostrar em %1$s primeiro Chat público sobre %1$s From 356a8056811498ef62e51c12a0e27276d93b876d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 Aug 2025 18:15:12 -0400 Subject: [PATCH 34/48] Adds a child safety standards section --- PRIVACY.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index a63c9a2186..cfed3b5e56 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -4,33 +4,39 @@ Effective as of Jun 12, 2023 -The Amethyst app for Android does not collect or process any personal information from its users. The app is used to connect to third-party Nostr servers (also called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them. +The Amethyst app for Android does not collect or process any personal information from its users. -The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts are only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. +The app is used to browse third-party Nostr servers (called Relays) that may or may not collect personal information and are not covered by this privacy policy. Each third-party relay server comes equipped with its own privacy policy and terms of use that can be viewed through the app or through that server's website. The developers of this open-source project or maintainers of the distribution channels (app stores) do not have access to the data located in the user's phone. Accounts are fully maintained by the user. We do not have control over them. -Amethyst offers a few options to upload pictures and videos in order to post them online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use. +The app may collect a per-device token, your public key, and a preferred Relay to connect to and provide push notification services through Google's Firebase Cloud Messaging. Other than that, the data from connected accounts is only stored locally on the device when it's required for the functionality and performance of Amethyst. This data is strictly confidential and cannot be accessed by other apps (on non-rooted devices). Phone data can be deleted by clearing Amethyst's local storage or uninstalling the app. -### Privacy with Relays services +Amethyst offers several options for uploading pictures and videos to post online. You can choose the server at your discretion. Similar to relays, such services are independent of the app and have their own privacy policy and terms of use. -Your internet protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online. +### Privacy with Relay services + +Your Internet Protocol (IP) address is exposed to the relays you connect to. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g., a VPN) from trackers online. The relay can also see which public keys you are using and what information you are requesting from the network. Your public key is tied to your IP address and your relay filters. Relays have all your data in raw text. They know your IP, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) with the exception of the content inside Private Zaps and Private DMs. -While the content of direct messages (DMs) is only visible to you, and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link on the DM text. Your uploaded pictures are available to anyone with that direct link. +While the content of direct messages (DMs) is only visible to you and your DM Nostr counterparty, everyone can see when you and your counterparty are DM-ing each other. Image uploads in the DM screen use one of the chosen image servers and simply paste the image link into the DM text. Your uploaded pictures are available to anyone with that direct link. ### Visibility & Permanence of Your Content on Nostr Relays #### Information Visibility Content that you share can be shared with other relays by any user of the network. -Information that you share is publicly visible to anyone reading from relays that have your information. Your information may also be visible to Nostr users who do not share relays with you. +The information you share is publicly visible to anyone reading from relays that have access to your information. Your information may also be visible to Nostr users who do not share relays with you. #### Information Permanence Information shared on Nostr should be assumed permanent for privacy purposes. There is no way to guarantee deleting or editing any content once posted. +## Child safety standards + +Amethyst does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone. The application is 17+. We rely on Google Play's age verification to make sure the user downloading the app is an adult. Since we do not control which relays the user connects to, there is no content moderation beyond the standard block post, block account, and report post and/or account that will hide the content from the user. + ## Terms of Use ### For versions downloaded from Google's Play Store From 36b14f93cb092b918871f1402232e39736357e08 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Aug 2025 10:49:40 -0400 Subject: [PATCH 35/48] Removing multiple relays in the same line. --- .../nip01Core/relay/normalizer/RelayUrlNormalizer.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 8a74e2cdec..87bbf0306e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -95,6 +95,14 @@ class RelayUrlNormalizer { @OptIn(ExperimentalContracts::class) fun fix(url: String): String? { if (url.length < 3) return null + if (url.length > 100) { + // removes multiple urls in the same line + val schemeIdx = url.indexOf("://") + val nextScheme = url.indexOf("://", schemeIdx + 3) + if (nextScheme > 0) { + return null + } + } val trimmed = if (url[0].isWhitespace() || url[url.length - 1].isWhitespace()) { From d2e7e171a183fd2b712374ca9ae35196d79b01e9 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 23 Aug 2025 14:57:56 +0000 Subject: [PATCH 36/48] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index d8812ec585..ec6ee7a20c 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -247,8 +247,8 @@ Forçar uso do Tor ao conectar postagens recebidas Remover - Automaticamente - traduzido de + Autotraduzido + de para Mostrar em %1$s primeiro Chat público sobre %1$s From 83870cd06bc316bb1209ca910fa3048f681df242 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Aug 2025 11:05:22 -0400 Subject: [PATCH 37/48] Version 1.00.5 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index cbeda8e3ac..5c72709ddc 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 423 - versionName = generateVersionName("1.00.4") + versionCode = 424 + versionName = generateVersionName("1.00.5") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 41ca6d281e5daa4613e57dc123977c0355ecd272 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Aug 2025 11:06:11 -0400 Subject: [PATCH 38/48] Revert "Version 1.00.5" This reverts commit 83870cd06bc316bb1209ca910fa3048f681df242. --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 5c72709ddc..cbeda8e3ac 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 424 - versionName = generateVersionName("1.00.5") + versionCode = 423 + versionName = generateVersionName("1.00.4") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From d90618f7f34bc0b837f36eb224112d661987e9b0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 23 Aug 2025 11:06:45 -0400 Subject: [PATCH 39/48] Corrected version 1.00.5 --- amethyst/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index cbeda8e3ac..5c72709ddc 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -44,8 +44,8 @@ android { applicationId = "com.vitorpamplona.amethyst" minSdk = libs.versions.android.minSdk.get().toInteger() targetSdk = libs.versions.android.targetSdk.get().toInteger() - versionCode = 423 - versionName = generateVersionName("1.00.4") + versionCode = 424 + versionName = generateVersionName("1.00.5") buildConfigField "String", "RELEASE_NOTES_ID", "\"08abe267baf5d7ce14db7975866f929e2794cc23484171aef0816c60a2416597\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 0381e221988c3b9e4897d8d10e5092f9975a933d Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Fri, 22 Aug 2025 09:22:10 +0100 Subject: [PATCH 40/48] Corrected TextSpinner package --- .../com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/components/TextSpinner.kt | 2 +- .../amethyst/ui/navigation/topbars/FeedFilterSpinner.kt | 2 +- .../vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt | 5 ++--- .../com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt | 4 ++-- .../ui/note/creators/uploads/ImageVideoDescription.kt | 4 ++-- .../ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt | 4 ++-- .../screen/loggedIn/discover/nip99Classifieds/SellProduct.kt | 4 ++-- .../amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt | 4 ++-- .../ui/screen/loggedIn/settings/AppSettingsScreen.kt | 4 ++-- .../ui/screen/loggedIn/settings/SecurityFiltersScreen.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt | 2 +- 12 files changed, 21 insertions(+), 22 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 643a72ace7..899c3c59c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -58,12 +58,12 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index 954e6a5a28..d36fbec7d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn +package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index c031d040a5..ae0dc505e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition import com.vitorpamplona.amethyst.ui.screen.CommunityName @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.screen.Name import com.vitorpamplona.amethyst.ui.screen.PeopleListName import com.vitorpamplona.amethyst.ui.screen.ResourceName import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index ee6102a1e3..13a5092570 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -81,12 +81,12 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.stringRes @@ -97,7 +97,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Composable fun UpdateZapAmountDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 7526b3e9eb..5f16c7d801 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -65,11 +65,11 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index a119bc8a94..0d3b1052ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -61,11 +61,11 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.CancelIcon import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.QuoteBorder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index 668878e1d5..908b15fc7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -58,14 +58,14 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt index 8697cf643d..6d7831a1a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt @@ -50,9 +50,9 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt index 663b21dd72..c10933db85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt @@ -60,10 +60,10 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.LightRedColor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 64523f48d3..0248eecc07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -56,13 +56,13 @@ import com.vitorpamplona.amethyst.model.parseFeatureSetType import com.vitorpamplona.amethyst.model.parseGalleryType import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.ui.components.PushNotificationSettingsRow +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.mockSharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 02b5a801c3..b8934c2a97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -68,6 +68,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.WarningType import com.vitorpamplona.amethyst.model.parseWarningType import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord +import com.vitorpamplona.amethyst.ui.components.TextSpinner +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -75,8 +77,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.elements.AddButton import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.SpammerAccountsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt index fc370db073..782e60b4be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt @@ -46,8 +46,8 @@ import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp From f3f8beeebe5ec65bd015d8fb7e5caa9fb206d19b Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Fri, 22 Aug 2025 09:49:25 +0100 Subject: [PATCH 41/48] Added semantics to the invisible box contentDescription - provides current state info TODO: add translations --- .../amethyst/ui/components/TextSpinner.kt | 70 ++++++++++++++++--- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index d36fbec7d7..bf97eafff7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -50,6 +50,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -59,13 +65,13 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun TextSpinner( - label: String?, + modifier: Modifier = Modifier, + label: String? = null, placeholder: String, options: ImmutableList, onSelect: (Int) -> Unit, - modifier: Modifier = Modifier, ) { - TextSpinner( + BaseTextSpinner( placeholder, options, onSelect, @@ -89,12 +95,36 @@ fun TextSpinner( onSelect: (Int) -> Unit, modifier: Modifier = Modifier, mainElement: @Composable (currentOption: String, modifier: Modifier) -> Unit, +) { + BaseTextSpinner( + placeholder = placeholder, + options = options, + onSelect = onSelect, + modifier = modifier, + mainElement = mainElement, + ) +} + +@Composable +private fun BaseTextSpinner( + placeholder: String, + options: ImmutableList, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, + mainElement: @Composable (currentOption: String, modifier: Modifier) -> Unit, ) { val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } var optionsShowing by remember { mutableStateOf(false) } var currentText by remember(placeholder) { mutableStateOf(placeholder) } + val accessibilityDescription = + if (currentText == placeholder) { + "Dropdown menu, $placeholder, not selected" + } else { + "Dropdown menu, $currentText selected" + } + Box( modifier = modifier, contentAlignment = Alignment.Center, @@ -105,13 +135,23 @@ fun TextSpinner( ) Box( modifier = - Modifier.matchParentSize().clickable( - interactionSource = interactionSource, - indication = null, - ) { - optionsShowing = true - focusRequester.requestFocus() - }, + Modifier + .matchParentSize() + .clickable( + interactionSource = interactionSource, + indication = null, + ) { + optionsShowing = true + focusRequester.requestFocus() + }.semantics { + role = Role.DropdownList + stateDescription = accessibilityDescription + onClick(label = "Open dropdown menu") { + optionsShowing = true + focusRequester.requestFocus() + return@onClick true + } + }, ) } @@ -188,7 +228,15 @@ fun SpinnerSelectionDialog( } itemsIndexed(options) { index, item -> Row( - modifier = Modifier.fillMaxWidth().clickable { onSelect(index) }.padding(16.dp, 16.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { onSelect(index) } + .padding(16.dp, 16.dp) + .semantics { + role = Role.Button + contentDescription = "Option ${index + 1} of ${options.size}" + }, ) { Column { onRenderItem(item) } } From 03483c932c5fb91a58a9d2ef6787dd1f23755219 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 23 Aug 2025 17:08:17 +0100 Subject: [PATCH 42/48] reduce warnings: remove unused nav parameter remove unnecessary ? operator replace .filter().first() with .first --- .../navigation/topbars/FeedFilterSpinner.kt | 20 ++++++++++++------- .../discover/nip99Classifieds/SellProduct.kt | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index ae0dc505e8..5832d24bee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -260,12 +260,18 @@ fun RenderOption( val noteEvent = noteState.note.event val name = - if (noteEvent is PeopleListEvent) { - noteEvent.nameOrTitle() ?: option.note.dTag() - } else if (noteEvent is FollowListEvent) { - noteEvent.title() ?: option.note.dTag() - } else { - option.note.dTag() + when (noteEvent) { + is PeopleListEvent -> { + noteEvent.nameOrTitle() ?: option.note.dTag() + } + + is FollowListEvent -> { + noteEvent.title() ?: option.note.dTag() + } + + else -> { + option.note.dTag() + } } Text(text = name, color = MaterialTheme.colorScheme.onSurface) @@ -278,7 +284,7 @@ fun RenderOption( ) { val it by observeNote(option.note, accountViewModel) - Text(text = "/n/${((it?.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface) + Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt index 6d7831a1a2..ffd978b741 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt @@ -211,7 +211,7 @@ fun SellProduct(postViewModel: NewProductViewModel) { } TextSpinner( - placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, + placeholder = conditionTypes.first { it.first == postViewModel.condition }.second, options = conditionOptions, onSelect = { postViewModel.updateCondition(conditionTypes[it].first) From b0aa2a8c31f07ca4d6c1adf2acf968665e1d9395 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 23 Aug 2025 17:12:23 +0100 Subject: [PATCH 43/48] added accessibility Description TODO: add translations --- .../navigation/topbars/FeedFilterSpinner.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 5832d24bee..7b9edb4253 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -44,6 +44,11 @@ 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.semantics.Role +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -113,6 +118,13 @@ fun FeedFilterSpinner( Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted) } + val accessibilityDescription = + if (selected != null) { + "Feed filter, $currentText selected" + } else { + "Feed filter, $selectAnOption" + } + Box( modifier = modifier, contentAlignment = Alignment.Center, @@ -195,6 +207,13 @@ fun FeedFilterSpinner( indication = null, ) { optionsShowing = true + }.semantics { + role = Role.DropdownList + stateDescription = accessibilityDescription + onClick(label = "Open feed filter menu") { + optionsShowing = true + return@onClick true + } }, ) } @@ -202,6 +221,7 @@ fun FeedFilterSpinner( if (optionsShowing) { options.isNotEmpty().also { SpinnerSelectionDialog( + title = explainer, options = options, onDismiss = { optionsShowing = false }, onSelect = { From 4006aebf35feb4121c354fbe9b123a0011e7c3b2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 23 Aug 2025 18:24:10 +0100 Subject: [PATCH 44/48] added I18n for new strings --- .../vitorpamplona/amethyst/ui/components/TextSpinner.kt | 9 +++++++-- .../amethyst/ui/navigation/topbars/FeedFilterSpinner.kt | 4 ++-- amethyst/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index bf97eafff7..70ac084219 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -59,6 +59,8 @@ import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import kotlinx.collections.immutable.ImmutableList @@ -125,6 +127,8 @@ private fun BaseTextSpinner( "Dropdown menu, $currentText selected" } + val openDropdownLabel = stringRes(R.string.open_dropdown_menu) + Box( modifier = modifier, contentAlignment = Alignment.Center, @@ -146,7 +150,7 @@ private fun BaseTextSpinner( }.semantics { role = Role.DropdownList stateDescription = accessibilityDescription - onClick(label = "Open dropdown menu") { + onClick(label = openDropdownLabel) { optionsShowing = true focusRequester.requestFocus() return@onClick true @@ -227,6 +231,7 @@ fun SpinnerSelectionDialog( } } itemsIndexed(options) { index, item -> + val optionsOfLabel = stringRes(R.string.option_of, index + 1, options.size) Row( modifier = Modifier @@ -235,7 +240,7 @@ fun SpinnerSelectionDialog( .padding(16.dp, 16.dp) .semantics { role = Role.Button - contentDescription = "Option ${index + 1} of ${options.size}" + contentDescription = optionsOfLabel }, ) { Column { onRenderItem(item) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 7b9edb4253..163011d9c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -120,9 +120,9 @@ fun FeedFilterSpinner( val accessibilityDescription = if (selected != null) { - "Feed filter, $currentText selected" + stringRes(R.string.feed_filter_selected, currentText) } else { - "Feed filter, $selectAnOption" + stringRes(R.string.feed_filter_select_an_option, selectAnOption) } Box( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e89766e8e1..6d334f05f9 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1231,4 +1231,8 @@ Languages shown here will not be translated. Select a language to remove it and have it translated again. Pause Play + Open dropdown menu + Option %1$s of %2$s + Feed filter, %1$s selected + Feed filter, %1$s From 56f299569e417b84d64e22716e961f506bed888c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 23 Aug 2025 22:24:48 +0100 Subject: [PATCH 45/48] added translations CS, DE, PT, SV --- amethyst/src/main/res/values-cs/strings.xml | 3 +++ amethyst/src/main/res/values-de/strings.xml | 3 +++ amethyst/src/main/res/values-pt-rBR/strings.xml | 3 +++ amethyst/src/main/res/values-sv-rSE/strings.xml | 3 +++ 4 files changed, 12 insertions(+) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index fc85f6d271..223c59d5fb 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -933,4 +933,7 @@ Vyberte seznam pro filtrování kanálu Odhlásit se na zámek zařízení Sdílet obrázek… + Možnost %1$s z %2$s + Filtr kanálu, %1$s vybráno + Filtr kanálu, %1$s diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index c7e84846a4..ff6ebf190a 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -973,4 +973,7 @@ anz der Bedingungen ist erforderlich Blockierte Relays Amethyst wird sich niemals mit diesen Relays verbinden Öffentliche Nachricht + Option %1$s von %2$s + Feed-Filter, %1$s ausgewählt + Feed-Filter, %1$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index d8812ec585..264cc8573c 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1012,4 +1012,7 @@ Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. Pausar Reproduzir + Opção %1$s de %2$s + Filtro de feed, %1$s selecionado + Filtro de feed, %1$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 39165fb5a4..fa986f452f 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1011,4 +1011,7 @@ Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. Pausa Spela + Alternativ %1$s av %2$s + Flödesfilter, %1$s valt + Flödesfilter, %1$s From c93cc04409c262b9901f89cfe221f2c1f857d92c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 23 Aug 2025 22:58:16 +0100 Subject: [PATCH 46/48] updated imports --- .../amethyst/ui/components/SelectNotificationProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index f830850aac..2367c9f751 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -54,9 +54,9 @@ import com.halilibo.richtext.ui.material3.RichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler +import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog +import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes From cc6a9f1818e24180f753e103a32f44cc6af87b60 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 25 Aug 2025 13:14:18 +0000 Subject: [PATCH 47/48] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pl-rPL/strings.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 94d56519b7..6c651da040 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -162,7 +162,7 @@ Nowe Kontakty Zablokowani użytkownicy Nowe Wpisy - Konwersacje + Komentarze Wpisy Odpowiedzi Twoje @@ -572,9 +572,9 @@ Wylogowanie usuwa wszystkie informacje lokalne. Upewnij się, że masz kopię zapasową kluczy prywatnych, aby uniknąć utraty konta. Czy chcesz kontynuować? Obserwowane tagi Transmitery - Obserwowani - Wyświetlenia - Algorytmy kanału + Godne uwagi + Popularne + Wybrane przez algorytm Market Transmisja na żywo Społeczności @@ -1009,4 +1009,6 @@ Szukaj tagu: #%1$s Nie tłumacz z Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych. + Pauza + Odtwórz From 4baa5bf65928eb7604705721a8e314e0d1b48b1f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 25 Aug 2025 10:09:39 -0400 Subject: [PATCH 48/48] Fixes https://github.com/vitorpamplona/amethyst/issues/1070 --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 7 ++++--- .../vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt | 4 ++++ .../ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../ui/screen/loggedIn/discover/ChannelCardCompose.kt | 4 ++-- .../screen/loggedIn/profile/gallery/GalleryCardCompose.kt | 2 +- .../ui/screen/loggedIn/threadview/ThreadFeedView.kt | 3 ++- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b5c921998a..4bdf1b7b56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -240,6 +240,7 @@ fun NoteCompose( WatchNoteEvent( baseNote = baseNote, accountViewModel = accountViewModel, + nav, modifier, ) { CheckHiddenFeedWatchBlockAndReport( @@ -312,7 +313,7 @@ fun AcceptableNote( } is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, nav) { showPopup -> CheckNewAndRenderNote( baseNote = baseNote, modifier = modifier, @@ -356,9 +357,9 @@ fun AcceptableNote( nav = nav, ) } - is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) + is BadgeDefinitionEvent -> BadgeDisplay(baseNote, accountViewModel) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> CheckNewAndRenderNote( baseNote = baseNote, modifier = modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 698f434910..c2afd3651a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -82,7 +82,6 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.painterRes @@ -142,6 +141,7 @@ val externalLinkForNote = { note: Note -> fun LongPressToQuickAction( baseNote: Note, accountViewModel: AccountViewModel, + nav: INav, content: @Composable (() -> Unit) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } @@ -153,7 +153,7 @@ fun LongPressToQuickAction( note = baseNote, onDismiss = { popupExpanded.value = false }, accountViewModel = accountViewModel, - nav = EmptyNav, + nav = nav, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index 30358376a4..61c3954814 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @OptIn(ExperimentalFoundationApi::class) @@ -36,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun WatchNoteEvent( baseNote: Note, accountViewModel: AccountViewModel, + nav: INav, modifier: Modifier = Modifier, shortPreview: Boolean = false, onNoteEventFound: @Composable () -> Unit, @@ -47,6 +50,7 @@ fun WatchNoteEvent( LongPressToQuickAction( baseNote = baseNote, accountViewModel = accountViewModel, + nav = nav, ) { showPopup -> BlankNote( remember { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 8a510adab3..1102c7ec0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -97,7 +97,7 @@ fun ChatroomMessageCompose( onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav) { WatchBlockAndReport( note = baseNote, showHiddenWarning = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 2f87c20798..100ab27402 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -62,7 +62,7 @@ fun ChannelCardCompose( accountViewModel: AccountViewModel, nav: INav, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav) { if (forceEventKind == null || baseNote.event?.kind == forceEventKind) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, @@ -94,7 +94,7 @@ fun NormalChannelCard( accountViewModel: AccountViewModel, nav: INav, ) { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> CheckNewAndRenderChannelCard( baseNote, routeForLastRead, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index aeb22a742e..d83168ddb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -47,7 +47,7 @@ fun GalleryCardCompose( nav: INav, ratio: Float = 1.0f, ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) { + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav, shortPreview = true) { CheckHiddenFeedWatchBlockAndReport( note = baseNote, modifier = modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index d4d89fd7c7..1beaef7334 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -395,6 +395,7 @@ fun NoteMaster( WatchNoteEvent( baseNote = baseNote, accountViewModel = accountViewModel, + nav, modifier, ) { CheckHiddenFeedWatchBlockAndReport( @@ -405,7 +406,7 @@ fun NoteMaster( accountViewModel = accountViewModel, nav = nav, ) { canPreview -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> FullBleedNoteCompose( baseNote, modifier