From 0bdada5e414b82cbcef6d85877857553426ad6d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 22:39:46 +0000 Subject: [PATCH 01/45] refactor: reach the media player only through NotePlatform slots Screens and feed composables called the app's player and media renderers directly, around the NotePlatform slots that already exist for them. Twenty-three screens reached media3 that way, mostly through EditPostView. - The chat action sheet and bookmark options open the EditPostView slot; EditPostView and the composer previews use the VideoView and LoadUrlPreview slots. - The thread view, pictures, shorts, file-metadata and video cards, Cordn, Marmot media, group-DM preview, profile header and banner, URL screen, geocache and relay-group screens, podcast list and chat text notes call the ZoomableContentView, ZoomableImageDialog, rememberUrlPreviewState, LocationPreviewMap, PodcastEpisodeAudioPlayer, RenderAudio*, RenderGit*, RenderMusicTrack, Render*SiteEvent and RenderVoiceTrack slots. - New FullscreenVideoView slot for the Blossom blob viewer, which reached into the internal VideoViewInner. - LocationPreviewMap's slot takes the modifier and an optional zoom the map callers pass. - syntheticWaveformFor (pure) moves to commons.audio. Only the platform side (AndroidNotePlatform, the renderers behind the slots, the zoom dialog, AppModules, MainActivity) imports playback now, and no screen reaches it. The media3 engine stays in the app. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/actions/EditPostView.kt | 4 +-- .../mediaServers/BlossomBlobManagerScreen.kt | 8 ++--- .../creators/location/LocationPreviewMap.kt | 6 ++-- .../ui/note/creators/previews/PreviewUrl.kt | 6 +--- .../ui/note/platform/AndroidNotePlatform.kt | 25 ++++++++++++++++ .../amethyst/ui/note/types/MusicTrack.kt | 2 +- .../note/types/PodcastEpisodeAudioPlayer.kt | 2 +- .../amethyst/ui/note/types/VideoDisplay.kt | 2 +- .../display/BookmarkGroupItemOptions.kt | 2 +- .../chats/cordnGroup/CordnGroupChatScreen.kt | 2 +- .../chats/feed/ChatMessageActionSheet.kt | 2 +- .../feed/types/RenderMarmotEncryptedMedia.kt | 4 +-- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 2 +- .../relayGroup/RelayGroupMetadataScreen.kt | 2 +- .../geocaches/create/NewGeocacheScreen.kt | 2 +- .../geocaches/detail/GeocacheDetailScreen.kt | 2 +- .../loggedIn/pictures/PictureCardCompose.kt | 2 +- .../podcasts/PodcastEpisodeListItem.kt | 2 +- .../loggedIn/profile/header/DrawBanner.kt | 2 +- .../loggedIn/profile/header/ProfileHeader.kt | 2 +- .../loggedIn/shorts/VideoCardCompose.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 30 +++++++++---------- .../ui/screen/loggedIn/url/UrlScreen.kt | 2 +- .../loggedIn/video/FileMetadataCardCompose.kt | 2 +- .../commons/audio}/SyntheticWaveform.kt | 3 +- .../commons/ui/note/platform/NotePlatform.kt | 16 ++++++++++ .../ui/note/platform/NotePlatformCalls.kt | 14 +++++++++ 28 files changed, 100 insertions(+), 52 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio}/SyntheticWaveform.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index e093d90651..de413a513b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -89,6 +89,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadUrlPreview +import com.vitorpamplona.amethyst.commons.ui.note.platform.VideoView import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation @@ -102,8 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt index 13069aa9df..33be70f938 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt @@ -131,13 +131,13 @@ import com.vitorpamplona.amethyst.commons.resources.retry import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.FullscreenVideoView import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip56Reports.ReportType import kotlinx.coroutines.launch @@ -535,15 +535,13 @@ private fun BlossomBlobViewer( if (url != null && isVideo) { val controllerVisible = remember { mutableStateOf(true) } Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - VideoViewInner( + FullscreenVideoView( videoUri = url, mimeType = row.type, contentScale = ContentScale.Fit, - borderModifier = Modifier.fillMaxWidth(), - automaticallyStartPlayback = true, controllerVisible = controllerVisible, - isFullscreen = true, accountViewModel = accountViewModel, + modifier = Modifier.fillMaxWidth(), ) } } else if (url != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationPreviewMap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationPreviewMap.kt index 217e151d5f..746fce691d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationPreviewMap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationPreviewMap.kt @@ -47,8 +47,8 @@ import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker -/** Default close-up zoom for a single pinned location (street-level). */ -private const val DEFAULT_ZOOM = 16.0 +/** Default close-up zoom for a single pinned location (street-level), when the caller picks none. */ +const val DEFAULT_MAP_ZOOM = 16.0 /** * Night-mode filter for the (always-light) MAPNIK tiles so the map follows the @@ -145,7 +145,7 @@ fun LocationPreviewMap( latitude: Double, longitude: Double, modifier: Modifier = Modifier, - zoom: Double = DEFAULT_ZOOM, + zoom: Double = DEFAULT_MAP_ZOOM, aspectRatio: Float = 1f, pinColor: Color? = null, pinEmoji: String? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index 72aa4b9509..347e1ee356 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -50,10 +50,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.components.WaitAndDisplay import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose +import com.vitorpamplona.amethyst.commons.ui.note.platform.VideoView import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview @Composable @@ -79,7 +79,6 @@ fun PreviewUrl( myUrlPreview, mimeType = null, roundedCorner = false, - gallery = false, contentScale = ContentScale.FillHeight, accountViewModel = accountViewModel, ) @@ -133,7 +132,6 @@ fun PreviewUrlFillWidth( myUrlPreview, mimeType = null, roundedCorner = false, - gallery = false, contentScale = ContentScale.FillWidth, accountViewModel = accountViewModel, ) @@ -233,7 +231,6 @@ private fun MyLoadUrlPreviewDirect( state.previewInfo.url, mimeType = state.previewInfo.mimeType, roundedCorner = false, - gallery = false, contentScale = ContentScale.Crop, accountViewModel = accountViewModel, ) @@ -301,7 +298,6 @@ private fun MyLoadUrlPreviewDirectFillWidth( state.previewInfo.url, mimeType = state.previewInfo.mimeType, roundedCorner = false, - gallery = false, contentScale = ContentScale.FillWidth, accountViewModel = accountViewModel, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt index f887f9ff58..a7b311234e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt @@ -41,6 +41,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.platform.NotePlatform import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer +import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner +import com.vitorpamplona.amethyst.ui.note.creators.location.DEFAULT_MAP_ZOOM import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.podcasts.PodcastAudio @@ -206,9 +208,13 @@ object AndroidNotePlatform : NotePlatform { pinColor: Color?, pinEmoji: String?, pinAlpha: Float, + modifier: Modifier, + zoom: Double?, ) = AppLocationPreviewMap( latitude = latitude, longitude = longitude, + modifier = modifier, + zoom = zoom ?: DEFAULT_MAP_ZOOM, aspectRatio = aspectRatio, pinColor = pinColor, pinEmoji = pinEmoji, @@ -596,6 +602,25 @@ object AndroidNotePlatform : NotePlatform { callbacks: ZapButtonCallbacks, ) = AppReusableZapButton(baseNote, accountViewModel, nav, config, callbacks) + @Composable + override fun FullscreenVideoView( + videoUri: String, + mimeType: String?, + contentScale: ContentScale, + modifier: Modifier, + controllerVisible: MutableState, + accountViewModel: AccountViewModel, + ) = VideoViewInner( + videoUri = videoUri, + mimeType = mimeType, + contentScale = contentScale, + borderModifier = modifier, + automaticallyStartPlayback = true, + controllerVisible = controllerVisible, + isFullscreen = true, + accountViewModel = accountViewModel, + ) + @Composable override fun EditPostView( onClose: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt index 1fd6a4765a..e69b3f2660 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.syntheticWaveformFor import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note @@ -81,7 +82,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVi import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhenInBackground import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem -import com.vitorpamplona.amethyst.service.playback.composable.wavefront.syntheticWaveformFor import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastEpisodeAudioPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastEpisodeAudioPlayer.kt index cd419e24fb..b797fb8f64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastEpisodeAudioPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastEpisodeAudioPlayer.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.audio.WaveformData +import com.vitorpamplona.amethyst.commons.audio.syntheticWaveformFor import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.note.PodcastSoundbites import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastChaptersView @@ -38,7 +39,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhenInBackground import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem -import com.vitorpamplona.amethyst.service.playback.composable.wavefront.syntheticWaveformFor import com.vitorpamplona.quartz.podcasts.PodcastAudio import com.vitorpamplona.quartz.podcasts.PodcastEpisode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index d6017a041d..67ab2f71bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -37,8 +37,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.BlurhashBackdrop import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index 5c658ad8ce..7be9666345 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -67,12 +67,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.copyNoteTextAction import com.vitorpamplona.amethyst.commons.ui.note.elements.DropDownParams import com.vitorpamplona.amethyst.commons.ui.note.elements.observeBookmarksFollowsAndAccount import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote +import com.vitorpamplona.amethyst.commons.ui.note.platform.EditPostView import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index f5815f5152..d6eeb8c94a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.commons.ui.note.UserPicture +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -122,7 +123,6 @@ import com.vitorpamplona.amethyst.model.cordn.CordnMediaService import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessageRecorder -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt index ae7da2c626..70fd734ab7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt @@ -92,6 +92,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.NoteActionHandlers import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomSheet import com.vitorpamplona.amethyst.commons.ui.note.elements.noteActionSections import com.vitorpamplona.amethyst.commons.ui.note.elements.observeBookmarksFollowsAndAccount +import com.vitorpamplona.amethyst.commons.ui.note.platform.EditPostView import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint @@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox import com.vitorpamplona.amethyst.commons.ui.theme.selectedReactionBoxModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt index 95b955f564..67437f55b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderAudioWithWaveform +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWithWaveform import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaReferenceV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaV2 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index 27dad4cdbe..078f2f6b81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadDecryptedContentOrNull +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderAudioFromIMeta import com.vitorpamplona.amethyst.commons.ui.note.types.getAudioMetaWithWaveform import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.types.RenderAudioFromIMeta @Composable fun RenderRegularTextNote( 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 93d0c369e0..d1011cd08b 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 @@ -108,6 +108,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -124,7 +125,6 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index e22a0582ec..7f4806425f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -109,12 +109,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.note.platform.LocationPreviewMap import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index d99e892f54..4a9cca8365 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt @@ -104,11 +104,11 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji import com.vitorpamplona.amethyst.commons.ui.note.geocacheLabelRes +import com.vitorpamplona.amethyst.commons.ui.note.platform.LocationPreviewMap import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.tags.geohash.toGeoHash import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheGeohash import com.vitorpamplona.quartz.nipCCGeocaching.listing.tags.CacheSize diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt index 5cb6948c20..d997133943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt @@ -98,13 +98,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.GeocacheSpoilerHint import com.vitorpamplona.amethyst.commons.ui.note.forListing import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji import com.vitorpamplona.amethyst.commons.ui.note.geocachePoint +import com.vitorpamplona.amethyst.commons.ui.note.platform.LocationPreviewMap import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette import com.vitorpamplona.amethyst.commons.ui.note.specSummary import com.vitorpamplona.amethyst.commons.ui.note.types.distanceToCache import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent import com.vitorpamplona.quartz.nipCCGeocaching.listing.tags.TypeModifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt index cd3a8ed3d5..d6667840ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningRea import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index f6a1dd63f6..563182ba93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_season import com.vitorpamplona.amethyst.commons.resources.podcast_season_episode import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.note.platform.PodcastEpisodeAudioPlayer import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer import com.vitorpamplona.quartz.podcasts.PodcastEpisode private val PLAYER_SHAPE = Modifier.clip(RoundedCornerShape(12.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index 647bbdfc7c..5cdec7ba24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -46,10 +46,10 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner import com.vitorpamplona.amethyst.commons.resources.profile_image import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.util.setText +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableImageDialog import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index cc5d516355..c7c08b227f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -64,13 +64,13 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableImageDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size100dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt index 197918f1a6..ca053f5158 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt @@ -52,8 +52,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningRea import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip31Alts.alt 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 e22773d3c0..25abe6fa2c 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 @@ -144,6 +144,18 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayCommentScope import com.vitorpamplona.amethyst.commons.ui.note.observeEdits import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderAudioHeader +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderAudioTrack +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderGitIssueEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderGitPatchEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderGitPullRequestEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderGitPullRequestUpdateEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderGitRepositoryEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderMusicTrack +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderNamedSiteEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderRootSiteEvent +import com.vitorpamplona.amethyst.commons.ui.note.platform.RenderVoiceTrack +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBroadcastRelayList @@ -261,18 +273,6 @@ import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.types.AudioHeader -import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader -import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderGitPullRequestEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderGitPullRequestUpdateEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack -import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent -import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent -import com.vitorpamplona.amethyst.ui.note.types.VoiceHeader import com.vitorpamplona.quartz.cyberspace.CyberspaceBagEvent import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoAvatarEvent import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoObjectEvent @@ -894,7 +894,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is WorkoutRecordEvent) { WorkoutDisplay(baseNote, backgroundColor, canPreview = true, quotesLeft = 3, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is BaseVoiceEvent) { - VoiceHeader(noteEvent, baseNote, accountViewModel, nav) + RenderVoiceTrack(baseNote, accountViewModel, nav) } else if (noteEvent is FileMetadataEvent) { FileMetadataDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { @@ -906,9 +906,9 @@ private fun FullBleedNoteCompose( } else if (noteEvent is ContactListEvent) { DisplayContactList(baseNote, accountViewModel, nav) } else if (noteEvent is AudioTrackEvent) { - AudioTrackHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav) + RenderAudioTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) } else if (noteEvent is AudioHeaderEvent) { - AudioHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav) + RenderAudioHeader(baseNote, ContentScale.FillWidth, accountViewModel, nav) } else if (noteEvent is MusicTrackEvent) { RenderMusicTrack(baseNote, makeItShort = false, canPreview = true, backgroundColor, accountViewModel, nav) } else if (noteEvent is MusicPlaylistEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt index 0e42872c12..ef62cfc3f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt @@ -38,11 +38,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarP import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.LocalCurrentExternalScope +import com.vitorpamplona.amethyst.commons.ui.note.platform.rememberUrlPreviewState import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.rememberUrlPreviewState import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal.UrlFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssemblerSubscription import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt index aa5b9947f3..d70ab26391 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt @@ -46,10 +46,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningRea import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.note.types.FileMetadataAttachmentCard import com.vitorpamplona.amethyst.commons.ui.note.types.toMediaContent import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SyntheticWaveform.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SyntheticWaveform.kt index c4fcbd672c..d4128dbde2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SyntheticWaveform.kt @@ -18,9 +18,8 @@ * 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.service.playback.composable.wavefront +package com.vitorpamplona.amethyst.commons.audio -import com.vitorpamplona.amethyst.commons.audio.WaveformData import kotlin.math.PI import kotlin.math.sin import kotlin.random.Random diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt index 367dd82ee0..70db109d20 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt @@ -106,6 +106,20 @@ interface NotePlatform { accountViewModel: AccountViewModel, ) {} + /** + * A video filling a full-screen viewer, playing at once. [controllerVisible] shows and hides + * its playback controls, so the viewer can toggle them with its own chrome. + */ + @Composable + fun FullscreenVideoView( + videoUri: String, + mimeType: String?, + contentScale: ContentScale, + modifier: Modifier, + controllerVisible: MutableState, + accountViewModel: AccountViewModel, + ) {} + /** The link card for [url]: OpenGraph preview, inline media, or a plain link. */ @Composable fun UrlPreview( @@ -142,6 +156,8 @@ interface NotePlatform { pinColor: Color?, pinEmoji: String?, pinAlpha: Float, + modifier: Modifier, + zoom: Double?, ) {} /** Resolves [geohashStr] to a place name for [content], showing [onLoading] meanwhile. */ diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt index 30f946780d..a900b9e992 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt @@ -107,6 +107,16 @@ fun GifVideoView( thumbhash = thumbhash, ) +@Composable +fun FullscreenVideoView( + videoUri: String, + mimeType: String?, + contentScale: ContentScale, + controllerVisible: MutableState, + accountViewModel: AccountViewModel, + modifier: Modifier = Modifier, +) = LocalNotePlatform.current.FullscreenVideoView(videoUri, mimeType, contentScale, modifier, controllerVisible, accountViewModel) + @Composable fun VideoView( videoUri: String, @@ -151,6 +161,8 @@ fun LoadUrlPreview( fun LocationPreviewMap( latitude: Double, longitude: Double, + modifier: Modifier = Modifier, + zoom: Double? = null, aspectRatio: Float = 1f, pinColor: Color? = null, pinEmoji: String? = null, @@ -162,6 +174,8 @@ fun LocationPreviewMap( pinColor = pinColor, pinEmoji = pinEmoji, pinAlpha = pinAlpha, + modifier = modifier, + zoom = zoom, ) @Composable From d8c4682f1f1d43f932a71a32cee59b2586024a5e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 22:59:32 +0000 Subject: [PATCH 02/45] refactor: move the thread, bookmark group, URL feed and relay-group metadata screens The player side doors were their last exits. Moves 16 files to commonsUI (the thread feed view, bookmark group views and options, the URL feed and its filter/subscription, relay group metadata and its parent picker). ThreadFeedView parses prices with the KMP parseBigDecimalOrNull, as the classifieds card does. Records wave 7 and the player seam in the plan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/navigation/AppNavigation.kt | 10 +++--- .../relayGroup/RelayGroupChannelListScreen.kt | 2 +- .../relayGroup/RelayGroupSubgroupsBar.kt | 2 +- .../2026-09-27-one-ui-android-desktop.md | 33 +++++++++++++++---- .../bookmarkgroups/display/ArticleListView.kt | 2 +- .../display/BookmarkGroupItemOptions.kt | 3 +- .../display/BookmarkGroupScreen.kt | 6 ++-- .../display/BookmarkGroupViewModel.kt | 11 +++++-- .../bookmarkgroups/display/PostListView.kt | 2 +- .../relayGroup/RelayGroupMetadataScreen.kt | 8 ++--- .../relayGroup/RelayGroupMetadataViewModel.kt | 3 +- .../relayGroup/RelayGroupParentPicker.kt | 4 +-- .../RelayGroupsOnRelaySubscription.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 6 ++-- .../loggedIn/threadview/ThreadScreen.kt | 6 ++-- .../screen/loggedIn/url/NewUrlNoteButton.kt | 2 +- .../ui/screen/loggedIn/url/UrlScreen.kt | 10 +++--- .../screen/loggedIn/url/dal/UrlFeedFilter.kt | 2 +- .../loggedIn/url/dal/UrlFeedViewModel.kt | 9 +++-- .../UrlFilterAssemblerSubscription.kt | 2 +- 20 files changed, 79 insertions(+), 46 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/threadview/ThreadFeedView.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/threadview/ThreadScreen.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/NewUrlNoteButton.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/UrlScreen.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt (85%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt (95%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b7271fb601..b13bad6b32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.BadgesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.award.AwardBadgeScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.profile.ProfileBadgesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen @@ -100,7 +101,9 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupDiscoveryScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupEditScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen @@ -182,6 +185,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.Cord import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46SignerScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.ThreadScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.UrlScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddClinkDebitWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddNwcWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddWalletScreen @@ -230,7 +235,6 @@ import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzForumThreadScreen @@ -264,8 +268,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28P import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupEditScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen @@ -333,9 +335,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupS import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.ShortsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 324fd1b444..a405d37d85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.HiddenDmHeader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot @@ -121,7 +122,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzImportRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzRelayImportViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzWorkspaceOverflowMenu import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_DM import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_FORUM import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt index cded118fbb..6f70b39896 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt @@ -49,9 +49,9 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.quartz.nip29RelayGroups.GroupId /** Cap on how many child chips get their own warm-up subscription, to bound relay load. */ diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index cfdef91a50..7f5e5d7428 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -426,13 +426,32 @@ assemblers, `EventSync`, …). Packages are renamed on the way: image. They now call the shared `uploadToDefaultServer`. - Moved: the profile editor, the emoji pack screen and the emoji pack metadata screen (7 files). - - **Next:** most composer screens still exit through `ShowImageUploadItem` (the upload - preview, which uses Android media APIs to show a thumbnail) and `SelectFromFiles`. - After those comes the per-composer state: `ShortNotePostViewModel`, `PreviewState` - and the location pickers. The video player stack (media3) holds about 22 screens. - Smaller exits remain: the Android-only `AccountViewModel` actions, - `CalendarTimeFormat`, `NestUiState`, `NappletLauncher`, `UpdateZapAmountDialog` - (biometrics), the `java.time` pickers and the language list. + - **Wave 7 (2026-09-29):** + - The upload preview, the file picker and the single-document picker are shared. + - A shared wallet-app launcher, edge-to-edge dialog properties, `CashuWalletDiscovery` + in commonMain, the Namecoin resolver on `AppServices`, and `ReusableZapButton` as a + slot. + - Moved: the wallet screen, the discovery tabs, badges, communities, music playlists, + Concord create/edit, the Cashu wizard, podcast show/trailer, geocache log and the + visualizer settings. 136 of 252 screens shared. + - **Player seam (2026-09-30):** the media3 player stays in the app, like WebView. Shared + UI reaches it only through `NotePlatform` slots. + - Twenty-three screens used to reach it through side doors: direct calls to + `EditPostView`, `ZoomableContentView`, the audio/music/voice renderers and + `VideoViewInner`. They now call the slots, plus a new `FullscreenVideoView` slot. + - No screen reaches `service/playback` now. + - Moved: the thread screen, bookmark groups, the URL feed and relay-group metadata. + - **Later:** sharing the player *chrome* (voice/music/podcast rows, controls, waveform) + needs a `PlaybackController` port: state flows plus play/pause/seek/mute, wrapping the + pooled media3 `MediaController` on Android. Do it when Desktop gets a player, and pick + that engine through the license gate. VLCJ's README says "GPL, version 3 or later", + with no linking exception (a commercial license is sold separately), so it is a stop. + - **Next:** the remaining screens exit mostly through navigation (`RememberNavs`, `Nav`, + step 7). After that: `Amethyst.instance` reads, the relay exporters, `TakePicture`, + `ReactionsRow`, `ZoomableContentView`, `UrlCachedPreviewer`, `MLKitImageLabelService` + and `ShortNotePostViewModel`. Smaller exits remain too: the Android-only + `AccountViewModel` actions, `CalendarTimeFormat`, `NestUiState`, `NappletLauncher`, + `UpdateZapAmountDialog` (biometrics), the `java.time` pickers and the language list. 7. **Navigation**: the library swap, then `AppNavigation` + rail + drawer + bottom bar. 8. **The app root port** and the new JVM shim. Then the Desktop feature inventory, and retiring the old `desktopApp`. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt index 38524194f0..ae5f291491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.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.bookmarkgroups.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxSize diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index 7be9666345..662a141897 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.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.bookmarkgroups.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt index 837a4b38b1..3bd9c26c95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.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.bookmarkgroups.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -51,7 +51,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType @@ -71,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.DeletedItemsBanner import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar @@ -92,7 +92,7 @@ fun BookmarkGroupScreen( nav: INav, ) { val bookmarkGroupViewModel: BookmarkGroupViewModel = - viewModel( + rememberViewModel( factory = BookmarkGroupViewModel.Initializer(accountViewModel.account, bookmarkIdentifier), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt index 7d6a37fe6a..460f964cdc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt @@ -18,12 +18,13 @@ * 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.bookmarkgroups.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlin.reflect.KClass @Stable class BookmarkGroupViewModel( @@ -172,7 +174,10 @@ class BookmarkGroupViewModel( class Initializer( val account: Account, val bookmarkGroupIdentifier: String, - ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T = BookmarkGroupViewModel(account, bookmarkGroupIdentifier) as T + ) : ViewModelProvider.Factory { + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = BookmarkGroupViewModel(account, bookmarkGroupIdentifier) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt index 1deec14d08..323585295f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/display/PostListView.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.bookmarkgroups.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxSize diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index 7f4806425f..731036a82e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -63,7 +63,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -104,6 +103,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_section_discover import com.vitorpamplona.amethyst.commons.resources.relay_group_section_discovery_desc import com.vitorpamplona.amethyst.commons.resources.relay_group_section_permissions import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar @@ -133,7 +133,7 @@ fun RelayGroupCreateScreen( isForum: Boolean = false, ) { val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return - val viewModel: RelayGroupMetadataViewModel = viewModel(key = "RelayGroupCreate:$relayUrl") + val viewModel: RelayGroupMetadataViewModel = rememberViewModel(key = "RelayGroupCreate:$relayUrl") { RelayGroupMetadataViewModel() } LaunchedEffect(relay) { viewModel.initCreate(accountViewModel, relay) if (isForum) viewModel.isForum = true @@ -174,7 +174,7 @@ fun RelayGroupEditScreen( ) { val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val groupId = remember(id, relay) { GroupId(id, relay) } - val viewModel: RelayGroupMetadataViewModel = viewModel(key = "RelayGroupEdit:${groupId.toKey()}") + val viewModel: RelayGroupMetadataViewModel = rememberViewModel(key = "RelayGroupEdit:${groupId.toKey()}") { RelayGroupMetadataViewModel() } LoadRelayGroupChannel(groupId) { channel -> // Keep the relay-signed metadata fresh while editing so a late load prefills. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt index a5ded4051f..2ea29d8757 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt index dceeb4485d..5787f6b365 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background @@ -81,10 +81,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt index c93a040021..891b8c09b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsOnRelaySubscription.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.chats.publicChannels.relayGroup.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 25abe6fa2c..efab5cb903 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -20,7 +20,7 @@ */ @file:Suppress("DEPRECATION") -package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -396,8 +396,10 @@ import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.Podcasting20PodcastMetadata import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent +import com.vitorpamplona.quartz.utils.parseBigDecimalOrNull import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -1354,7 +1356,7 @@ private fun RenderClassifiedsReaderForThread( Modifier.padding(top = 5.dp), verticalAlignment = Alignment.CenterVertically, ) { - val newAmount = price.amount.toBigDecimalOrNull()?.let { showAmount(it) } ?: price.amount + val newAmount = parseBigDecimalOrNull(price.amount)?.let { showAmount(it) } ?: price.amount val priceTag = remember(noteEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadScreen.kt index e5edcfb2be..4703d2e733 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -18,15 +18,15 @@ * 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.threadview +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.thread_title import com.vitorpamplona.amethyst.commons.ui.components.LoadNote +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -45,7 +45,7 @@ fun ThreadScreen( if (noteId == null) return val feedViewModel: ThreadFeedViewModel = - viewModel( + rememberViewModel( key = noteId + "NostrThreadFeedViewModel", factory = ThreadFeedViewModel.Factory(accountViewModel.account, noteId), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/NewUrlNoteButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/NewUrlNoteButton.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/NewUrlNoteButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/NewUrlNoteButton.kt index 0b80f8cf5a..8fb4eca7e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/NewUrlNoteButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/NewUrlNoteButton.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.url +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlScreen.kt index ef62cfc3f7..73d1b4dd01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlScreen.kt @@ -18,19 +18,19 @@ * 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.url +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.external_content_title import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedLoaded import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold @@ -40,11 +40,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensible import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.LocalCurrentExternalScope import com.vitorpamplona.amethyst.commons.ui.note.platform.rememberUrlPreviewState import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.dal.UrlFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.datasource.UrlFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal.UrlFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssemblerSubscription import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId @Composable @@ -66,7 +66,7 @@ fun PrepareViewModelsUrlScreen( nav: INav, ) { val urlFeedViewModel: UrlFeedViewModel = - viewModel( + rememberViewModel( key = url + "UrlFeedViewModel", factory = UrlFeedViewModel.Factory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt index baf3b141ac..52d61bba97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedFilter.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.url.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt index e9890a34d8..251924ae71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/dal/UrlFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.url.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.reflect.KClass @Stable class UrlFeedViewModel( @@ -43,6 +45,9 @@ class UrlFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = UrlFeedViewModel(url, relays, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = UrlFeedViewModel(url, relays, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt index 4864613b42..4799138d57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.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.url.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember From d7a45732dc8c50203ddbc7eed05f76ed759e9cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 00:07:02 +0000 Subject: [PATCH 03/45] refactor: share relay exporters, zap amount text and composer nav seams - FileSharer expect/actual (Android zips into the cache and shares via the FileProvider); RelayExporter/RelayZipExporter/RelayListCollection move to commonsUI on TextSharer/FileSharer. - rememberExtendedNav moves to commonsUI; composer screens take INav. - ZapAmountText and the NWC finder subscription move to commonsUI; QuickZapAmountRow becomes a NotePlatform slot. - ChannelNewMessageViewModel mines PoW with a monotonic deadline (KMP). - BlossomServerFinder port on AppServices for MarmotGroupIconDisplay. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../compose/NotifyRequestDialog.kt | 2 +- .../ui/navigation/navs/RememberNavs.kt | 13 -- .../amethyst/ui/note/ReactionsRow.kt | 105 +------------ .../nip22Comments/GenericCommentPostScreen.kt | 8 +- .../ui/note/platform/AndroidNotePlatform.kt | 10 ++ .../ui/platform/AndroidAppServices.kt | 16 ++ .../chats/feed/ChatMessageActionSheet.kt | 108 +------------ .../loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../loggedIn/chats/feed/ChatReactionChips.kt | 2 +- .../loggedIn/chats/feed/QuickZapAmountRow.kt | 145 +++++++++++++++++ .../marmotGroup/MarmotGroupIconDisplay.kt | 11 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 5 +- .../send/ChannelNewMessageViewModel.kt | 9 +- .../nip23LongForm/LongFormPostScreen.kt | 6 +- .../nip99Classifieds/NewProductScreen.kt | 3 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 4 +- .../loggedIn/hashtag/HashtagPostScreen.kt | 4 +- .../loggedIn/highlights/NewHighlightScreen.kt | 4 +- .../loggedIn/home/ShortNotePostScreen.kt | 8 +- .../screen/loggedIn/home/VoiceReplyScreen.kt | 6 +- .../loggedIn/home/nip75Goals/NewGoalScreen.kt | 3 +- .../publicMessages/NewPublicMessageScreen.kt | 3 +- .../screen/loggedIn/polls/PollPostScreen.kt | 4 +- .../loggedIn/relays/AllRelayListScreen.kt | 16 +- .../relays/blocked/BlockedRelayListView.kt | 2 +- .../broadcast/BroadcastRelayListView.kt | 2 +- .../connected/ConnectedRelayListView.kt | 2 +- .../loggedIn/relays/dm/DMRelayListView.kt | 2 +- .../relays/feeds/RelayFeedsListView.kt | 2 +- .../relays/indexer/IndexerRelayListView.kt | 2 +- .../keyPackage/KeyPackageRelayListView.kt | 2 +- .../relays/local/LocalRelayListView.kt | 2 +- .../nip37/PrivateOutboxRelayListView.kt | 2 +- .../relays/nip65/Nip65RelayListView.kt | 2 +- .../relays/proxy/ProxyRelayListView.kt | 2 +- .../relays/search/SearchRelayListView.kt | 2 +- .../relays/trusted/TrustedRelayListView.kt | 2 +- .../ui/screen/loggedIn/url/UrlPostScreen.kt | 4 +- .../amethyst/commons/service/AppServices.kt | 3 + .../commons/service/BlossomServerFinder.kt | 40 +++++ .../ui/components/FileSharer.android.kt | 61 ++++++++ .../NWCFinderFilterAssemblerSubscription.kt | 2 +- .../commons/ui/components/FileSharer.kt | 43 ++++++ .../ui/navigation/navs/RememberExtendedNav.kt | 37 +++++ .../amethyst/commons/ui/note/ZapAmountText.kt | 146 ++++++++++++++++++ .../commons/ui/note/platform/NotePlatform.kt | 13 ++ .../ui/note/platform/NotePlatformCalls.kt | 9 ++ .../loggedIn/relays/common/RelayExporter.kt | 25 +-- .../relays/common/RelayListCollection.kt | 3 +- .../relays/common/RelayZipExporter.kt | 61 ++------ .../commons/ui/components/FileSharer.ios.kt | 29 ++++ .../commons/ui/components/FileSharer.jvm.kt | 29 ++++ 52 files changed, 669 insertions(+), 359 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/QuickZapAmountRow.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BlossomServerFinder.kt create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt (97%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/RememberExtendedNav.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapAmountText.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/common/RelayExporter.kt (76%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/common/RelayListCollection.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/common/RelayZipExporter.kt (50%) create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt index e268a9d483..a496e38c19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt @@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok import com.vitorpamplona.amethyst.commons.resources.notify_block_relay import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav @Composable fun NotifyRequestDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt index a9133a7c43..061581ce95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.compose.rememberNavController -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.ObservableNav @Composable fun rememberNav(): Nav { @@ -37,14 +35,3 @@ fun rememberNav(): Nav { Nav(navController, scope, ime) } } - -@Composable -fun rememberExtendedNav( - nav: INav, - onBeforeNavigate: () -> Unit, -): INav { - val scope = rememberCoroutineScope() - return remember(nav, scope) { - ObservableNav(nav, scope, onBeforeNavigate) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 88beb7be95..9c2cbc2076 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -66,7 +66,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Surface @@ -77,7 +76,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.SideEffect import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableLongStateOf @@ -95,7 +93,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -124,7 +121,6 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ReactionRowAction import com.vitorpamplona.amethyst.commons.model.ReactionRowItem import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.ZapraiserStatus import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes @@ -138,6 +134,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReposts import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteRepostsBy import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteZaps import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.boost @@ -164,7 +161,6 @@ import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts import com.vitorpamplona.amethyst.commons.resources.quote import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.resources.reload_mint_title -import com.vitorpamplona.amethyst.commons.resources.sats_to_complete import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox @@ -181,11 +177,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.ExpandMoreIcon import com.vitorpamplona.amethyst.commons.ui.note.LikeIcon import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.note.ObserveZapAmountText import com.vitorpamplona.amethyst.commons.ui.note.OutlinedZapIcon import com.vitorpamplona.amethyst.commons.ui.note.RenderBoostGallery import com.vitorpamplona.amethyst.commons.ui.note.RenderLikeGallery import com.vitorpamplona.amethyst.commons.ui.note.RenderReaction import com.vitorpamplona.amethyst.commons.ui.note.RenderZapGallery +import com.vitorpamplona.amethyst.commons.ui.note.RenderZapRaiser import com.vitorpamplona.amethyst.commons.ui.note.RepostIcon import com.vitorpamplona.amethyst.commons.ui.note.RepostedIcon import com.vitorpamplona.amethyst.commons.ui.note.ShareIcon @@ -203,10 +201,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.commons.ui.theme.Height24dpFilledModifier -import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpFilledModifier import com.vitorpamplona.amethyst.commons.ui.theme.ModifierWidth3dp -import com.vitorpamplona.amethyst.commons.ui.theme.NoSoTinyBorders import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowExpandButton import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeight import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeightWithPadding @@ -223,11 +218,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size28Modifier import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.commons.ui.theme.TinyBorders import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenDuration import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenFloatSpec import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenIntOffsetSpec -import com.vitorpamplona.amethyst.commons.ui.theme.fundraiserProgressColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp @@ -239,7 +232,6 @@ import com.vitorpamplona.amethyst.model.zap.CashuRailStatus import com.vitorpamplona.amethyst.model.zap.RailCapability import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog @@ -268,7 +260,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import kotlin.math.roundToInt import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid import androidx.compose.material3.Icon as Material3Icon @@ -524,57 +515,6 @@ fun LoadAndDisplayZapraiser( } } -@Composable -fun RenderZapRaiser( - baseNote: Note, - zapraiserAmount: Long, - details: Boolean, - accountViewModel: AccountViewModel, -) { - val zapsState by observeNoteZaps(baseNote, accountViewModel) - - var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) } - - LaunchedEffect(key1 = zapsState) { - zapsState?.note?.let { - val newStatus = accountViewModel.calculateZapraiser(baseNote) - if (zapraiserStatus != newStatus) { - zapraiserStatus = newStatus - } - } - } - - LinearProgressIndicator( - modifier = if (details) Height24dpFilledModifier else Height4dpFilledModifier, - color = MaterialTheme.colorScheme.fundraiserProgressColor, - progress = { zapraiserStatus.progress }, - gapSize = 0.dp, - strokeCap = StrokeCap.Square, - drawStopIndicator = {}, - ) - - if (details) { - Box( - contentAlignment = Center, - modifier = TinyBorders, - ) { - val totalPercentage by - remember(zapraiserStatus) { - derivedStateOf { "${(zapraiserStatus.progress * 100).roundToInt()}%" } - } - - Text( - text = - stringRes(id = Res.string.sats_to_complete, totalPercentage, zapraiserStatus.left), - modifier = NoSoTinyBorders, - // color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font14SP, - maxLines = 1, - ) - } - } -} - @Composable private fun WatchReactionsZapsBoostsAndDisplayIfExists( baseNote: Note, @@ -1668,45 +1608,6 @@ fun ObserveZapIcon( inner(wasZappedByLoggedInUser) } -@Composable -fun ObserveZapAmountText( - baseNote: Note, - accountViewModel: AccountViewModel, - inner: @Composable (String) -> Unit, -) { - val zapsState by observeNoteZaps(baseNote, accountViewModel) - - if (zapsState?.note?.zapPayments?.isNotEmpty() == true) { - zapsState?.note?.zapPayments?.forEach { - if (it.value == null) { - NWCFinderFilterAssemblerSubscription(it.key, accountViewModel) - } - } - - @Suppress("ProduceStateDoesNotAssignValue") - val zapAmountTxt by - produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) { - zapsState?.note?.let { - val newZapAmount = accountViewModel.calculateZapAmount(it) - if (value != newZapAmount) { - value = newZapAmount - } - } - } - - inner(zapAmountTxt) - } else { - // Include the signed-in user's own pending onchain zaps so - // the counter reflects the optimistic value the gallery shows. - val ownPubKey = accountViewModel.account.userProfile().pubkeyHex - val note = zapsState?.note - val total = - (note?.zapsAmount ?: java.math.BigDecimal(0)) + - java.math.BigDecimal(note?.extraOwnPendingOnchainSats(ownPubKey) ?: 0L) - inner(showAmount(total)) - } -} - @Composable private fun BoostTypeChoicePopup( baseNote: Note, 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 484b7aa166..167e17b837 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 @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose @@ -114,7 +115,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -144,7 +144,7 @@ fun ReplyCommentPostScreen( quoteId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) @@ -184,7 +184,7 @@ fun ReplyCommentPostScreen( fun GenericCommentPostScreen( postViewModel: CommentPostViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { WatchAndLoadMyEmojiList(accountViewModel) @@ -279,7 +279,7 @@ fun GenericCommentPostScreen( private fun GenericCommentPostBody( postViewModel: CommentPostViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val scrollState = rememberScrollState() val audienceLists = rememberAudienceLists(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt index a7b311234e..88f9dd929c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent as AppRende import com.vitorpamplona.amethyst.ui.note.types.RenderRootNappletEvent as AppRenderRootNappletEvent import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent as AppRenderRootSiteEvent import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack as AppRenderVoiceTrack +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.QuickZapAmountRow as AppQuickZapAmountRow /** Android's [NotePlatform]: the app's own media, map, audio, chess, git, napplet and zap composables. */ object AndroidNotePlatform : NotePlatform { @@ -621,6 +622,15 @@ object AndroidNotePlatform : NotePlatform { accountViewModel = accountViewModel, ) + @Composable + override fun QuickZapAmountRow( + note: Note, + onDismiss: () -> Unit, + onOnchainRequest: (Long?) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppQuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav) + @Composable override fun EditPostView( onClose: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index b31a64e385..6d66399b4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermi import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.AppServices +import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver @@ -49,4 +50,19 @@ object AndroidAppServices : AppServices { override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver + + override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder +} + +/** [BlossomServerFinder] over the app's BUD-10 resolver, read lazily (main process only). */ +private object AndroidBlossomServerFinder : BlossomServerFinder { + override fun cachedServerUrl(blossomUri: String): String? = + Amethyst.instance.blossomResolver + .cachedFindServer(blossomUri) + ?.serverUrl + + override suspend fun findServerUrl(blossomUri: String): String? = + Amethyst.instance.blossomResolver + .findServers(blossomUri) + ?.serverUrl } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt index 70fd734ab7..63f08bdc70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt @@ -49,7 +49,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -59,7 +58,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel @@ -69,16 +67,13 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_edit_message import com.vitorpamplona.amethyst.commons.resources.chat_delivery_details_title import com.vitorpamplona.amethyst.commons.resources.edit_draft import com.vitorpamplona.amethyst.commons.resources.edit_message -import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error import com.vitorpamplona.amethyst.commons.resources.more_options -import com.vitorpamplona.amethyst.commons.resources.no_wallet_found import com.vitorpamplona.amethyst.commons.resources.quick_action_delete_dialog_btn import com.vitorpamplona.amethyst.commons.resources.quick_action_request_deletion_alert_title import com.vitorpamplona.amethyst.commons.resources.relay_group_pin_message import com.vitorpamplona.amethyst.commons.resources.relay_group_unpin_message import com.vitorpamplona.amethyst.commons.resources.reply_description import com.vitorpamplona.amethyst.commons.resources.show_less -import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ChangeReactionIcon @@ -93,9 +88,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomShe import com.vitorpamplona.amethyst.commons.ui.note.elements.noteActionSections import com.vitorpamplona.amethyst.commons.ui.note.elements.observeBookmarksFollowsAndAccount import com.vitorpamplona.amethyst.commons.ui.note.platform.EditPostView +import com.vitorpamplona.amethyst.commons.ui.note.platform.QuickZapAmountRow import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog -import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size28Modifier @@ -104,17 +99,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox import com.vitorpamplona.amethyst.commons.ui.theme.selectedReactionBoxModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid -import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability -import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nipC7Chats.ChatEvent -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlin.uuid.ExperimentalUuidApi // null amount = open the on-chain dialog with no prefill. @Immutable @@ -622,98 +611,3 @@ private fun QuickReactionRow( } } } - -/** - * The user's zap presets unpacked as rail-aware amount chips (same grid the zap - * amount popup shows), firing directly from the sheet. Lightning/cashu zaps - * dismiss immediately — errors surface as toasts and the receipt lands on the - * bubble's sats chip; on-chain amounts hand off to the dialog hosted by the sheet. - */ -@OptIn(ExperimentalUuidApi::class) -@Composable -private fun QuickZapAmountRow( - note: Note, - onDismiss: () -> Unit, - onOnchainRequest: (Long?) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - val noWalletFoundStr = stringRes(Res.string.no_wallet_found) - val zapAmountChoices by - accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices - .collectAsStateWithLifecycle() - - val amountChoices = remember(zapAmountChoices) { zapAmountChoices.distinct().toImmutableList() } - if (amountChoices.isEmpty()) return - - val railCapability = - observeZapRailCapability( - baseNote = note, - accountViewModel = accountViewModel, - // Onchain zap events are public and would e-tag the rumor id. - onchainSupported = !note.isPrivateRumor(), - ) - - val context = LocalContext.current - - val onError = { _: String, message: String, user: User? -> - // Payment failed — drop the optimistic "zapping" indicator on the bubble. - accountViewModel.endZapInFlight(note.idHex) - accountViewModel.toastManager.toast(Res.string.error_dialog_zap_error, message, user) - } - - val onPayViaIntent = { payables: ImmutableList -> - // Handoff to an external wallet: we can't observe whether it completes, so clear - // the optimistic indicator rather than leave it spinning forever. - accountViewModel.endZapInFlight(note.idHex) - payViaIntentOrManualSplit(payables, context, noWalletFoundStr, accountViewModel, nav) - } - - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.Center, - ) { - ZapAmountChoiceGrid( - amountChoices = amountChoices, - railCapability = railCapability, - onLightningZap = { amountInSats -> - // Show a pending zap chip on the bubble immediately; it settles into the - // real sats chip once the receipt lands (or clears on error/timeout). - accountViewModel.markZapInFlight(note.idHex) - accountViewModel.zap( - note, - amountInSats * 1000, - null, - "", - true, - onError, - { }, - onPayViaIntent, - ) - onDismiss() - }, - onNutzap = { amountInSats -> - accountViewModel.markZapInFlight(note.idHex) - accountViewModel.sendNutzap( - baseNote = note, - amountSats = amountInSats, - message = "", - onError = onError, - onProgress = { }, - ) - onDismiss() - }, - onOnchainAmount = onOnchainRequest, - onReloadNutzap = { amount -> - navigateToReloadMint(accountViewModel, nav, note, amount) - onDismiss() - }, - onChangeAmount = { - nav.nav(Route.UpdateZapAmount()) - onDismiss() - }, - // Hands off to another app; the sheet must not stay stacked behind it. - onHandedOff = onDismiss, - ) - } -} 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 bb2cf0cb60..8117997bf7 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 @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.note.RenderZapRaiser import com.vitorpamplona.amethyst.commons.ui.note.WatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.DisplayZapSplits @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.RenderZapRaiser import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt index 9ffea11b26..6524f49c9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.chat_minichat_reply_count import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon +import com.vitorpamplona.amethyst.commons.ui.note.ObserveZapAmountText import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer @@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ObserveZapAmountText import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/QuickZapAmountRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/QuickZapAmountRow.kt new file mode 100644 index 0000000000..00e43f4b4a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/QuickZapAmountRow.kt @@ -0,0 +1,145 @@ +/* + * 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.chats.feed + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error +import com.vitorpamplona.amethyst.commons.resources.no_wallet_found +import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid +import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability +import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlin.uuid.ExperimentalUuidApi + +/** + * The user's zap presets unpacked as rail-aware amount chips (same grid the zap + * amount popup shows), firing directly from the sheet. Lightning/cashu zaps + * dismiss immediately — errors surface as toasts and the receipt lands on the + * bubble's sats chip; on-chain amounts hand off to the dialog hosted by the sheet. + */ +@OptIn(ExperimentalUuidApi::class) +@Composable +fun QuickZapAmountRow( + note: Note, + onDismiss: () -> Unit, + onOnchainRequest: (Long?) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noWalletFoundStr = stringRes(Res.string.no_wallet_found) + val zapAmountChoices by + accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices + .collectAsStateWithLifecycle() + + val amountChoices = remember(zapAmountChoices) { zapAmountChoices.distinct().toImmutableList() } + if (amountChoices.isEmpty()) return + + val railCapability = + observeZapRailCapability( + baseNote = note, + accountViewModel = accountViewModel, + // Onchain zap events are public and would e-tag the rumor id. + onchainSupported = !note.isPrivateRumor(), + ) + + val context = LocalContext.current + + val onError = { _: String, message: String, user: User? -> + // Payment failed — drop the optimistic "zapping" indicator on the bubble. + accountViewModel.endZapInFlight(note.idHex) + accountViewModel.toastManager.toast(Res.string.error_dialog_zap_error, message, user) + } + + val onPayViaIntent = { payables: ImmutableList -> + // Handoff to an external wallet: we can't observe whether it completes, so clear + // the optimistic indicator rather than leave it spinning forever. + accountViewModel.endZapInFlight(note.idHex) + payViaIntentOrManualSplit(payables, context, noWalletFoundStr, accountViewModel, nav) + } + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + ZapAmountChoiceGrid( + amountChoices = amountChoices, + railCapability = railCapability, + onLightningZap = { amountInSats -> + // Show a pending zap chip on the bubble immediately; it settles into the + // real sats chip once the receipt lands (or clears on error/timeout). + accountViewModel.markZapInFlight(note.idHex) + accountViewModel.zap( + note, + amountInSats * 1000, + null, + "", + true, + onError, + { }, + onPayViaIntent, + ) + onDismiss() + }, + onNutzap = { amountInSats -> + accountViewModel.markZapInFlight(note.idHex) + accountViewModel.sendNutzap( + baseNote = note, + amountSats = amountInSats, + message = "", + onError = onError, + onProgress = { }, + ) + onDismiss() + }, + onOnchainAmount = onOnchainRequest, + onReloadNutzap = { amount -> + navigateToReloadMint(accountViewModel, nav, note, amount) + onDismiss() + }, + onChangeAmount = { + nav.nav(Route.UpdateZapAmount()) + onDismiss() + }, + // Hands off to another app; the sheet must not stay stacked behind it. + onHandedOff = onDismiss, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt index 7707996ad9..ba1c9dd33f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt @@ -24,9 +24,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupImage import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo +import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.marmot.appComponents.GroupAvatarUrlV1 import com.vitorpamplona.quartz.marmot.appComponents.MarmotWebUrl @@ -43,7 +44,7 @@ import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri * * The blob is content-addressed on Blossom by [MarmotGroupImage.hash]; the canonical * scheme stores only the hash, and the icon usually lives on the *uploader's* Blossom - * server rather than the viewer's. So we resolve it through [Amethyst.blossomResolver], + * server rather than the viewer's. So we resolve it through the app's BUD-10 resolver ([BlossomServerFinder]), * which probes the viewer's default server (passed as a first-try `xs` hint) and the * group admins' configured servers (via their pubkeys as `as` authors, BUD-03). While * that async probe runs, we optimistically load from the viewer's default server so the @@ -77,9 +78,9 @@ fun rememberMarmotGroupIconUrl( ).toUriString() } - val resolver = Amethyst.instance.blossomResolver - val url by produceState(resolver.cachedFindServer(blossomUri)?.serverUrl ?: fallbackUrl, blossomUri) { - value = resolver.findServers(blossomUri)?.serverUrl ?: fallbackUrl + val finder = LocalAppServices.current.blossomServerFinder + val url by produceState(finder.cachedServerUrl(blossomUri) ?: fallbackUrl, blossomUri) { + value = finder.findServerUrl(blossomUri) ?: fallbackUrl } val cipher = remember(image) { MarmotGroupImageCipher(image.key, image.nonce) } 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 d1011cd08b..d930043923 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 @@ -125,7 +125,6 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest @@ -148,7 +147,7 @@ fun NewGroupDMScreen( message: String? = null, attachment: String? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { NewGroupDMScreen(message, attachment?.ifBlank { null }?.toUri(), accountViewModel, nav) } @@ -159,7 +158,7 @@ fun NewGroupDMScreen( message: String? = null, attachment: Uri? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: ChatNewMessageViewModel = viewModel() postViewModel.init(accountViewModel) 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 68ae70fdf0..da332f094c 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 @@ -136,6 +136,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource @Stable open class ChannelNewMessageViewModel : @@ -465,10 +467,9 @@ open class ChannelNewMessageViewModel : val mined = withContext(Dispatchers.Default) { // Bitchat mines 8 bits by default; cap the effort so a slow device still sends. - val deadline = System.nanoTime() + 2_000_000_000L - val threads = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) + val started = TimeSource.Monotonic.markNow() runCatching { - PoWMiner.mine(template, pubKeyHex, 8, threads, isActive = { System.nanoTime() < deadline }) + PoWMiner.mine(template, pubKeyHex, 8, account.powMinerWorkers(), isActive = { started.elapsedNow() < 2.seconds }) }.getOrDefault(template) } @@ -1051,7 +1052,7 @@ open class ChannelNewMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 14a038c7db..d7427acbc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton @@ -133,7 +134,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -150,7 +150,7 @@ fun LongFormPostScreen( draftId: HexKey? = null, versionId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: LongFormPostViewModel = viewModel() postViewModel.init(accountViewModel) @@ -214,7 +214,7 @@ fun LongFormPostScreen( private fun MarkdownPostScreenBody( postViewModel: LongFormPostViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val scrollState = rememberScrollState() Column( 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 b06be8a622..ca063ad739 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 @@ -80,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -103,7 +102,7 @@ fun NewProductScreen( quoteId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: NewProductViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index c5f9554506..1f552025d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -28,8 +28,8 @@ import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -47,7 +47,7 @@ fun GeoHashPostScreen( quoteId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index f82ffcca8b..680e6808d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -28,8 +28,8 @@ import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -47,7 +47,7 @@ fun HashtagPostScreen( quoteId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt index 8358b47b80..fbc32c1b3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.new_highlight_passage_placeh import com.vitorpamplona.amethyst.commons.resources.new_highlight_source_label import com.vitorpamplona.amethyst.commons.resources.new_highlight_title import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList @@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField @@ -111,7 +111,7 @@ fun NewHighlightScreen( sourceEventId: String? = null, author: String? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: NewHighlightPostViewModel = viewModel() postViewModel.init(accountViewModel) 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 de50091951..e8d6202d5f 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 @@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose @@ -156,7 +157,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -195,7 +195,7 @@ fun ShortNotePostScreen( groupThreadId: HexKey? = null, groupThreadRelayUrl: String? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: ShortNotePostViewModel = viewModel() postViewModel.init(accountViewModel) @@ -269,7 +269,7 @@ fun ShortNotePostScreen( internal fun NewPostScreenInner( postViewModel: ShortNotePostViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { WatchAndLoadMyEmojiList(accountViewModel) @@ -324,7 +324,7 @@ internal fun NewPostScreenInner( private fun NewPostScreenBody( postViewModel: ShortNotePostViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val scrollState = rememberScrollState() val audienceLists = rememberAudienceLists(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index f9b5232f26..43185481ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -62,7 +62,7 @@ fun VoiceReplyScreen( duration: Int, amplitudesJson: String, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val viewModel: VoiceReplyViewModel = viewModel() @@ -107,7 +107,7 @@ fun VoiceReplyScreen( private fun VoiceReplyScreenBody( viewModel: VoiceReplyViewModel, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val scrollState = rememberScrollState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt index fe49ecb469..8c7ca1af77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt @@ -64,13 +64,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewGoalScreen( accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val goalViewModel: NewGoalViewModel = viewModel() goalViewModel.init(accountViewModel) 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 b3b9bce7ae..6fb130a3de 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 @@ -94,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest @@ -118,7 +117,7 @@ fun NewPublicMessageScreen( replyId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: NewPublicMessageViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt index 40f7184fbc..adecdff72b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -39,7 +39,7 @@ fun PollPostScreen( message: String? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: ShortNotePostViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index abbf158297..9a4eba42dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList @@ -87,9 +86,14 @@ import com.vitorpamplona.amethyst.commons.resources.trusted_section_explainer import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayExporter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayListCollection +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -102,9 +106,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelay import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel @@ -621,7 +622,8 @@ fun SettingsCategoryWithButton( @Composable fun ExportDropdownMenu(collection: () -> RelayListCollection) { var expanded by remember { mutableStateOf(false) } - val context = LocalContext.current + val textSharer = rememberTextSharer() + val fileSharer = rememberFileSharer() val scope = rememberCoroutineScope() IconButton(onClick = { expanded = true }) { @@ -642,14 +644,14 @@ fun ExportDropdownMenu(collection: () -> RelayListCollection) { text = stringRes(Res.string.export_as_text), ) { expanded = false - scope.launch { RelayExporter(context).export(collection()) } + scope.launch { RelayExporter(textSharer).export(collection()) } } M3ActionRow( icon = MaterialSymbols.FolderZip, text = stringRes(Res.string.export_as_zip), ) { expanded = false - scope.launch { RelayZipExporter(context).export(collection()) } + scope.launch { RelayZipExporter(fileSharer).export(collection()) } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt index 976150bbe1..860c331302 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt index 127c8df427..1fed5e4fbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt index b926c58619..a2bbc82c0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt @@ -41,12 +41,12 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.active_subs_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 8b5680b539..44ce8b212d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt index 85949dcc17..3c29b4ddba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt index 5360bd9692..09314f3b30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt index 8b6e3ad4a5..29357db979 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index 3580d9c981..1b9fdab81a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_description import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -47,7 +48,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingWithHorzBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index 05e0fa25a9..283f6e4ed2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 36e17be0e1..1c5f851d10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt index bb6f268bb5..bbfc8e3108 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index 0b7cdd27a1..9e4355857b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt index c96d2c8707..4f3a197c56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt index 1d10a9eab3..03bd66c9cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -28,8 +28,8 @@ import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -47,7 +47,7 @@ fun UrlPostScreen( quoteId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index 37ca48c1a4..d13b4f0e0e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -53,4 +53,7 @@ interface AppServices { /** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */ val namecoinResolver: NamecoinNameResolver + + /** Finds the server behind a `blossom:` URI (BUD-10). */ + val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BlossomServerFinder.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BlossomServerFinder.kt new file mode 100644 index 0000000000..8e74f719a9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/BlossomServerFinder.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.service + +/** + * Resolves a BUD-10 `blossom:` URI to the URL of a server that holds the blob, probing the + * author's (and the viewer's) Blossom servers. + */ +interface BlossomServerFinder { + /** The server URL already resolved for [blossomUri], without probing. */ + fun cachedServerUrl(blossomUri: String): String? + + /** Probes until a server answers for [blossomUri]; null when none does. */ + suspend fun findServerUrl(blossomUri: String): String? + + /** Resolves nothing: front ends without a Blossom client yet. */ + object None : BlossomServerFinder { + override fun cachedServerUrl(blossomUri: String): String? = null + + override suspend fun findServerUrl(blossomUri: String): String? = null + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt new file mode 100644 index 0000000000..6066b0f73c --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt @@ -0,0 +1,61 @@ +/* + * 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.commons.ui.components + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.FileProvider +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +@Composable +actual fun rememberFileSharer(): FileSharer { + val context = LocalContext.current + return remember(context) { + FileSharer { zipName, files, title -> + val zipFile = File(context.cacheDir, zipName) + ZipOutputStream(FileOutputStream(zipFile)).use { zip -> + files.forEach { (name, content) -> + zip.putNextEntry(ZipEntry(name)) + zip.write(content.toByteArray()) + zip.closeEntry() + } + } + + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "application/zip" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_TITLE, title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity(Intent.createChooser(sendIntent, title)) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index e0b580ef74..ea54209973 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.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.service.relayClient.reqCommand.nwc +package com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt new file mode 100644 index 0000000000..544103e9cc --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt @@ -0,0 +1,43 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +/** Hands generated files to the platform's share sheet. */ +fun interface FileSharer { + /** + * Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a + * chooser titled [title] where the platform has one. + */ + fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) +} + +/** + * The platform's file sharing: Android writes the zip to its cache and shares it through the + * app's `FileProvider`. Desktop and iOS have no share sheet wired yet and do nothing. + */ +@Composable +expect fun rememberFileSharer(): FileSharer diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/RememberExtendedNav.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/RememberExtendedNav.kt new file mode 100644 index 0000000000..9fafd83a95 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/RememberExtendedNav.kt @@ -0,0 +1,37 @@ +/* + * 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.commons.ui.navigation.navs + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope + +/** [nav], calling [onBeforeNavigate] first on every navigation (to close a sheet or dialog). */ +@Composable +fun rememberExtendedNav( + nav: INav, + onBeforeNavigate: () -> Unit, +): INav { + val scope = rememberCoroutineScope() + return remember(nav, scope) { + ObservableNav(nav, scope, onBeforeNavigate) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapAmountText.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapAmountText.kt new file mode 100644 index 0000000000..4c5e02f31e --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapAmountText.kt @@ -0,0 +1,146 @@ +/* + * 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.commons.ui.note + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.ZapraiserStatus +import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteZaps +import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.sats_to_complete +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP +import com.vitorpamplona.amethyst.commons.ui.theme.Height24dpFilledModifier +import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpFilledModifier +import com.vitorpamplona.amethyst.commons.ui.theme.NoSoTinyBorders +import com.vitorpamplona.amethyst.commons.ui.theme.TinyBorders +import com.vitorpamplona.amethyst.commons.ui.theme.fundraiserProgressColor +import com.vitorpamplona.amethyst.commons.util.showAmount +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount +import com.vitorpamplona.quartz.utils.BigDecimal +import com.vitorpamplona.quartz.utils.plus +import kotlin.math.roundToInt + +@Composable +fun ObserveZapAmountText( + baseNote: Note, + accountViewModel: AccountViewModel, + inner: @Composable (String) -> Unit, +) { + val zapsState by observeNoteZaps(baseNote, accountViewModel) + + if (zapsState?.note?.zapPayments?.isNotEmpty() == true) { + zapsState?.note?.zapPayments?.forEach { + if (it.value == null) { + NWCFinderFilterAssemblerSubscription(it.key, accountViewModel) + } + } + + @Suppress("ProduceStateDoesNotAssignValue") + val zapAmountTxt by + produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) { + zapsState?.note?.let { + val newZapAmount = accountViewModel.calculateZapAmount(it) + if (value != newZapAmount) { + value = newZapAmount + } + } + } + + inner(zapAmountTxt) + } else { + // Include the signed-in user's own pending onchain zaps so + // the counter reflects the optimistic value the gallery shows. + val ownPubKey = accountViewModel.account.userProfile().pubkeyHex + val note = zapsState?.note + val total = + (note?.zapsAmount ?: BigDecimal(0)) + + BigDecimal(note?.extraOwnPendingOnchainSats(ownPubKey) ?: 0L) + inner(showAmount(total)) + } +} + +@Composable +fun RenderZapRaiser( + baseNote: Note, + zapraiserAmount: Long, + details: Boolean, + accountViewModel: AccountViewModel, +) { + val zapsState by observeNoteZaps(baseNote, accountViewModel) + + var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) } + + LaunchedEffect(key1 = zapsState) { + zapsState?.note?.let { + val newStatus = accountViewModel.calculateZapraiser(baseNote) + if (zapraiserStatus != newStatus) { + zapraiserStatus = newStatus + } + } + } + + LinearProgressIndicator( + modifier = if (details) Height24dpFilledModifier else Height4dpFilledModifier, + color = MaterialTheme.colorScheme.fundraiserProgressColor, + progress = { zapraiserStatus.progress }, + gapSize = 0.dp, + strokeCap = StrokeCap.Square, + drawStopIndicator = {}, + ) + + if (details) { + Box( + contentAlignment = Center, + modifier = TinyBorders, + ) { + val totalPercentage by + remember(zapraiserStatus) { + derivedStateOf { "${(zapraiserStatus.progress * 100).roundToInt()}%" } + } + + Text( + text = + stringRes(id = Res.string.sats_to_complete, totalPercentage, zapraiserStatus.left), + modifier = NoSoTinyBorders, + // color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font14SP, + maxLines = 1, + ) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt index 70db109d20..a6a0f8cb73 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt @@ -426,6 +426,19 @@ interface NotePlatform { callbacks: ZapButtonCallbacks, ) {} + /** + * The user's zap presets as rail-aware amount chips that zap [note] straight away (the chat + * long-press sheet). [onOnchainRequest] hands an on-chain amount to the caller's dialog. + */ + @Composable + fun QuickZapAmountRow( + note: Note, + onDismiss: () -> Unit, + onOnchainRequest: (Long?) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + /** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */ @Composable fun EditPostView( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt index a900b9e992..32c38eb268 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt @@ -559,6 +559,15 @@ fun ReusableZapButton( callbacks: ZapButtonCallbacks = ZapButtonCallbacks(), ) = LocalNotePlatform.current.ReusableZapButton(baseNote, accountViewModel, nav, config, callbacks) +@Composable +fun QuickZapAmountRow( + note: Note, + onDismiss: () -> Unit, + onOnchainRequest: (Long?) -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.QuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav) + @Composable fun EditPostView( onClose: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayExporter.kt similarity index 76% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayExporter.kt index 0b05718e60..de849afe4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayExporter.kt @@ -18,35 +18,20 @@ * 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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common -import android.content.Context -import android.content.Intent import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.export_relay_settings import com.vitorpamplona.amethyst.commons.resources.relay_settings +import com.vitorpamplona.amethyst.commons.ui.components.TextSharer import com.vitorpamplona.amethyst.commons.ui.loadStringRes class RelayExporter( - val context: Context, + val sharer: TextSharer, ) { suspend fun export(collection: RelayListCollection) { - val text = buildExportText(collection) - - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, text) - putExtra(Intent.EXTRA_TITLE, loadStringRes(Res.string.export_relay_settings)) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - loadStringRes(Res.string.export_relay_settings), - ) - context.startActivity(shareIntent) + val title = loadStringRes(Res.string.export_relay_settings) + sharer.share(buildExportText(collection), title, title) } suspend fun buildExportText(collection: RelayListCollection): String { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayListCollection.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayListCollection.kt index e8e986423e..1e525bbae6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayListCollection.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.blocked_section @@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.resources.search_section import com.vitorpamplona.amethyst.commons.resources.search_section_explainer import com.vitorpamplona.amethyst.commons.resources.trusted_section import com.vitorpamplona.amethyst.commons.resources.trusted_section_explainer -import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import org.jetbrains.compose.resources.StringResource data class RelayListCollection( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayZipExporter.kt similarity index 50% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayZipExporter.kt index 5bb043eb06..b98af43c39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayZipExporter.kt @@ -18,67 +18,30 @@ * 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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common -import android.content.Context -import android.content.Intent -import androidx.core.content.FileProvider import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.export_relay_settings +import com.vitorpamplona.amethyst.commons.ui.components.FileSharer import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream +/** Exports each non-empty relay section as `
.json` (an array of URLs) in one zip. */ class RelayZipExporter( - val context: Context, + val sharer: FileSharer, ) { suspend fun export(collection: RelayListCollection) { - val zipFile = buildZipFile(collection) - - val uri = - FileProvider.getUriForFile( - context, - "${context.packageName}.provider", - zipFile, - ) - - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "application/zip" - putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_TITLE, loadStringRes(Res.string.export_relay_settings)) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - loadStringRes(Res.string.export_relay_settings), - ) - context.startActivity(shareIntent) + sharer.shareTextFilesAsZip( + zipName = "relay_settings.zip", + files = buildFiles(collection), + title = loadStringRes(Res.string.export_relay_settings), + ) } - fun buildZipFile(collection: RelayListCollection): File { - val zipFile = File(context.cacheDir, "relay_settings.zip") - - ZipOutputStream(FileOutputStream(zipFile)).use { zip -> - collection.sections().forEach { section -> - if (section.relays.isNotEmpty()) { - val json = buildJsonArray(section.relays) - zip.putNextEntry(ZipEntry("${section.fileName}.json")) - zip.write(json.toByteArray()) - zip.closeEntry() - } - } + fun buildFiles(collection: RelayListCollection): List> = + collection.sections().mapNotNull { section -> + if (section.relays.isEmpty()) null else "${section.fileName}.json" to buildJsonArray(section.relays) } - return zipFile - } - private fun buildJsonArray(relays: List): String { val builder = StringBuilder() builder.appendLine("[") diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt new file mode 100644 index 0000000000..34b5b9ff77 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +private val NoFileSharer = FileSharer { _, _, _ -> } + +/** No share sheet wired yet. */ +@Composable +actual fun rememberFileSharer(): FileSharer = NoFileSharer diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt new file mode 100644 index 0000000000..34b5b9ff77 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +private val NoFileSharer = FileSharer { _, _, _ -> } + +/** No share sheet wired yet. */ +@Composable +actual fun rememberFileSharer(): FileSharer = NoFileSharer From 56274d23ef7eaffa92e981b3d40ffa82fa6452cb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 00:43:05 +0000 Subject: [PATCH 04/45] refactor: move the chat message row, relay settings, share-to-DM and goal screens - 64 files freed by the previous seams move to commonsUI: the chat feed row and its action/reaction sheets, AllRelayListScreen and every relay-list view + view model, ShareToDMScreen, CordnGroupListScreen, NewEphemeralChatScreen, NewGoalScreen, PaymentTargetsScreen and ActiveSubscriptionsScreen. - TakePicture/TakeVideo become expect/actual; the Android actual keeps the camera intent + FileProvider flow, desktop and iOS hide the buttons. - The chat sheet helpers the Cordn row reuses become public. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../actions/uploads/FileProviderPathsTest.kt | 2 + .../bolt12Offers/Bolt12OffersScreen.kt | 2 +- .../bolt12Offers/Bolt12OffersViewModel.kt | 2 +- .../nestsServers/NestsServersScreen.kt | 4 +- .../amethyst/ui/navigation/AppNavigation.kt | 14 +- .../nip22Comments/GenericCommentPostScreen.kt | 4 +- .../metadata/BookmarkGroupMetadataScreen.kt | 2 +- .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 2 +- .../chats/cordnGroup/CordnMessageRow.kt | 18 +-- .../loggedIn/chats/feed/ChatFeedView.kt | 1 + .../chats/geohashChat/GeohashChatScreen.kt | 10 +- .../marmotGroup/MarmotGroupChatScreen.kt | 1 + .../chats/marmotGroup/MarmotGroupChatView.kt | 2 +- .../marmotGroup/MarmotGroupIconEditor.kt | 1 + .../marmotGroup/MarmotGroupInfoScreen.kt | 2 + .../marmotGroup/MarmotGroupListScreen.kt | 2 + .../loggedIn/chats/minichat/MinichatScreen.kt | 4 +- .../loggedIn/chats/privateDM/ChatroomView.kt | 2 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +- .../publicChannels/ephemChat/ChannelView.kt | 1 + .../ephemChat/EphemeralChatScreen.kt | 1 + .../metadata/ChannelMetadataScreen.kt | 4 +- .../relayGroup/RelayGroupChannelListScreen.kt | 2 +- .../chats/rooms/feed/ChatroomListTabs.kt | 1 + .../chats/utils/DisplayReplyingToNote.kt | 2 +- .../nip23LongForm/LongFormPostScreen.kt | 4 +- .../nip99Classifieds/NewProductScreen.kt | 4 +- .../loggedIn/home/AddOutboxRelayListDialog.kt | 4 +- .../loggedIn/home/ShortNotePostScreen.kt | 4 +- .../list/metadata/FollowPackMetadataScreen.kt | 2 +- .../list/metadata/PeopleListMetadataScreen.kt | 2 +- .../loggedIn/longs/NewLongVideoButton.kt | 2 +- .../loggedIn/nests/room/chat/NestChatPanel.kt | 2 +- .../nests/room/lobby/NestLobbyScreen.kt | 2 +- .../notifications/AddInboxRelayListDialog.kt | 4 +- .../loggedIn/notifications/CardFeedView.kt | 2 +- .../publicMessages/NewPublicMessageScreen.kt | 4 +- .../loggedIn/pictures/NewPictureButton.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 2 +- .../relays/dm/AddDMRelayListDialog.kt | 2 + .../relays/vanish/RequestToVanishScreen.kt | 2 +- .../search/AddSearchRelayListDialog.kt | 4 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 2 +- .../loggedIn/settings/NIP47SetupScreen.kt | 6 +- .../loggedIn/shorts/NewShortVideoButton.kt | 2 +- .../loggedIn/video/NewVideoFeedButton.kt | 4 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 2 +- .../AggregateSubscriptionsTest.kt | 1 + commonsUI/build.gradle.kts | 2 + .../ui/actions/uploads/TakePicture.android.kt | 78 +----------- .../paymentTargets/PaymentTargetsScreen.kt | 8 +- .../paymentTargets/PaymentTargetsViewModel.kt | 2 +- .../commons/ui/actions/uploads/TakePicture.kt | 120 ++++++++++++++++++ .../components/DefaultInlineQuoteRenderer.kt | 2 +- .../chats/cordnGroup/CordnGroupListScreen.kt | 4 +- .../loggedIn/chats/feed/ChatDeliveryTicks.kt | 2 +- .../chats/feed/ChatEngagementDetailSheet.kt | 2 +- .../chats/feed/ChatInlineQuoteRenderer.kt | 4 +- .../chats/feed/ChatMessageActionSheet.kt | 10 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 32 ++--- .../loggedIn/chats/feed/ChatReactionChips.kt | 8 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 4 +- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 2 +- .../loggedIn/chats/feed/LoadingReplyNote.kt | 2 +- .../chats/feed/types/RenderBuzzNotes.kt | 2 +- .../feed/types/RenderChannelAdminNote.kt | 2 +- .../chats/feed/types/RenderChatRaid.kt | 2 +- .../chats/feed/types/RenderChatZap.kt | 2 +- .../chats/feed/types/RenderDraftEvent.kt | 6 +- .../chats/feed/types/RenderEditedNote.kt | 2 +- .../feed/types/RenderMarmotEncryptedMedia.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../marmotGroup/MarmotGroupIconDisplay.kt | 2 +- .../chats/marmotGroup/MarmotGroupPreview.kt | 8 +- .../privateDM/header/DmReportWarningCard.kt | 2 +- .../ephemChat/LoadEphemeralChatChannel.kt | 2 +- .../metadata/NewEphemeralChatMetaViewModel.kt | 2 +- .../metadata/NewEphemeralChatScreen.kt | 10 +- .../chats/rooms/ChatroomHeaderCompose.kt | 18 +-- .../chats/rooms/feed/ChatroomListFeedView.kt | 4 +- .../chats/share/ShareDMRoomsFeedFilter.kt | 2 +- .../loggedIn/chats/share/ShareToDMScreen.kt | 4 +- .../loggedIn/home/nip75Goals/NewGoalScreen.kt | 6 +- .../home/nip75Goals/NewGoalViewModel.kt | 2 +- .../loggedIn/relays/AllRelayListScreen.kt | 84 ++++++------ .../relays/blocked/BlockedRelayListView.kt | 4 +- .../blocked/BlockedRelayListViewModel.kt | 4 +- .../broadcast/BroadcastRelayListView.kt | 4 +- .../broadcast/BroadcastRelayListViewModel.kt | 4 +- .../common/BasicRelaySetupInfoDialog.kt | 2 +- .../relays/common/BasicRelaySetupInfoModel.kt | 3 +- .../connected/ConnectedRelayListView.kt | 4 +- .../connected/ConnectedRelayListViewModel.kt | 6 +- .../loggedIn/relays/dm/DMRelayListView.kt | 4 +- .../relays/dm/DMRelayListViewModel.kt | 4 +- .../feeds/FavoriteRelayListViewModel.kt | 4 +- .../relays/feeds/RelayFeedsListView.kt | 4 +- .../relays/indexer/IndexerRelayListView.kt | 4 +- .../indexer/IndexerRelayListViewModel.kt | 4 +- .../keyPackage/KeyPackageRelayListView.kt | 4 +- .../KeyPackageRelayListViewModel.kt | 4 +- .../relays/local/LocalRelayListView.kt | 4 +- .../relays/local/LocalRelayListViewModel.kt | 4 +- .../nip37/PrivateOutboxRelayListView.kt | 4 +- .../nip37/PrivateOutboxRelayListViewModel.kt | 4 +- .../relays/nip65/Nip65RelayListView.kt | 4 +- .../relays/nip65/Nip65RelayListViewModel.kt | 3 +- .../relays/proxy/ProxyRelayListView.kt | 4 +- .../relays/proxy/ProxyRelayListViewModel.kt | 4 +- .../relays/search/SearchRelayListView.kt | 4 +- .../relays/search/SearchRelayListViewModel.kt | 4 +- .../ActiveSubscriptionsScreen.kt | 10 +- .../ActiveSubscriptionsViewModel.kt | 2 +- .../relays/trusted/TrustedRelayListView.kt | 4 +- .../trusted/TrustedRelayListViewModel.kt | 4 +- .../ui/actions/uploads/TakePicture.ios.kt | 41 ++++++ .../ui/actions/uploads/TakePicture.jvm.kt | 41 ++++++ 117 files changed, 478 insertions(+), 329 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt => commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.android.kt (76%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/paymentTargets/PaymentTargetsScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/paymentTargets/PaymentTargetsViewModel.kt (98%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/InlineQuoteRenderer.kt => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/DefaultInlineQuoteRenderer.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt (91%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/AllRelayListScreen.kt (86%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/dm/DMRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/local/LocalRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/search/SearchRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt (89%) create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.jvm.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/actions/uploads/FileProviderPathsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/actions/uploads/FileProviderPathsTest.kt index ae096595f1..0bb6202786 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/actions/uploads/FileProviderPathsTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/actions/uploads/FileProviderPathsTest.kt @@ -24,6 +24,8 @@ import android.os.Environment import androidx.core.content.FileProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.getPhotoUri +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.getVideoUri import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt index 61f916c933..115cf97b94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory @Composable fun Bolt12OffersScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt index bf3c50e88e..4ea27a19aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.launch /** * Edits the logged-in user's NIP-B1 BOLT12 offer list (kind 10058). Mirrors - * [com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsViewModel]; + * [com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetsViewModel]; * each entry is a canonical raw `lno1...` offer string. */ @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt index d72db6f0ea..50262e6e54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt @@ -72,6 +72,8 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_use_defaults import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertPadding @@ -84,8 +86,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton import com.vitorpamplona.quartz.nip01Core.tags.references.HttpUrlFormatter import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b13bad6b32..8c2bdb2732 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri_description import com.vitorpamplona.amethyst.commons.ui.actions.NewUserMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetsScreen import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator @@ -91,6 +92,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScree import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen @@ -100,12 +102,14 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteLinksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordMembersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupDiscoveryScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupEditScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen @@ -129,6 +133,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.metadata.InterestSetMetadataScreen @@ -159,9 +164,11 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ShowQRScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.redirect.LoadRedirectScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.RelayFeedScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relayauth.RelayAuthSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.AllRelayListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.RelayInformationScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.eventsync.EventSyncScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip43.RelayMembersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions.ActiveSubscriptionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.VanishEventsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen @@ -216,7 +223,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomBlobManagerScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomImportScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.DisplayBlossomSyncProgress -import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsScreen import com.vitorpamplona.amethyst.ui.broadcast.DisplayBroadcastProgress import com.vitorpamplona.amethyst.ui.call.CallActivity import com.vitorpamplona.amethyst.ui.components.getActivity @@ -247,7 +253,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEv import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen @@ -262,14 +267,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScr import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen @@ -296,7 +299,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.NewHighlightScre import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen @@ -316,9 +318,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions.ActiveSubscriptionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen 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 167e17b837..03f99c89ff 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 @@ -81,6 +81,8 @@ import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDial import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -112,8 +114,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt index 719bbfe4eb..c5af527e5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt @@ -58,12 +58,12 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index 528deed877..5144a2deef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -60,10 +60,10 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count import com.vitorpamplona.amethyst.commons.resources.relay_group_no_messages_yet import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.quartz.nip29RelayGroups.GroupId /** A first screen's worth of recent messages to prefetch per visible card, so previews fill in. */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index b789cd5a8e..740aa35c9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -99,6 +99,14 @@ import com.vitorpamplona.amethyst.commons.ui.note.QuickActionAlertDialog import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ActionTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatChipFlowRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.MoreActionsToggle +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ReactionChip +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ReactionChipView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.SectionDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.TileRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.authorNameColorFor import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -109,14 +117,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ActionTile -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatChipFlowRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.MoreActionsToggle -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ReactionChip -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ReactionChipView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.SectionDivider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.TileRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.authorNameColorFor import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnAnnotationIndex import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessage @@ -143,7 +143,7 @@ import kotlin.math.abs * opened a menu and nothing could be swiped. * * What cordn cannot share is the *content* of those slots. The shared fillings - * ([com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatReactionChips], + * ([com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatReactionChips], * `ChatMessageActionSheet`, `ChatMessageFooter`, `DrawAuthorInfo`) all take a `Note`, * and a `Note` comes from `LocalCache` — which nothing in a cordn room may enter, see * the screen's KDoc. So the slots are filled from the envelope and the annotation fold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 4ce029ed54..a21898eb65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.SaveableFeedState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index 5b1590c891..e3b13ec255 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt @@ -75,12 +75,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarActi import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatReactOverride +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatShowSelfAuthorName +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatSuppressGeohash import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatReactOverride -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatShowSelfAuthorName -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatSuppressGeohash import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt index fa57d7efda..fcd934f5f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt @@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.DisplayUserSetAsSubject import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index b902700071..746d735fe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatIsOneOnOne import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -84,7 +85,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatIsOneOnOne import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index bb320241ca..3323bad523 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupIconUrl import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 26bc045929..99eb376ac8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -139,6 +139,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserS import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotGroupTitle +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index 6fae96011f..ead2bc081e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -84,6 +84,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotGroupPreviewText +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.DisplayUserSetAsSubject import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index f54529cd63..523647dcb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -71,6 +71,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalSuppressReplyToNoteId import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder @@ -79,8 +81,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalSuppressReplyToNoteId import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index b12fe8797e..c8023478bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.DmReportWarningCard import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisp import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.DmReportWarningCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow import com.vitorpamplona.quartz.nip01Core.core.HexKey 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 d930043923..e0790fb1df 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 @@ -90,6 +90,8 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -122,8 +124,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 98b236ba66..5d56b38b0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 1613bbfec7..2361f1486e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index c42a93fa4a..1f1ff69fe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -69,6 +69,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -80,8 +82,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index a405d37d85..d227ec69ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -111,6 +111,7 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.HiddenDmHeader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -121,7 +122,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzAddPeopleDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzImportRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzRelayImportViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzWorkspaceOverflowMenu -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_DM import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_FORUM import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 1f5b5d2314..aa9c4ebe6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.zonedDrawerSwipeIfModal +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.feed.ChatroomListFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index 129fb8df84..d60b71de7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose @Composable fun DisplayReplyingToNote( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index d7427acbc0..35b625f97a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -104,6 +104,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -130,8 +132,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList 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 ca063ad739..81cab99ca7 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 @@ -55,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDial import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -78,8 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt index 34932d3318..942fc7dd47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt @@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.resources.outbox_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65OutboxRelayList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65OutboxRelayList -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable 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 e8d6202d5f..1fd8054efb 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 @@ -112,6 +112,8 @@ import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDial import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -150,8 +152,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt index 79579f1835..db7782814f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt index 4d2a4b3ecb..c5c9fea019 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt index 72ac47929c..35d7d74d7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt index 33273adf90..d1ede67cef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt @@ -48,11 +48,11 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_chat_empty +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.BouncingIntentNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt index dbdf4cdf2d..c52dd74e07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt @@ -85,10 +85,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceClosedFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceOpenFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePlannedFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePrivateFlag +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt index 5b6ac5f244..807c74558d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt @@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.resources.inbox_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65InboxRelayList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65InboxRelayList -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 96d8811bec..42ff519994 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -168,7 +168,7 @@ fun RenderCardFeed( * * The padding is applied here because only the `Loaded` branch has a `LazyColumn` to carry the * scaffold's inset as content padding; without it the header would draw under the disappearing top bar. - * Same shape [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.ChatroomListFeedView] uses. + * Same shape [com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.feed.ChatroomListFeedView] uses. */ @Composable private fun HeaderAbove( 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 6fb130a3de..3fa4b8d0ba 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 @@ -62,6 +62,8 @@ import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDial import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -91,8 +93,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt index bf80d12fbb..aa81f029da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 9bd8dff391..815f999872 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.resources.public_notif_section_explain import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.RelayCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.MyRelayInfo import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index fa587725a0..35c0c20484 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt @@ -47,6 +47,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm.DMRelayList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt index 6c2b5e7171..ef923afa9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt @@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index bb8fe5a062..6ab9ffd93e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt @@ -47,12 +47,12 @@ import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search.SearchRelayList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayList -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 2e0c06fa66..1417b7553f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow import com.vitorpamplona.amethyst.commons.ui.search.GroupCandidate import com.vitorpamplona.amethyst.commons.ui.search.SEARCH_PICKER_LIMIT @@ -147,7 +148,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 111c7bf346..e960d9d1e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -50,8 +50,11 @@ import com.vitorpamplona.amethyst.commons.resources.no_payment_targets_message import com.vitorpamplona.amethyst.commons.resources.payment_targets import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer import com.vitorpamplona.amethyst.commons.resources.wallet_connect +import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetAddField +import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetsViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -59,11 +62,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifi import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetAddField -import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsViewModel import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index 234c89f792..d87da1be98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -62,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt index fa0450900f..81868ac4d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt @@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -62,8 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture -import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 04a3631842..13abace69b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.model.AccentColorType import com.vitorpamplona.amethyst.commons.model.FontFamilyType import com.vitorpamplona.amethyst.commons.model.FontSizeType import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.components.LocalTranslationPlatform import com.vitorpamplona.amethyst.commons.ui.note.platform.LocalNotePlatform @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.isDarkTheme import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground import com.vitorpamplona.amethyst.ui.components.AndroidRichTextPlatform -import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.amethyst.ui.components.FlavorTranslationPlatform import com.vitorpamplona.amethyst.ui.note.platform.AndroidNotePlatform import com.vitorpamplona.amethyst.ui.platform.AndroidAppPlatform diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/AggregateSubscriptionsTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/AggregateSubscriptionsTest.kt index 87b3beca2f..c97b8c2673 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/AggregateSubscriptionsTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/AggregateSubscriptionsTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions.aggregateSubscriptions import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import org.junit.Assert.assertEquals diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 53b1c20463..1551fcaeff 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -214,6 +214,8 @@ kotlin { implementation(libs.androidx.core.ktx) // BackHandler, behind the shared PlatformBackHandler. Apache-2.0, already in the app. implementation(libs.androidx.activity.compose) + // Camera permission prompt for TakePicture. Apache-2.0, already in the app. + implementation(libs.accompanist.permissions) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.android.kt similarity index 76% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.android.kt index ecade0bf8c..dc50335ae3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.android.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import android.Manifest import android.content.ActivityNotFoundException @@ -29,9 +29,6 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.height -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -39,22 +36,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.no_camera_app_found -import com.vitorpamplona.amethyst.commons.resources.record_a_video -import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -65,26 +55,11 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -@Composable -fun TakePictureButton(onPictureTaken: (ImmutableList) -> Unit) { - var showCamera by remember { mutableStateOf(false) } - if (showCamera) { - TakePicture( - onPictureTaken = { uri -> - showCamera = false - if (uri.isNotEmpty()) { - onPictureTaken(uri) - } - }, - ) - } - - PictureButton { showCamera = true } -} +actual val canCaptureFromCamera: Boolean = true @OptIn(ExperimentalPermissionsApi::class) @Composable -fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) { +actual fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) { val context = LocalContext.current var cameraUri by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() @@ -134,20 +109,6 @@ fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) { } } -@Composable -fun PictureButton(onClick: () -> Unit) { - IconButton( - onClick = onClick, - ) { - Icon( - symbol = MaterialSymbols.CameraAlt, - contentDescription = stringRes(id = Res.string.take_a_picture), - modifier = Modifier.height(22.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } -} - /** * Launches the camera capture intent, switching to the main thread so the * [ActivityResultLauncher] is invoked safely. Devices without a camera app throw @@ -178,26 +139,9 @@ fun getPhotoUri(context: Context): Uri = storageDir = Environment.DIRECTORY_PICTURES, ) -@Composable -fun TakeVideoButton(onVideoTaken: (ImmutableList) -> Unit) { - var showCamera by remember { mutableStateOf(false) } - if (showCamera) { - TakeVideo( - onVideoTaken = { uri -> - showCamera = false - if (uri.isNotEmpty()) { - onVideoTaken(uri) - } - }, - ) - } - - VideoButton { showCamera = true } -} - @OptIn(ExperimentalPermissionsApi::class) @Composable -fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) { +actual fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) { val context = LocalContext.current var videoUri by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() @@ -247,20 +191,6 @@ fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) { } } -@Composable -fun VideoButton(onClick: () -> Unit) { - IconButton( - onClick = onClick, - ) { - Icon( - symbol = MaterialSymbols.Videocam, - contentDescription = stringRes(id = Res.string.record_a_video), - modifier = Modifier.height(22.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } -} - fun getVideoUri(context: Context): Uri = getMediaUri( context = context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsScreen.kt index 1ff0fb807a..722e91734b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsScreen.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.actions.paymentTargets +package com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -49,7 +49,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -61,9 +60,11 @@ import com.vitorpamplona.amethyst.commons.resources.payment_target_type import com.vitorpamplona.amethyst.commons.resources.payment_targets import com.vitorpamplona.amethyst.commons.resources.payment_targets_explainer import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable @@ -81,7 +81,7 @@ fun PaymentTargetsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: PaymentTargetsViewModel = viewModel() + val viewModel: PaymentTargetsViewModel = rememberViewModel { PaymentTargetsViewModel() } viewModel.init(accountViewModel) LaunchedEffect(key1 = accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsViewModel.kt index d7850cb550..5f0b2eeda7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/paymentTargets/PaymentTargetsViewModel.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.actions.paymentTargets +package com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.kt new file mode 100644 index 0000000000..402335134d --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.kt @@ -0,0 +1,120 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.foundation.layout.height +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.record_a_video +import com.vitorpamplona.amethyst.commons.resources.take_a_picture +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.stringRes +import kotlinx.collections.immutable.ImmutableList + +/** Whether this platform can capture a photo or video from a camera app. */ +expect val canCaptureFromCamera: Boolean + +/** + * Asks for the camera permission if needed, then opens the camera to take a picture. Reports the + * captured media, or an empty list when the user cancels or no camera app is available. + */ +@Composable +expect fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) + +/** Like [TakePicture], but records a video. */ +@Composable +expect fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) + +@Composable +fun TakePictureButton(onPictureTaken: (ImmutableList) -> Unit) { + if (!canCaptureFromCamera) return + + var showCamera by remember { mutableStateOf(false) } + if (showCamera) { + TakePicture( + onPictureTaken = { uri -> + showCamera = false + if (uri.isNotEmpty()) { + onPictureTaken(uri) + } + }, + ) + } + + PictureButton { showCamera = true } +} + +@Composable +fun PictureButton(onClick: () -> Unit) { + IconButton( + onClick = onClick, + ) { + Icon( + symbol = MaterialSymbols.CameraAlt, + contentDescription = stringRes(id = Res.string.take_a_picture), + modifier = Modifier.height(22.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } +} + +@Composable +fun TakeVideoButton(onVideoTaken: (ImmutableList) -> Unit) { + if (!canCaptureFromCamera) return + + var showCamera by remember { mutableStateOf(false) } + if (showCamera) { + TakeVideo( + onVideoTaken = { uri -> + showCamera = false + if (uri.isNotEmpty()) { + onVideoTaken(uri) + } + }, + ) + } + + VideoButton { showCamera = true } +} + +@Composable +fun VideoButton(onClick: () -> Unit) { + IconButton( + onClick = onClick, + ) { + Icon( + symbol = MaterialSymbols.Videocam, + contentDescription = stringRes(id = Res.string.record_a_video), + modifier = Modifier.height(22.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/InlineQuoteRenderer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/DefaultInlineQuoteRenderer.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/InlineQuoteRenderer.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/DefaultInlineQuoteRenderer.kt index 728be96715..0e576a45b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/InlineQuoteRenderer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/DefaultInlineQuoteRenderer.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.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.material3.MaterialTheme import com.vitorpamplona.amethyst.commons.ui.components.InlineQuoteRenderer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt index 68c10c0a45..09385a0814 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.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.chats.cordnGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -65,11 +65,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.CordnGroupRoomCompose import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.CordnGroupRoomCompose import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt index f2f0000f73..0085d4f870 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt index e856d62f1a..619c6f2100 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatEngagementDetailSheet.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt index 7578d88e8a..7df8f8fcaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatInlineQuoteRenderer.kt @@ -18,12 +18,12 @@ * 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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.components.InlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent -import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt index 63f08bdc70..2aa9cf9a64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -430,7 +430,7 @@ private fun RelayGroupPinTile( * zap, or a reply) and reveals everything else on demand. */ @Composable -internal fun MoreActionsToggle( +fun MoreActionsToggle( expanded: Boolean, onToggle: () -> Unit, ) { @@ -492,7 +492,7 @@ private fun ChatOnlyRow( @OptIn(ExperimentalLayoutApi::class) @Composable -internal fun TileRow(content: @Composable () -> Unit) { +fun TileRow(content: @Composable () -> Unit) { FlowRow( modifier = Modifier @@ -506,7 +506,7 @@ internal fun TileRow(content: @Composable () -> Unit) { } @Composable -internal fun ActionTile( +fun ActionTile( symbol: MaterialSymbol, label: String, isDestructive: Boolean = false, @@ -545,7 +545,7 @@ internal fun ActionTile( private const val SECTION_DIVIDER_ALPHA = 0.5f @Composable -internal fun SectionDivider() { +fun SectionDivider() { HorizontalDivider( thickness = DividerThickness, color = MaterialTheme.colorScheme.placeholderText.copy(alpha = SECTION_DIVIDER_ALPHA), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 8117997bf7..9179271d71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatMessageCompose.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -60,28 +60,28 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderZapRaiser import com.vitorpamplona.amethyst.commons.ui.note.WatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.DisplayZapSplits +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzForumVote import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzSystemMessage +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChannelAdminSystemMessage import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatClip +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatRaid +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatZap import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderConcordTimerNotice +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderEditedNote import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderEncryptedMediaV2 +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderMarmotEncryptedMedia +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderRegularTextNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2 +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasMip04Media +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.isBuzzActivityRow import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzForumVote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChannelAdminSystemMessage -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatRaid -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatZap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEditedNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedMediaV2 -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderMarmotEncryptedMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderRegularTextNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2 -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasMip04Media -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.isBuzzActivityRow import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt index 6524f49c9f..fc965758f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatReactionChips.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatReactionChips.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi @@ -83,7 +83,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Immutable -internal data class ReactionChip( +data class ReactionChip( val type: String, val count: Int, val includesMe: Boolean, @@ -219,7 +219,7 @@ fun ChatReactionChips( */ @OptIn(ExperimentalLayoutApi::class) @Composable -internal fun ChatChipFlowRow(content: @Composable () -> Unit) { +fun ChatChipFlowRow(content: @Composable () -> Unit) { FlowRow( // Inset from the bubble's edge so overlapping chips ride the border without // poking past the bubble's rounded corners. @@ -302,7 +302,7 @@ private fun MinichatChip( @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun ReactionChipView( +fun ReactionChipView( chip: ReactionChip, onClick: () -> Unit, onLongClick: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 246d683152..63d5606454 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatTimeAgo.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -29,7 +29,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -182,7 +181,6 @@ fun ChatDisplayExpiration(expirationDate: Long) { modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.placeholderText, ) - val context = LocalContext.current Text( text = timeAheadNoDot(expirationDate), color = MaterialTheme.colorScheme.placeholderText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index 12efb2524c..897ffca77f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt index 836da6cfc3..eebc9220b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/LoadingReplyNote.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt index c8285ca31f..4fae3c37e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt index 9d076d82e9..f9b83e2d80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt index 36295d5eac..12130f105b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt index eb9dc3f248..919f592563 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt index 746c729d19..8c53ae57ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ObserveDraftEvent +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.NoteRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RenderReplyRow import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.NoteRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RenderReplyRow @Composable fun RenderDraftEvent( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt index 867c082f6b..710d18733f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderEditedNote.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt index 67437f55b2..69ba4e25cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderMarmotEncryptedMedia.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index 078f2f6b81..5599015351 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.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.chats.feed.types +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt index ba1c9dd33f..992b6311e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconDisplay.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt index 69445c053b..951a760f58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupPreview.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.key @@ -33,12 +33,12 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_preview_group_updated import com.vitorpamplona.amethyst.commons.resources.marmot_preview_media import com.vitorpamplona.amethyst.commons.resources.marmot_preview_no_text import com.vitorpamplona.amethyst.commons.resources.marmot_preview_with_sender +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.observeChatEdit +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2 +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasMip04Media import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.observeChatEdit -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2 -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasMip04Media import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotAppEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt index 25ad963b7f..ba583191f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.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.chats.privateDM.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt index 8673f99314..50124d18fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.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.chats.publicChannels.ephemChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt index c5f8910f74..35f3dda694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.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.chats.publicChannels.ephemChat.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt index 1650680780..87796afadd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.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.chats.publicChannels.ephemChat.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -39,7 +39,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.channel_name @@ -48,11 +47,13 @@ import com.vitorpamplona.amethyst.commons.resources.my_awesome_group import com.vitorpamplona.amethyst.commons.resources.relay_chat import com.vitorpamplona.amethyst.commons.resources.relay_chat_explainer import com.vitorpamplona.amethyst.commons.resources.relay_chat_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier @@ -60,14 +61,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory @Composable fun NewEphemeralChatScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: NewEphemeralChatMetaViewModel = viewModel() + val postViewModel: NewEphemeralChatMetaViewModel = rememberViewModel { NewEphemeralChatMetaViewModel() } postViewModel.load(accountViewModel.account) ChannelMetadataScaffold( @@ -81,7 +81,7 @@ fun NewEphemeralChatScreen( @Composable private fun DialogContentPreview() { val accountViewModel = mockAccountViewModel() - val postViewModel: NewEphemeralChatMetaViewModel = viewModel() + val postViewModel: NewEphemeralChatMetaViewModel = rememberViewModel { NewEphemeralChatMetaViewModel() } postViewModel.load(accountViewModel.account) ThemeComparisonColumn { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 664d75f72a..74a4e8a1f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.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.chats.rooms +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope @@ -134,9 +134,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotGroupPreviewText +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotGroupTitle +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier @@ -148,14 +156,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupPreviewText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupTitle -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 8a536c9a44..9e81bd344e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.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.chats.rooms.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.feed import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Column @@ -61,12 +61,12 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.util.JavaSerializable import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt index 734cd2d143..d99a782b35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareDMRoomsFeedFilter.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.chats.share +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt index dc72134862..1213930ea0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/share/ShareToDMScreen.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.chats.share +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -45,10 +45,10 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.ShareToDMNav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.feed.ChatroomListFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.ChatroomListFeedView @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt index 8c7ca1af77..c5a0a9701c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.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.home.nip75Goals +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -44,7 +44,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.goal_amount_label import com.vitorpamplona.amethyst.commons.resources.goal_amount_placeholder @@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.goal_website_label import com.vitorpamplona.amethyst.commons.resources.goal_website_placeholder import com.vitorpamplona.amethyst.commons.resources.new_goal import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -71,7 +71,7 @@ fun NewGoalScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val goalViewModel: NewGoalViewModel = viewModel() + val goalViewModel: NewGoalViewModel = rememberViewModel { NewGoalViewModel() } goalViewModel.init(accountViewModel) LaunchedEffect(goalViewModel, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt index 04b437e14c..9b54d5afa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.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.home.nip75Goals +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/AllRelayListScreen.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 9a4eba42dc..40e505bf97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/AllRelayListScreen.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.relays +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -41,7 +41,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList @@ -88,13 +87,41 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.blocked.renderBlockedItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.connected.renderConnectedItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm.DMRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm.renderDMItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.feeds.FavoriteRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.feeds.renderRelayFeedsItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.indexer.IndexerRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.indexer.renderIndexerItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.keyPackage.KeyPackageRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.keyPackage.renderKeyPackageItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.local.LocalRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.local.renderLocalItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip37.renderPrivateOutboxItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.renderNip65HomeItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.proxy.ProxyRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.proxy.renderProxyItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search.SearchRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search.renderSearchItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.trusted.renderTrustedItems import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing @@ -102,33 +129,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstWithHorz import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingWithHorzBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.feeds.FavoriteRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.feeds.renderRelayFeedsItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.IndexerRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.renderIndexerItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.keyPackage.KeyPackageRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.keyPackage.renderKeyPackageItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.renderLocalItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.renderPrivateOutboxItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65HomeItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy.ProxyRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy.renderProxyItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource @@ -137,19 +137,19 @@ fun AllRelayListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val dmViewModel: DMRelayListViewModel = viewModel() - val keyPackageViewModel: KeyPackageRelayListViewModel = viewModel() - val nip65ViewModel: Nip65RelayListViewModel = viewModel() - val privateOutboxViewModel: PrivateOutboxRelayListViewModel = viewModel() - val searchViewModel: SearchRelayListViewModel = viewModel() - val blockedViewModel: BlockedRelayListViewModel = viewModel() - val trustedViewModel: TrustedRelayListViewModel = viewModel() - val localViewModel: LocalRelayListViewModel = viewModel() - val connectedViewModel: ConnectedRelayListViewModel = viewModel() - val broadcastViewModel: BroadcastRelayListViewModel = viewModel() - val indexerViewModel: IndexerRelayListViewModel = viewModel() - val proxyViewModel: ProxyRelayListViewModel = viewModel() - val relayFeedsViewModel: FavoriteRelayListViewModel = viewModel() + val dmViewModel: DMRelayListViewModel = rememberViewModel { DMRelayListViewModel() } + val keyPackageViewModel: KeyPackageRelayListViewModel = rememberViewModel { KeyPackageRelayListViewModel() } + val nip65ViewModel: Nip65RelayListViewModel = rememberViewModel { Nip65RelayListViewModel() } + val privateOutboxViewModel: PrivateOutboxRelayListViewModel = rememberViewModel { PrivateOutboxRelayListViewModel() } + val searchViewModel: SearchRelayListViewModel = rememberViewModel { SearchRelayListViewModel() } + val blockedViewModel: BlockedRelayListViewModel = rememberViewModel { BlockedRelayListViewModel() } + val trustedViewModel: TrustedRelayListViewModel = rememberViewModel { TrustedRelayListViewModel() } + val localViewModel: LocalRelayListViewModel = rememberViewModel { LocalRelayListViewModel() } + val connectedViewModel: ConnectedRelayListViewModel = rememberViewModel { ConnectedRelayListViewModel() } + val broadcastViewModel: BroadcastRelayListViewModel = rememberViewModel { BroadcastRelayListViewModel() } + val indexerViewModel: IndexerRelayListViewModel = rememberViewModel { IndexerRelayListViewModel() } + val proxyViewModel: ProxyRelayListViewModel = rememberViewModel { ProxyRelayListViewModel() } + val relayFeedsViewModel: FavoriteRelayListViewModel = rememberViewModel { FavoriteRelayListViewModel() } dmViewModel.init(accountViewModel) keyPackageViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt index 860c331302..b7f21fcdf3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.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.relays.blocked +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.blocked import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun BlockedRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt index abaf3faabf..1d24a52475 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.blocked +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.blocked import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt index 1fed5e4fbc..78b4907020 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.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.relays.broadcast +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.broadcast import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun BroadcastRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt index 1c94de0f69..0f211b9c10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.broadcast +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.broadcast import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 43964a6b3a..c9cbac07bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index b91d5bb6fe..cfb3ecf069 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.count import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt index a2bbc82c0d..ac8b50d5c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.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.relays.connected +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.connected import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row @@ -43,11 +43,11 @@ import com.vitorpamplona.amethyst.commons.resources.active_subs_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun ConnectedRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index 23092ef734..378e6c75db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -18,12 +18,12 @@ * 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.relays.connected +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.connected import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip56Reports.ReportEvent @@ -38,7 +38,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { val reqs = account.client.activeRequests(it) val users = mutableSetOf() - reqs.forEach { _, filters -> + reqs.forEach { (_, filters) -> filters.forEach { filter -> val isReportFilter = filter.kinds?.size == 1 && filter.kinds?.firstOrNull() == ReportEvent.KIND if (!isReportFilter) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 44ce8b212d..9700f3f3aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListView.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.relays.dm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index d0f5ac593c..b120e0676b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -18,13 +18,13 @@ * 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.relays.dm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.dms -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt index 91fd7cf9a6..e80ed850f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.feeds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt index 3c29b4ddba..0418f5ce35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.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.relays.feeds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.feeds import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun FavoriteRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt index 09314f3b30..5a575c1129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.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.relays.indexer +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.indexer import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt index d5325d8e91..276295ba75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt @@ -18,14 +18,14 @@ * 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.relays.indexer +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.indexer import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profiles import com.vitorpamplona.amethyst.commons.resources.relay_settings_lower2 -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt index 29357db979..c810bdc9ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.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.relays.keyPackage +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.keyPackage import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt index f39543adc6..eef3dd2c06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListViewModel.kt @@ -18,13 +18,13 @@ * 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.relays.keyPackage +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.keyPackage import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.keypackages -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index 1b9fdab81a..43a923af7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListView.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.relays.local +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.local import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_ti import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow @@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingWithHorzBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun LocalRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt index 0a4df4eef5..460508c7dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.local +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.local import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index 283f6e4ed2..5bb3f2cbbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.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.relays.nip37 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip37 import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt index 58cdb381ec..5b61a847ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt @@ -18,13 +18,13 @@ * 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.relays.nip37 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip37 import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.events_from_you -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 1c5f851d10..5cf4e6f315 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.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.relays.nip65 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65 import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 5f7fa37608..328da7d207 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.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.relays.nip65 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65 import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt index bbfc8e3108..2bfb6edb0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.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.relays.proxy +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.proxy import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt index e2a0f13596..8215c71019 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.proxy +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.proxy import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index 9e4355857b..cf18b99bbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListView.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.relays.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt index a40047e6b5..8386043f83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt @@ -18,13 +18,13 @@ * 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.relays.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.search import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.searchable_events -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index 488797bcfc..d68b7e4634 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.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.relays.subscriptions +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background @@ -57,7 +57,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo @@ -90,20 +89,21 @@ import com.vitorpamplona.amethyst.commons.resources.active_subs_unattributed import com.vitorpamplona.amethyst.commons.resources.active_subs_untagged import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -125,7 +125,7 @@ import kotlin.math.roundToInt fun ActiveSubscriptionsScreen( accountViewModel: AccountViewModel, nav: INav, - viewModel: ActiveSubscriptionsViewModel = viewModel(), + viewModel: ActiveSubscriptionsViewModel = rememberViewModel { ActiveSubscriptionsViewModel() }, ) { LaunchedEffect(Unit) { viewModel.pollWhileShown(accountViewModel.account.client) } val state by viewModel.state.collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt index 5e79ff23c3..359c897119 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.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.relays.subscriptions +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt index 4f3a197c56..176ee2f50f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.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.relays.trusted +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.trusted import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable fun TrustedRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt index c6b46496e7..0de2ebc3f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt @@ -18,10 +18,10 @@ * 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.relays.trusted +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.trusted import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.ios.kt new file mode 100644 index 0000000000..44675f239c --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.ios.kt @@ -0,0 +1,41 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +// No camera capture wired on this platform yet: the buttons hide, and a direct call reports a cancel. + +actual val canCaptureFromCamera: Boolean = false + +@Composable +actual fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onPictureTaken(persistentListOf()) } +} + +@Composable +actual fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onVideoTaken(persistentListOf()) } +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.jvm.kt new file mode 100644 index 0000000000..44675f239c --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/TakePicture.jvm.kt @@ -0,0 +1,41 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +// No camera capture wired on this platform yet: the buttons hide, and a direct call reports a cancel. + +actual val canCaptureFromCamera: Boolean = false + +@Composable +actual fun TakePicture(onPictureTaken: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onPictureTaken(persistentListOf()) } +} + +@Composable +actual fun TakeVideo(onVideoTaken: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onVideoTaken(persistentListOf()) } +} From 035ed020e461eacb0597e35872940c5242419cad Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 01:03:41 +0000 Subject: [PATCH 05/45] refactor: composer seams for location, expiration, message field and imeta - DeviceLocation port in commons (LocationState implements it), reached through AccountViewModelHost and AppServices; composers and geohash screens stop reading Amethyst.instance.locationManager. - rememberCoarseLocationPermission expect/actual (accompanist on Android, denied elsewhere) replaces direct permission-state calls. - ExpirationDatePicker drops java.time/android.text for LocalClock's new utcOffsetSeconds and a rememberIs24HourClock expect. - MessageField takes MediaUri; PreviewState moves to commonMain. - MediaUploader.remoteFileHeader replaces IMetaAttachments' MimeTypeMap + OkHttp download. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../service/location/LocationState.kt | 9 ++-- .../service/uploads/AndroidMediaUploader.kt | 13 +++++ .../topbars/AroundMeLocationLabel.kt | 4 +- .../expiration/ExpirationDatePicker.kt | 29 ++++++----- .../location/DisplayLocationObserver.kt | 1 + .../location/GeohashLocationPickerDialog.kt | 5 +- .../creators/location/ILocationGrabber.kt | 4 +- .../note/creators/location/LocationAsHash.kt | 19 +++---- .../creators/messagefield/MessageField.kt | 4 +- .../nip22Comments/CommentPostViewModel.kt | 9 ++-- .../ui/platform/AndroidAppServices.kt | 3 ++ .../loggedIn/AndroidAccountViewModelHost.kt | 3 ++ .../ui/screen/loggedIn/LoggedInPage.kt | 10 ++-- .../chats/geohashChat/GeohashChatScreen.kt | 19 +++---- .../chats/geohashChat/NewGeohashChatScreen.kt | 21 ++++---- .../privateDM/send/ChatNewMessageViewModel.kt | 9 ++-- .../chats/privateDM/send/IMetaAttachments.kt | 15 ++---- .../send/ChannelNewMessageViewModel.kt | 9 ++-- .../nip23LongForm/LongFormPostViewModel.kt | 9 ++-- .../nip99Classifieds/NewProductViewModel.kt | 9 ++-- .../ui/screen/loggedIn/home/HomeScreen.kt | 4 +- .../loggedIn/home/ShortNotePostViewModel.kt | 8 ++- .../room/chat/NestNewMessageViewModel.kt | 9 ++-- .../NewPublicMessageViewModel.kt | 9 ++-- .../commons/model/composer/PreviewState.kt | 1 + .../commons/model/location/DeviceLocation.kt | 49 +++++++++++++++++++ .../commons/search/calendar/LocalClock.kt | 3 ++ .../amethyst/commons/service/AppServices.kt | 4 ++ .../commons/service/uploads/MediaUploader.kt | 17 +++++++ .../viewmodels/AccountViewModelHost.kt | 4 ++ .../commons/search/calendar/LocalClock.ios.kt | 2 + .../search/calendar/LocalClock.jvmAndroid.kt | 7 +++ .../components/LocationPermission.android.kt | 41 ++++++++++++++++ .../ui/note/PlatformDateFormat.android.kt | 6 +++ .../ui/components/LocationPermission.kt | 30 ++++++++++++ .../ui/components/PlatformPermission.kt | 32 ++++++++++++ .../commons/ui/note/PlatformDateFormat.kt | 4 ++ .../ui/components/LocationPermission.ios.kt | 33 +++++++++++++ .../commons/ui/note/PlatformDateFormat.ios.kt | 5 ++ .../ui/components/LocationPermission.jvm.kt | 33 +++++++++++++ .../commons/ui/note/PlatformDateFormat.jvm.kt | 11 +++++ 41 files changed, 390 insertions(+), 126 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt (98%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/location/DeviceLocation.kt create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.android.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformPermission.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.jvm.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt index 34743eeada..74d92448ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.location import android.content.Context import android.location.Location import android.location.LocationManager +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision @@ -75,7 +76,7 @@ class LocationState( LocationFlow(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager) .get(minTimeMs, minDistanceM, onListening) }, -) { +) : DeviceLocation { companion object { /** A 5 km cell takes 2.5 minutes to cross at 120 km/h; 60s/500m is ample. */ const val COARSE_MIN_TIME: Long = 60_000L @@ -111,7 +112,7 @@ class LocationState( private val latestPreciseLocation = MutableStateFlow(LocationResult.Loading) - fun setLocationPermission(newValue: Boolean) { + override fun setLocationPermission(newValue: Boolean) { if (newValue != hasLocationPermission.value) { hasLocationPermission.tryEmit(newValue) } @@ -195,7 +196,7 @@ class LocationState( } }.stateIn(scope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_TIMEOUT_MS), cache.value) - val geohashStateFlow: StateFlow by lazy { + override val geohashStateFlow: StateFlow by lazy { buildGeohashStateFlow( tag = "GeohashStateFlow", charsCount = GeohashPrecision.KM_5_X_5.digits, @@ -217,7 +218,7 @@ class LocationState( * building-level today. The profile is kept so the intent survives if the * app ever requests `ACCESS_FINE_LOCATION`. */ - val preciseGeohashStateFlow: StateFlow by lazy { + override val preciseGeohashStateFlow: StateFlow by lazy { buildGeohashStateFlow( tag = "PreciseGeohashStateFlow", charsCount = GeohashChannelLevel.BUILDING.chars, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt index 2e670493a4..9615badac2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.service.uploads import android.content.Context import android.net.Uri +import android.webkit.MimeTypeMap import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException import com.vitorpamplona.amethyst.commons.service.upload.FileHeader @@ -52,6 +54,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import java.io.File +import java.util.Locale import kotlin.coroutines.cancellation.CancellationException /** @@ -592,4 +595,14 @@ class AndroidMediaUploader( } } } + + override suspend fun remoteFileHeader( + url: String, + httpClients: IRoleBasedHttpClientBuilder, + ): FileHeader? = + withContext(Dispatchers.IO) { + val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url) + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault())) + FileHeader.prepare(url, mimeType, null) { httpClients.okHttpClientForImage(it) }.getOrNull() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt index dae3a52c12..37716f40e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt @@ -34,13 +34,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions import com.vitorpamplona.amethyst.commons.resources.loading_location import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer @@ -61,7 +61,7 @@ fun AndroidAroundMeLocationLabel() { overflow = TextOverflow.Ellipsis, ) } else { - val location by Amethyst.instance.locationManager.geohashStateFlow + val location by LocalAppServices.current.deviceLocation.geohashStateFlow .collectAsStateWithLifecycle() when (val myLocation = location) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 2e6e2421d0..714bb5defb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.expiration -import android.text.format.DateFormat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -64,14 +62,15 @@ import com.vitorpamplona.amethyst.commons.resources.expiration_date_select import com.vitorpamplona.amethyst.commons.resources.expiration_expires_in import com.vitorpamplona.amethyst.commons.resources.expiration_time import com.vitorpamplona.amethyst.commons.resources.next +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset +import com.vitorpamplona.quartz.utils.currentTimeMillis @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -79,24 +78,26 @@ fun ExpirationDatePicker(model: IExpiration) { var showDatePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } - val currentTime = Instant.ofEpochMilli(model.expirationDate * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() + // The expiration as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = model.expirationDate + LocalClock.utcOffsetSeconds(model.expirationDate) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = model.expirationDate * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86400000 + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= currentTimeMillis() - 86400000 }, ) - val context = LocalContext.current val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) Column(Modifier.fillMaxWidth()) { @@ -183,9 +184,7 @@ fun ExpirationDatePicker(model: IExpiration) { (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - - model.expirationDate = datetimeLocalTimeZone - offset.totalSeconds + model.expirationDate = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) showTimePicker = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt index 222befd9b0..39f808170d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions import com.vitorpamplona.amethyst.commons.resources.loading_location import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt index 02445f15df..fe2f920d0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt @@ -82,7 +82,6 @@ import androidx.compose.ui.window.DialogProperties import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.location.LocationResult @@ -97,6 +96,8 @@ import com.vitorpamplona.amethyst.commons.resources.location_picker_search_hint import com.vitorpamplona.amethyst.commons.resources.location_picker_title import com.vitorpamplona.amethyst.commons.resources.location_picker_use_mine import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.location.ForwardGeolocation import com.vitorpamplona.amethyst.service.location.LocationState @@ -206,7 +207,7 @@ fun GeohashLocationPickerContent( val context = LocalContext.current val keyboard = LocalSoftwareKeyboardController.current val scope = rememberCoroutineScope() - val locationManager = Amethyst.instance.locationManager + val locationManager = LocalAppServices.current.deviceLocation val seed = remember(initialGeohash) { initialGeohash?.takeIf { it.isNotBlank() }?.let { GeoHash.decode(it) } } val seedLen = initialGeohash?.trim()?.length ?: 0 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt index 9f78db5852..89945df357 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.location +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult -import com.vitorpamplona.amethyst.service.location.LocationState import kotlinx.coroutines.flow.StateFlow interface ILocationGrabber { - fun locationManager(): LocationState + fun locationManager(): DeviceLocation fun locationFlow(): StateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt index 4a74cc7907..73dc3a764e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.location -import android.Manifest import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -39,37 +38,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.geohash_explainer import com.vitorpamplona.amethyst.commons.resources.geohash_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -@OptIn(ExperimentalPermissionsApi::class) @Composable fun LocationAsHash( postViewModel: ILocationGrabber, innerContent: @Composable () -> Unit = {}, ) { - val locationPermissionState = - rememberPermissionState( - Manifest.permission.ACCESS_COARSE_LOCATION, - ) + val locationPermission = rememberCoarseLocationPermission() - LaunchedEffect(locationPermissionState.status.isGranted) { - postViewModel.locationManager().setLocationPermission(locationPermissionState.status.isGranted) + LaunchedEffect(locationPermission.isGranted) { + postViewModel.locationManager().setLocationPermission(locationPermission.isGranted) } - if (locationPermissionState.status.isGranted) { + if (locationPermission.isGranted) { DisplayLocationBox(postViewModel, innerContent) } else { - LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() } + LaunchedEffect(locationPermission) { locationPermission.launchRequest() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt index bb58f52b24..768421cb6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.messagefield -import android.net.Uri import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle @@ -40,6 +39,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation @@ -55,7 +55,7 @@ fun MessageField( placeholder: StringResource, viewModel: IMessageField, requestFocus: Boolean = true, - onContentReceived: ((Uri, String?) -> Unit)? = null, + onContentReceived: ((MediaUri, String?) -> Unit)? = null, ) { val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current 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 bb649a217a..5b05122de6 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState @@ -73,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -1006,9 +1005,7 @@ open class CommentPostViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -1081,5 +1078,5 @@ open class CommentPostViewModel : return location!! } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index 6d66399b4a..41062f32af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.AppServices import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder @@ -52,6 +53,8 @@ object AndroidAppServices : AppServices { override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder + + override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager } /** [BlossomServerFinder] over the app's BUD-10 resolver, read lazily (main process only). */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index 3f7a1d7466..4aedc816af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AppModules import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure @@ -74,6 +75,8 @@ class AndroidAccountViewModelHost( override val mediaUploader: MediaUploader by lazy { AndroidMediaUploader(modules.appContext) } + override val deviceLocation: DeviceLocation get() = modules.locationManager + override val savedAccounts: Flow> = LocalPreferences .accountsFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 8312cf5c02..8150638e42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -45,6 +45,8 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_opening_external_signer import com.vitorpamplona.amethyst.commons.resources.error_opening_external_signer_description +import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreload import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils @@ -133,13 +135,13 @@ fun LoggedInPage( ) } -@OptIn(ExperimentalPermissionsApi::class) @Composable fun WatchLocationPermissions() { - val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) + val locationPermissionState = rememberCoarseLocationPermission() - LaunchedEffect(locationPermissionState.status.isGranted) { - Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted) + val locationSource = LocalAppServices.current.deviceLocation + LaunchedEffect(locationPermissionState.isGranted) { + locationSource.setLocationPermission(locationPermissionState.isGranted) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index e3b13ec255..51c8e98202 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat -import android.Manifest import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -57,16 +56,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserPicture import com.vitorpamplona.amethyst.commons.search.SearchSeed import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold @@ -74,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver @@ -121,7 +118,6 @@ fun GeohashChatScreen( } } -@OptIn(ExperimentalPermissionsApi::class) @Composable private fun GeohashChatRoom( channel: GeohashChatChannel, @@ -159,13 +155,14 @@ private fun GeohashChatRoom( // otherwise we fall back to how the user arrived — the map picker passes teleported=true, "near me" // and manual entry default to false. This drives the ["t","teleport"] tag honestly. rememberPermissionState // only reads the current grant (it never prompts), so opening a location chat can't trigger a GPS dialog. - val locationPermission = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) - LaunchedEffect(locationPermission.status.isGranted) { - if (locationPermission.status.isGranted) { - Amethyst.instance.locationManager.setLocationPermission(true) + val locationPermission = rememberCoarseLocationPermission() + val locationSource = LocalAppServices.current.deviceLocation + LaunchedEffect(locationPermission.isGranted) { + if (locationPermission.isGranted) { + locationSource.setLocationPermission(true) } } - val deviceLocation by Amethyst.instance.locationManager.preciseGeohashStateFlow + val deviceLocation by locationSource.preciseGeohashStateFlow .collectAsStateWithLifecycle() val isTeleported = remember(deviceLocation, geohash, teleported) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt index 7e8c436be2..544216c5d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat -import android.Manifest import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -56,18 +55,16 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash @@ -274,18 +271,18 @@ private fun ManualEntrySection(onOpen: (String) -> Unit) { } } -@OptIn(ExperimentalPermissionsApi::class) @Composable private fun NearMeSection(onOpen: (String) -> Unit) { Column { SectionHeader(MaterialSymbols.Explore, "Near me") - val permission = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) - LaunchedEffect(permission.status.isGranted) { - Amethyst.instance.locationManager.setLocationPermission(permission.status.isGranted) + val permission = rememberCoarseLocationPermission() + val locationSource = LocalAppServices.current.deviceLocation + LaunchedEffect(permission.isGranted) { + locationSource.setLocationPermission(permission.isGranted) } - if (!permission.status.isGranted) { + if (!permission.isGranted) { Card( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow), modifier = Modifier.fillMaxWidth(), @@ -297,7 +294,7 @@ private fun NearMeSection(onOpen: (String) -> Unit) { color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 12.dp), ) - FilledTonalButton(onClick = { permission.launchPermissionRequest() }) { + FilledTonalButton(onClick = { permission.launchRequest() }) { SymbolIcon(symbol = MaterialSymbols.LocationOn, contentDescription = null, modifier = Modifier.size(18.dp)) Text(" Use my location") } @@ -306,7 +303,7 @@ private fun NearMeSection(onOpen: (String) -> Unit) { return } - val location by Amethyst.instance.locationManager.preciseGeohashStateFlow + val location by LocalAppServices.current.deviceLocation.preciseGeohashStateFlow .collectAsStateWithLifecycle() when (val loc = location) { is LocationResult.Success -> { 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 5059cfe763..27d164978e 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -57,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader @@ -798,9 +797,7 @@ class ChatNewMessageViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -870,5 +867,5 @@ class ChatNewMessageViewModel : draftTag.newVersion() } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index e04b6ce484..420543551e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt @@ -20,13 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import android.webkit.MimeTypeMap import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.commons.service.upload.FileHeader +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.prepare import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip92IMeta.imetaTagBuilder @@ -40,21 +39,17 @@ import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.nip94FileMetadata.thumbhash -import okhttp3.OkHttpClient -import java.util.Locale class IMetaAttachments { var iMetaAttachments by mutableStateOf>(emptyList()) suspend fun downloadAndPrepare( url: String, - okHttpClient: (String) -> OkHttpClient, + uploader: MediaUploader, + httpClients: IRoleBasedHttpClientBuilder, ) { - val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url) - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault())) - val imeta = - FileHeader.prepare(url, mimeType, null, okHttpClient).getOrNull()?.let { + uploader.remoteFileHeader(url, httpClients)?.let { IMetaTagBuilder(url) .apply { hash(it.hash) 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 da332f094c..c5a59ffdf0 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel @@ -44,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel @@ -71,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -1016,9 +1015,7 @@ open class ChannelNewMessageViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -1041,7 +1038,7 @@ open class ChannelNewMessageViewModel : message.insertUrlAtCursor(newElement) } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation override fun locationFlow(): StateFlow { if (location == null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 527a64cf98..f121e543ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia @@ -72,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -686,9 +685,7 @@ class LongFormPostViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -762,5 +759,5 @@ class LongFormPostViewModel : draftTag.newVersion() } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } 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 a5b3dfdad5..c0bd0a7c17 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState @@ -67,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -579,9 +578,7 @@ open class NewProductViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaDescription.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaDescription.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -681,5 +678,5 @@ open class NewProductViewModel : draftTag.newVersion() } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 8ff65412ac..aed1eef237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -59,7 +59,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedState import com.vitorpamplona.amethyst.commons.feeds.FeedContentState @@ -92,6 +91,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.zonedDrawerSwipeIfModal import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.NewGeoPostButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.NewHashtagPostButton import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -334,7 +334,7 @@ fun HomeScreenFloatingButton( when (val filter = list.value) { TopFilter.AroundMe -> { - val location by Amethyst.instance.locationManager.geohashStateFlow + val location by LocalAppServices.current.deviceLocation.geohashStateFlow .collectAsStateWithLifecycle() when (val myLocation = location) { 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 629fb3cc2d..d8f4703e56 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 @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia @@ -91,7 +92,6 @@ import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber @@ -1822,9 +1822,7 @@ open class ShortNotePostViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -2120,5 +2118,5 @@ open class ShortNotePostViewModel : draftTag.newVersion() } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 378d7a8aa3..670e8b49ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -40,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -571,9 +570,7 @@ open class NestNewMessageViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -596,7 +593,7 @@ open class NestNewMessageViewModel : message.insertUrlAtCursor(newElement) } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation override fun locationFlow(): StateFlow { if (location == null) { 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 4031174597..ccdf7f60ca 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 @@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState @@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -643,9 +642,7 @@ class NewPublicMessageViewModel : val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link) { - accountViewModel.httpClientBuilder.okHttpClientForImage(item.link) - } + iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder) } message.replaceCurrentWord(wordToInsert) @@ -715,5 +712,5 @@ class NewPublicMessageViewModel : draftTag.newVersion() } - override fun locationManager(): LocationState = Amethyst.instance.locationManager + override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt index b5c748ecd5..b8f4939fd9 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.richtext.CachedUrlParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/location/DeviceLocation.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/location/DeviceLocation.kt new file mode 100644 index 0000000000..bc21155841 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/location/DeviceLocation.kt @@ -0,0 +1,49 @@ +/* + * 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.commons.model.location + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * The device's position as geohashes, for "around me" feeds, location chats and the composers' + * location tag. The platform decides when it actually listens; readers only collect. + */ +interface DeviceLocation { + /** A roughly 5 km geohash cell, or why there is none. */ + val geohashStateFlow: StateFlow + + /** The finest geohash the platform offers, for location channels that truncate it per level. */ + val preciseGeohashStateFlow: StateFlow + + /** Tells the source whether the user has granted location access, so it can start or stop. */ + fun setLocationPermission(newValue: Boolean) + + /** No location source on this platform: always reports a missing permission. */ + object None : DeviceLocation { + private val lacking = MutableStateFlow(LocationResult.LackPermission) + + override val geohashStateFlow: StateFlow = lacking + override val preciseGeohashStateFlow: StateFlow = lacking + + override fun setLocationPermission(newValue: Boolean) = Unit + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.kt index c776a8d4d2..cc21649e3d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.kt @@ -41,6 +41,9 @@ expect object LocalClock { /** Today, in the reader's timezone. */ fun today(): SearchDate + /** How many seconds this zone is ahead of UTC at [unixSeconds]; a clock change moves it. */ + fun utcOffsetSeconds(unixSeconds: Long): Long + /** * Which local day a unix second falls on, with the zone read once for the whole counter * rather than per timestamp: for grouping a batch of events by day in one pass. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index d13b4f0e0e..ffa7fcea27 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver @@ -56,4 +57,7 @@ interface AppServices { /** Finds the server behind a `blossom:` URI (BUD-10). */ val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None + + /** The device's position as geohashes. */ + val deviceLocation: DeviceLocation get() = DeviceLocation.None } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt index a20a1de7bc..285902e8a0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.commons.service.uploads import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder +import com.vitorpamplona.amethyst.commons.service.upload.FileHeader import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.ciphers.NostrCipher @@ -74,6 +76,16 @@ interface MediaUploader { useH265: Boolean, convertGifToMp4: Boolean, ): MediaCompressorResult + + /** + * Downloads the already-hosted file at [url] and computes its [FileHeader] (hash, size, + * dimensions, preview hashes) for an `imeta` tag, guessing the MIME type from the URL's + * extension. Null when it cannot be downloaded or decoded. + */ + suspend fun remoteFileHeader( + url: String, + httpClients: IRoleBasedHttpClientBuilder, + ): FileHeader? } /** No media pipeline on this front end yet: every upload fails with a clear message. */ @@ -121,4 +133,9 @@ object UnavailableMediaUploader : MediaUploader { useH265: Boolean, convertGifToMp4: Boolean, ): MediaCompressorResult = MediaCompressorResult(uri, mimeType, null) + + override suspend fun remoteFileHeader( + url: String, + httpClients: IRoleBasedHttpClientBuilder, + ): FileHeader? = null } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 8f497e176e..4cbef6742e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.viewmodels +import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader @@ -70,6 +71,9 @@ interface AccountViewModelHost { /** Compresses, strips, encrypts and uploads picked media for the composers. */ val mediaUploader: MediaUploader + /** The device's position as geohashes. */ + val deviceLocation: DeviceLocation get() = DeviceLocation.None + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.ios.kt index cfebd03c92..f535752b38 100644 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.ios.kt @@ -67,6 +67,8 @@ actual object LocalClock { actual fun today(): SearchDate = ZoneMath.dayAt(currentTimeSeconds(), localZone) + actual fun utcOffsetSeconds(unixSeconds: Long): Long = localZone.at(unixSeconds) + actual fun epochDayCounter(): EpochDayCounter = EpochDayCounter { ZoneMath.epochDayAt(it, localZone) } /** diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.jvmAndroid.kt index f7288c0cad..cd836faa47 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.jvmAndroid.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/search/calendar/LocalClock.jvmAndroid.kt @@ -54,6 +54,13 @@ actual object LocalClock { SearchDate(it.year, it.monthValue, it.dayOfMonth) } + actual fun utcOffsetSeconds(unixSeconds: Long): Long = + zone() + .rules + .getOffset(Instant.ofEpochSecond(unixSeconds)) + .totalSeconds + .toLong() + actual fun epochDayCounter(): EpochDayCounter { val rules = zone().rules val offsets = ZoneOffsets { rules.getOffset(Instant.ofEpochSecond(it)).totalSeconds.toLong() } diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.android.kt new file mode 100644 index 0000000000..6e23c715b5 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.android.kt @@ -0,0 +1,41 @@ +/* + * 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.commons.ui.components + +import android.Manifest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +actual fun rememberCoarseLocationPermission(): PlatformPermission { + val state = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) + return remember(state) { + object : PlatformPermission { + override val isGranted: Boolean get() = state.status.isGranted + + override fun launchRequest() = state.launchPermissionRequest() + } + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt index f4c13f3478..f33865d82e 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -70,6 +70,12 @@ actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String { return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } } } +@Composable +actual fun rememberIs24HourClock(): Boolean { + val context = LocalContext.current + return remember(context) { DateFormat.is24HourFormat(context) } +} + actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.kt new file mode 100644 index 0000000000..d49f8e84ab --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +/** + * The approximate-location permission the geohash features need. Android asks for + * `ACCESS_COARSE_LOCATION`; desktop and iOS have no location source wired yet and report it denied. + */ +@Composable +expect fun rememberCoarseLocationPermission(): PlatformPermission diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformPermission.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformPermission.kt new file mode 100644 index 0000000000..d2ebb6b6a2 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformPermission.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Stable + +/** A runtime permission the user may grant or deny. [isGranted] is observable from composition. */ +@Stable +interface PlatformPermission { + val isGranted: Boolean + + /** Asks the user for the permission; [isGranted] updates when they answer. */ + fun launchRequest() +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt index 4b88cbbbff..d8962cc592 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -45,6 +45,10 @@ expect fun calendarYearAndDay(epochMillis: Long): Int @Composable expect fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String +/** Whether times should be shown on a 24-hour clock, under the same rules as [rememberTimeOfDayFormatter]. */ +@Composable +expect fun rememberIs24HourClock(): Boolean + /** * The platform's abbreviated relative span ("5 min. ago", "Yesterday"), [nowLabel] for an * instant that rounds to now or lies ahead, or null where the platform has no such formatter. diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.ios.kt new file mode 100644 index 0000000000..1dc27c0ede --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.ios.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +private object NoLocationPermission : PlatformPermission { + override val isGranted: Boolean = false + + override fun launchRequest() = Unit +} + +/** No location source on this platform yet. */ +@Composable +actual fun rememberCoarseLocationPermission(): PlatformPermission = NoLocationPermission diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt index 3a03a0d9e7..77cede0810 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarUnitDay import platform.Foundation.NSCalendarUnitYear @@ -88,6 +89,10 @@ private val timeOfDay: (Long) -> String = { epochMillis -> timeOfDayFormatter.st @Composable actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay +// The "j" template asks for the locale's preferred hour; an "a" in the result means an AM/PM clock. +@Composable +actual fun rememberIs24HourClock(): Boolean = remember { NSDateFormatter.dateFormatFromTemplate("j", 0u, NSLocale.currentLocale)?.contains('a') != true } + actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.jvm.kt new file mode 100644 index 0000000000..1dc27c0ede --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LocationPermission.jvm.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +private object NoLocationPermission : PlatformPermission { + override val isGranted: Boolean = false + + override fun launchRequest() = Unit +} + +/** No location source on this platform yet. */ +@Composable +actual fun rememberCoarseLocationPermission(): PlatformPermission = NoLocationPermission diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt index 3ad02fe674..b95d0d321a 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt @@ -21,9 +21,12 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import java.time.Instant import java.time.ZoneId +import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder import java.time.format.FormatStyle import java.util.Locale @@ -80,6 +83,14 @@ private val timeOfDay: (Long) -> String = { epochMillis -> @Composable actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay +@Composable +actual fun rememberIs24HourClock(): Boolean = + remember { + DateTimeFormatterBuilder + .getLocalizedDateTimePattern(null, FormatStyle.SHORT, IsoChronology.INSTANCE, Locale.getDefault()) + .contains('H') + } + actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, From 64dcc7bf71aceb02377aa229a59d7df82cee1c24 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 01:14:01 +0000 Subject: [PATCH 06/45] refactor: share-intent ports for the composers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SharedMediaResolver (rememberSharedMediaResolver) replaces the app's resolveSharedMedia(context, …); OnIncomingShare replaces the per-screen SEND-intent listeners in ShortNotePostScreen and GenericCommentPostScreen. - ViewModels log their class via this::class.simpleName. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../ui/actions/uploads/SharedMediaResolver.kt | 63 ---------- .../nip22Comments/GenericCommentPostScreen.kt | 55 ++------- .../buzz/BuzzWorkspaceOverflowMenu.kt | 2 +- .../loggedIn/chats/privateDM/ChatroomView.kt | 7 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../nip23LongForm/LongFormPostViewModel.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 51 ++------ .../loggedIn/home/ShortNotePostViewModel.kt | 4 +- .../loggedIn/home/VoiceReplyViewModel.kt | 2 +- .../room/chat/NestNewMessageViewModel.kt | 2 +- .../ParticipantHostActionsSheet.kt | 2 +- .../NewPublicMessageViewModel.kt | 2 +- .../loggedIn/pictures/NewPictureButton.kt | 7 +- .../loggedIn/shorts/NewShortVideoButton.kt | 7 +- .../loggedIn/video/NewVideoFeedButton.kt | 7 +- .../video/hls/HlsPublishOrchestrator.kt | 2 +- .../uploads/SharedMediaResolver.android.kt | 112 ++++++++++++++++++ .../ui/actions/uploads/SharedMediaResolver.kt | 51 ++++++++ .../uploads/SharedMediaResolver.ios.kt | 44 +++++++ .../uploads/SharedMediaResolver.jvm.kt | 44 +++++++ 21 files changed, 294 insertions(+), 176 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt deleted file mode 100644 index 465afd5ee8..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt +++ /dev/null @@ -1,63 +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.actions.uploads - -import android.content.Context -import androidx.core.net.toUri -import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/** - * Resolves a shared content-URI string (from an Android SEND intent) into a - * [SelectedMedia] by reading its MIME type off the main thread. Returns null for - * a null/blank URI. Shared by the share-to-compose pre-fill paths (DM chatroom, - * group DM, …) so the URI→MIME→SelectedMedia logic lives in one place. - */ -suspend fun resolveSharedMedia( - context: Context, - uriString: String?, -): SelectedMedia? = - uriString?.ifBlank { null }?.toUri()?.let { uri -> - withContext(Dispatchers.IO) { - SelectedMedia(uri, context.contentResolver.getType(uri)) - } - } - -/** - * Batch form for SEND_MULTIPLE shares: resolves every URI in one trip to the IO dispatcher - * instead of one context switch per file. Blank entries are dropped, so an empty list in means - * an empty list out and the composer stays closed. - */ -suspend fun resolveSharedMedia( - context: Context, - uriStrings: List, -): ImmutableList { - val uris = uriStrings.mapNotNull { it.ifBlank { null }?.toUri() } - if (uris.isEmpty()) return persistentListOf() - - return withContext(Dispatchers.IO) { - uris.map { SelectedMedia(it, context.contentResolver.getType(it)) }.toImmutableList() - } -} 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 03f99c89ff..d440dc8c46 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 @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.nip22Comments -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box @@ -46,20 +44,14 @@ import androidx.compose.material3.Surface 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.content.IntentCompat -import androidx.core.net.toUri -import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -79,10 +71,12 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -114,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -132,9 +125,6 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Composable fun ReplyCommentPostScreen( @@ -149,7 +139,7 @@ fun ReplyCommentPostScreen( val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { replyId?.let { LocalCache.getNoteIfExists(it) }?.let { @@ -165,11 +155,8 @@ fun ReplyCommentPostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null) { postViewModel.applySignature() @@ -188,32 +175,11 @@ fun GenericCommentPostScreen( ) { WatchAndLoadMyEmojiList(accountViewModel) - val context = LocalContext.current - val activity = context.getActivity() - val scope = rememberCoroutineScope() - - DisposableEffect(nav, activity) { - // Microsoft's swift key sends Gifs as new actions - val consumer = - Consumer { intent -> - if (intent.action == Intent.ACTION_SEND) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { - postViewModel.addToMessage(it) - } - - // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> - scope.launch(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(uri) - postViewModel.selectImage(persistentListOf(SelectedMedia(uri, mediaType))) - } - } - } - } - - activity.addOnNewIntentListener(consumer) - onDispose { activity.removeOnNewIntentListener(consumer) } - } + // Microsoft's swift key sends Gifs as new actions + OnIncomingShare( + onText = postViewModel::addToMessage, + onMedia = { postViewModel.selectImage(persistentListOf(it)) }, + ) // NIP-9B: when replying into a NIP-72 community, mount the community feed // subscription so the latest kind:34551 rules document is fetched and @@ -466,7 +432,6 @@ private fun GenericCommentPostBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt index cd03b8f84b..1c7dd54571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt @@ -157,7 +157,7 @@ fun BuzzWorkspaceOverflowMenu( ) } catch (e: Exception) { if (e is CancellationException) throw e - error = e.message ?: e::class.simpleName + error = e.message ?: e.javaClass.simpleName } finally { minting = false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index c8023478bb..f9f8b86e19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -47,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFind import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04 import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip17 +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog @@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.hea import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel @@ -144,10 +143,10 @@ fun ChatroomView( newPostModel.onMessageChanged() } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() if (attachmentUri != null) { LaunchedEffect(key1 = attachmentUri) { - resolveSharedMedia(context, attachmentUri)?.let { + mediaResolver.resolve(attachmentUri)?.let { newPostModel.pickedMedia(persistentListOf(it)) } } 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 27d164978e..2f7bf5b5f5 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 @@ -823,7 +823,7 @@ class ChatNewMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } // NIP-04 sending is deprecated. NIP-17 is always used. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index f121e543ec..623388bce5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -718,7 +718,7 @@ class LongFormPostViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( 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 c0bd0a7c17..a923c1b1c3 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 @@ -616,7 +616,7 @@ open class NewProductViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( 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 1fd8054efb..1ce2877986 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 @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import android.content.Intent -import android.net.Uri import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable @@ -55,13 +53,11 @@ import androidx.compose.material3.Switch 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.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -72,9 +68,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.IntentCompat -import androidx.core.net.toUri -import androidx.core.util.Consumer import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -110,10 +103,12 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -156,7 +151,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -177,10 +171,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -202,8 +193,7 @@ fun ShortNotePostScreen( postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) val context = LocalContext.current - val activity = context.getActivity() - val scope = rememberCoroutineScope() + val mediaResolver = rememberSharedMediaResolver() val proposeAiImprovements by accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements @@ -227,39 +217,19 @@ fun ShortNotePostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null && forkId == null) { postViewModel.applySignature() } } - DisposableEffect(nav, activity) { - // Microsoft's swift key sends Gifs as new actions - val consumer = - Consumer { intent -> - if (intent.action == Intent.ACTION_SEND) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { - postViewModel.addToMessage(it) - } - - // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> - scope.launch(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(uri) - postViewModel.selectImage(persistentListOf(SelectedMedia(uri, mediaType))) - } - } - } - } - - activity.addOnNewIntentListener(consumer) - onDispose { activity.removeOnNewIntentListener(consumer) } - } + // Microsoft's swift key sends Gifs as new actions + OnIncomingShare( + onText = postViewModel::addToMessage, + onMedia = { postViewModel.selectImage(persistentListOf(it)) }, + ) NewPostScreenInner(postViewModel, accountViewModel, nav) } @@ -610,7 +580,6 @@ private fun NewPostScreenBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, 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 d8f4703e56..fafc19c91c 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 @@ -1995,7 +1995,7 @@ open class ShortNotePostViewModel : } catch (e: Exception) { onError( uploadErrorTitle, - loadStringRes(Res.string.upload_error_voice_message_exception, e.message ?: e.javaClass.simpleName), + loadStringRes(Res.string.upload_error_voice_message_exception, e.message ?: e::class.simpleName.orEmpty()), ) voiceRecording = null } finally { @@ -2015,7 +2015,7 @@ open class ShortNotePostViewModel : override fun onCleared() { writingAssistant?.close() writingAssistant = null - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 850b450647..1f1e65aca0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -326,6 +326,6 @@ class VoiceReplyViewModel : ViewModel() { override fun onCleared() { cancel() - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 670e8b49ef..80e8fa567d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -604,7 +604,7 @@ open class NestNewMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt index 1a371c87b1..a83271bea9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt @@ -555,7 +555,7 @@ private fun broadcastWithDiagnostics( } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val klass = e::class.simpleName ?: "Throwable" + val klass = e.javaClass.simpleName ?: "Throwable" val msg = e.message?.takeIf { it.isNotBlank() } ?: "no message" accountViewModel.toastManager.toast(failedTitle, "$klass: $msg") } 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 ccdf7f60ca..a7611652b2 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 @@ -670,7 +670,7 @@ class NewPublicMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt index aa81f029da..c122b7de76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -54,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -62,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -102,9 +101,9 @@ fun NewPictureButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToPostFromCamera) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index d87da1be98..6dbe33d697 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -55,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -103,9 +102,9 @@ fun NewShortVideoButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToRecordVideo) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt index 81868ac4d1..325cfcfbf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -56,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -110,9 +109,9 @@ fun NewVideoFeedButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToPostFromCamera) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt index 7a42bf22e3..c8245ef129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt @@ -279,7 +279,7 @@ class HlsPublishOrchestrator( // scope; NewHlsVideoViewModel.cancel() calls reset() to put state back to Idle. throw e } catch (e: Throwable) { - _state.value = HlsPublishState.Failure(message = e.message ?: e::class.simpleName.orEmpty()) + _state.value = HlsPublishState.Failure(message = e.message ?: e.javaClass.simpleName.orEmpty()) } } diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt new file mode 100644 index 0000000000..371121e23e --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt @@ -0,0 +1,112 @@ +/* + * 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.commons.ui.actions.uploads + +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.IntentCompat +import androidx.core.net.toUri +import androidx.core.util.Consumer +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private class ContentResolverSharedMedia( + private val context: Context, +) : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = + uriString?.ifBlank { null }?.toUri()?.let { uri -> + withContext(Dispatchers.IO) { + SelectedMedia(uri, context.contentResolver.getType(uri)) + } + } + + // One trip to the IO dispatcher for the whole batch instead of one context switch per file. + override suspend fun resolve(uriStrings: List): ImmutableList { + val uris = uriStrings.mapNotNull { it.ifBlank { null }?.toUri() } + if (uris.isEmpty()) return persistentListOf() + + return withContext(Dispatchers.IO) { + uris.map { SelectedMedia(it, context.contentResolver.getType(it)) }.toImmutableList() + } + } +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver { + val context = LocalContext.current.applicationContext + return remember(context) { ContentResolverSharedMedia(context) } +} + +private tailrec fun Context.findComponentActivity(): ComponentActivity? = + when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findComponentActivity() + else -> null + } + +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) { + val context = LocalContext.current + val activity = context.findComponentActivity() ?: return + val scope = rememberCoroutineScope() + val currentOnText by rememberUpdatedState(onText) + val currentOnMedia by rememberUpdatedState(onMedia) + + DisposableEffect(activity) { + val consumer = + Consumer { intent -> + if (intent.action == Intent.ACTION_SEND) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { + currentOnText(it) + } + + // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). + IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> + scope.launch(Dispatchers.IO) { + val mediaType = context.contentResolver.getType(uri) + currentOnMedia(SelectedMedia(uri, mediaType)) + } + } + } + } + + activity.addOnNewIntentListener(consumer) + onDispose { activity.removeOnNewIntentListener(consumer) } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt new file mode 100644 index 0000000000..8949989d98 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt @@ -0,0 +1,51 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList + +/** + * Turns the file addresses another app shared with this one (an Android SEND intent's + * `content://` URIs) into [SelectedMedia], reading each MIME type off the main thread. + */ +interface SharedMediaResolver { + /** Null for a null or blank address. */ + suspend fun resolve(uriString: String?): SelectedMedia? + + /** Batch form for multi-file shares; blank entries are dropped. */ + suspend fun resolve(uriStrings: List): ImmutableList +} + +@Composable +expect fun rememberSharedMediaResolver(): SharedMediaResolver + +/** + * Calls [onText] and [onMedia] for content shared into the app while this screen is showing (a + * keyboard sending a GIF as a new share, say). Android listens for new SEND intents; the other + * platforms receive none yet. + */ +@Composable +expect fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt new file mode 100644 index 0000000000..2f541f5d43 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt @@ -0,0 +1,44 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.StringMediaUri +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +// Addresses are plain paths or URLs here; nothing reports a MIME type for them yet. +private object StringSharedMedia : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = uriString?.ifBlank { null }?.let { SelectedMedia(StringMediaUri(it), null) } + + override suspend fun resolve(uriStrings: List): ImmutableList = uriStrings.mapNotNull { resolve(it) }.toImmutableList() +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver = StringSharedMedia + +/** No app shares into this one on this platform yet. */ +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) = Unit diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt new file mode 100644 index 0000000000..2f541f5d43 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt @@ -0,0 +1,44 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.StringMediaUri +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +// Addresses are plain paths or URLs here; nothing reports a MIME type for them yet. +private object StringSharedMedia : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = uriString?.ifBlank { null }?.let { SelectedMedia(StringMediaUri(it), null) } + + override suspend fun resolve(uriStrings: List): ImmutableList = uriStrings.mapNotNull { resolve(it) }.toImmutableList() +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver = StringSharedMedia + +/** No app shares into this one on this platform yet. */ +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) = Unit From 51923a2224ca3d6c1a69947548fafe25940ac5d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 01:41:15 +0000 Subject: [PATCH 07/45] refactor: move the media feeds, geohash chats and highlight composer 33 files freed by the composer seams move to commonsUI: the pictures, shorts, longs and video feeds with their new-post buttons and NewMediaView, GeohashChatsScreen and NewGeohashChatScreen, NewHighlightScreen, plus the shared MessageField and ThinPaddingTextField. NewMediaView uses the shared edge-to-edge dialog properties. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/actions/EditPostView.kt | 1 + .../amethyst/ui/navigation/AppNavigation.kt | 14 +++++++------- .../location/GeohashLocationPickerDialog.kt | 2 +- .../ui/note/creators/zapsplits/ForwardZapTo.kt | 2 +- .../note/nip22Comments/GenericCommentPostScreen.kt | 4 ++-- .../ui/screen/loggedIn/BottomBarFeedPreloaders.kt | 8 ++++---- .../loggedIn/calendars/CalendarCollectionsView.kt | 2 +- .../loggedIn/calendars/CalendarEventListCard.kt | 2 +- .../loggedIn/chats/cordnGroup/CordnComposer.kt | 6 +++--- .../chats/marmotGroup/MarmotGroupChatView.kt | 4 ++-- .../loggedIn/chats/minichat/MinichatScreen.kt | 2 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 8 ++++---- .../privateDM/send/PrivateMessageEditFieldRow.kt | 4 ++-- .../publicChannels/concord/ConcordChannelScreen.kt | 6 +++--- .../chats/publicChannels/send/EditFieldRow.kt | 4 ++-- .../discover/nip23LongForm/LongFormPostScreen.kt | 6 +++--- .../discover/nip99Classifieds/NewProductScreen.kt | 4 ++-- .../discover/nip99Classifieds/SellProduct.kt | 4 ++-- .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 6 +++--- .../loggedIn/nests/room/chat/NestEditFieldRow.kt | 4 ++-- .../publicMessages/NewPublicMessageScreen.kt | 6 +++--- .../loggedIn/pictures/PictureCardComposePreview.kt | 2 ++ .../loggedIn/profile/header/UserNicknameCard.kt | 2 +- .../profile/header/UserProfileDropDownMenu.kt | 2 +- .../amethyst/commons}/ui/actions/NewMediaModel.kt | 3 ++- .../amethyst/commons}/ui/actions/NewMediaView.kt | 11 +++-------- .../ui/actions/UrlUserTagOutputTransformation.kt | 2 +- .../commons}/ui/components/ThinPaddingTextField.kt | 2 +- .../emojiSuggestions/WatchAndLoadMyEmojiList.kt | 2 +- .../ui/note/creators/messagefield/MessageField.kt | 6 +++--- .../chats/geohashChat/GeohashChatsScreen.kt | 2 +- .../chats/geohashChat/NewGeohashChatScreen.kt | 4 ++-- .../highlights/NewHighlightPostViewModel.kt | 2 +- .../loggedIn/highlights/NewHighlightScreen.kt | 10 +++++----- .../ui/screen/loggedIn/longs/LongsFeedLoaded.kt | 4 ++-- .../ui/screen/loggedIn/longs/LongsScreen.kt | 4 ++-- .../ui/screen/loggedIn/longs/LongsTopBar.kt | 2 +- .../ui/screen/loggedIn/longs/NewLongVideoButton.kt | 11 ++++++----- .../datasource/LongsFilterAssemblerSubscription.kt | 2 +- .../screen/loggedIn/pictures/NewPictureButton.kt | 11 ++++++----- .../screen/loggedIn/pictures/PictureCardCompose.kt | 6 +++--- .../screen/loggedIn/pictures/PictureFeedLoaded.kt | 2 +- .../ui/screen/loggedIn/pictures/PicturesScreen.kt | 4 ++-- .../ui/screen/loggedIn/pictures/PicturesTopBar.kt | 2 +- .../PicturesFilterAssemblerSubscription.kt | 2 +- .../screen/loggedIn/shorts/NewShortVideoButton.kt | 11 ++++++----- .../ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt | 2 +- .../ui/screen/loggedIn/shorts/ShortsScreen.kt | 4 ++-- .../ui/screen/loggedIn/shorts/ShortsTopBar.kt | 2 +- .../ui/screen/loggedIn/shorts/VideoCardCompose.kt | 6 +++--- .../ShortsFilterAssemblerSubscription.kt | 2 +- .../loggedIn/video/FileMetadataCardCompose.kt | 4 ++-- .../ui/screen/loggedIn/video/NewVideoFeedButton.kt | 11 ++++++----- .../ui/screen/loggedIn/video/StoriesTopBar.kt | 2 +- .../ui/screen/loggedIn/video/UserCardHeader.kt | 2 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 8 ++++---- .../datasource/VideoFilterAssemblerSubscription.kt | 2 +- 57 files changed, 129 insertions(+), 126 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/NewMediaModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/NewMediaView.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/UrlUserTagOutputTransformation.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/ThinPaddingTextField.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/messagefield/MessageField.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/highlights/NewHighlightScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/longs/LongsFeedLoaded.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/longs/LongsScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/longs/LongsTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/longs/NewLongVideoButton.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/NewPictureButton.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/PictureCardCompose.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/PicturesScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/PicturesTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/NewShortVideoButton.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/ShortsScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/ShortsTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/VideoCardCompose.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/FileMetadataCardCompose.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/NewVideoFeedButton.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/StoriesTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/UserCardHeader.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/VideoScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index de413a513b..77bad9e23b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.BechLink diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 8c2bdb2732..1dd170120f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -94,7 +94,9 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoa import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordEditScreen @@ -133,6 +135,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen @@ -142,6 +145,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.packs import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.LiveStreamsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.LongsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicPlaylistsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicTracksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.NewMusicPlaylistScreen @@ -149,6 +153,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.ConnectedA import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.NsitesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastEpisodesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsScreen @@ -190,10 +195,12 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.Cord import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnKeyPackagesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnLinkScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46SignerScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.ShortsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.UrlScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddClinkDebitWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddNwcWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddWalletScreen @@ -254,8 +261,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreat import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen @@ -295,14 +300,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssues import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.LongsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppDetailScreen @@ -311,7 +314,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen @@ -334,9 +336,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.ShortsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt index fe2f920d0b..9621f5b755 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt @@ -98,10 +98,10 @@ import com.vitorpamplona.amethyst.commons.resources.location_picker_use_mine import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.label import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.location.ForwardGeolocation import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.label import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt index de91bf32bc..aea74ca131 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_split_explainer import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user_placeholder import com.vitorpamplona.amethyst.commons.resources.zap_split_title +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay @@ -58,7 +59,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import kotlin.math.round @Composable 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 d440dc8c46..b9fd9818e7 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 @@ -85,9 +85,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList @@ -108,11 +110,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet import com.vitorpamplona.amethyst.ui.note.creators.notify.rememberAudienceLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 557a03c373..f26a420742 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -36,24 +36,24 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasourc import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.datasource.HighlightsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource.PodcastEpisodesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource.PodcastsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.datasource.PollsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.datasource.ProductsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription /** * Activates the relay subscription for each feed the user has pinned to the bottom diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 1b05e7275d..6c399baa03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt @@ -69,10 +69,10 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt index 7c874b1323..1d611b18a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt @@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.CalendarAppointmen import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import java.time.Instant import java.time.ZoneId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt index c72f18e79b..f65fa5ec3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt @@ -49,7 +49,10 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestio import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_composer_hint +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -62,11 +65,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 746d735fe0..2a05f95f8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -68,7 +68,9 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_confirm_t import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_reset import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -83,8 +85,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 523647dcb5..0cb84787cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.concord_open_channel import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose @@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog 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 e0790fb1df..99d2db483c 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 @@ -88,11 +88,13 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage @@ -100,10 +102,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField +import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList @@ -123,13 +127,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index b8f96867f9..2095011e6a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -62,8 +62,10 @@ import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_ import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor @@ -86,8 +88,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.showCount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 3f41b23ed2..17a2b090c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -77,7 +77,9 @@ import com.vitorpamplona.amethyst.commons.resources.concord_typing_one import com.vitorpamplona.amethyst.commons.resources.concord_typing_two import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -86,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.concordTimerText import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinDuties @@ -102,9 +105,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 3ad55a0c43..a28d6db47f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -54,8 +54,10 @@ import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -67,8 +69,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 35b625f97a..642b90c5c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.commons.resources.your_article_title import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery @@ -108,11 +109,13 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton @@ -131,10 +134,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection 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 81cab99ca7..02ba7531ad 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 @@ -64,9 +64,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList @@ -80,11 +82,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo 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 11bc0c490b..99c07a6bab 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 @@ -79,7 +79,9 @@ import com.vitorpamplona.amethyst.commons.resources.classifieds_location_placeho import com.vitorpamplona.amethyst.commons.resources.classifieds_price import com.vitorpamplona.amethyst.commons.resources.classifieds_title import com.vitorpamplona.amethyst.commons.resources.classifieds_title_placeholder +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation @@ -91,8 +93,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SquaredQuoteBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag import kotlinx.collections.immutable.toImmutableList 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 1ce2877986..c6be482894 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 @@ -110,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -118,9 +119,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.aihelp.AiWritingHelpPanel import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest @@ -150,12 +153,9 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet import com.vitorpamplona.amethyst.ui.note.creators.notify.rememberAudienceLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index ac621ee4df..1cba3aaacf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -41,7 +41,9 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -52,8 +54,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.FlowPreview 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 3fa4b8d0ba..69cfe5127e 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 @@ -60,17 +60,20 @@ import com.vitorpamplona.amethyst.commons.resources.public_message import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton @@ -92,9 +95,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt index eda5b2b6e6..03b466f446 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt @@ -36,6 +36,8 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PictureCardCaption +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PictureCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt index 0ad2717538..283adadbc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt @@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.nip85TrustedAssertions.ui.EditNickname import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserNickname import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nickname_private +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList /** * The nickname (petname) and private note the account keeps for [baseUser], diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index 91bb215d53..4a3a9b18f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -48,10 +48,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.components.util.setText +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.quartz.nip56Reports.ReportType import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaModel.kt index 9ee3618759..7c74829b87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaModel.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.actions +package com.vitorpamplona.amethyst.commons.ui.actions import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaView.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaView.kt index ba41954360..70820a51e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewMediaView.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.actions +package com.vitorpamplona.amethyst.commons.ui.actions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -50,7 +50,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.commons.model.VideoPostKind import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.resources.Res @@ -76,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGall import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.components.edgeToEdgeDialogProperties import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -111,12 +111,7 @@ fun NewMediaView( Dialog( onDismissRequest = { onClose() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), + properties = edgeToEdgeDialogProperties(dismissOnClickOutside = false), ) { SetDialogToEdgeToEdge() Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/UrlUserTagOutputTransformation.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/UrlUserTagOutputTransformation.kt index ba59fe6ca9..490a21197a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/UrlUserTagOutputTransformation.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.actions +package com.vitorpamplona.amethyst.commons.ui.actions import androidx.compose.foundation.text.input.OutputTransformation import androidx.compose.foundation.text.input.TextFieldBuffer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ThinPaddingTextField.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ThinPaddingTextField.kt index 00bd0ac935..8680b626a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ThinPaddingTextField.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.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt index faa85d2e68..9a916f936d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.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.note.creators.emojiSuggestions +package com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/messagefield/MessageField.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/messagefield/MessageField.kt index 768421cb6c..486a56ca86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/messagefield/MessageField.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.note.creators.messagefield +package com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions @@ -40,12 +40,12 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation +import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt index 71955dd4e6..57dc5a6464 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.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.chats.geohashChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt index 544216c5d8..4ef92bd12f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.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.chats.geohashChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -372,7 +372,7 @@ private fun TeleportCard(onClick: () -> Unit) { } } -internal fun GeohashChannelLevel.label(): String = +fun GeohashChannelLevel.label(): String = when (this) { GeohashChannelLevel.CONTINENT -> "Continent" GeohashChannelLevel.REGION -> "Region" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt index 4866a544e4..c1ecea6c86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.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.highlights +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightScreen.kt index fbc32c1b3f..255402c722 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/highlights/NewHighlightScreen.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.highlights +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -60,7 +60,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -71,18 +70,19 @@ import com.vitorpamplona.amethyst.commons.resources.new_highlight_note_placehold import com.vitorpamplona.amethyst.commons.resources.new_highlight_passage_placeholder import com.vitorpamplona.amethyst.commons.resources.new_highlight_source_label import com.vitorpamplona.amethyst.commons.resources.new_highlight_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose +import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField /** A warm highlighter amber — the highlight metaphor reads as yellow regardless of theme. */ private val MarkerAccent = Color(0xFFF5C518) @@ -113,7 +113,7 @@ fun NewHighlightScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: NewHighlightPostViewModel = viewModel() + val postViewModel: NewHighlightPostViewModel = rememberViewModel { NewHighlightPostViewModel() } postViewModel.init(accountViewModel) WatchAndLoadMyEmojiList(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsFeedLoaded.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsFeedLoaded.kt index d52cd64a1d..f7943d17ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsFeedLoaded.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.longs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -34,10 +34,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.VideoCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.VideoCardCompose import com.vitorpamplona.quartz.nip71Video.VideoEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsScreen.kt index 62afa18f9d..85ea1b6386 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsScreen.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.longs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription @Composable fun LongsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsTopBar.kt index 61640fe87f..2777c2ed76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/LongsTopBar.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.longs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/NewLongVideoButton.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/NewLongVideoButton.kt index 35d7d74d7a..814fd180d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/NewLongVideoButton.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.longs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -41,7 +41,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.VideoPostKind @@ -51,19 +50,21 @@ import com.vitorpamplona.amethyst.commons.resources.new_long_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaView import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaView import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -86,7 +87,7 @@ fun NewLongVideoButton( var pickedURIs by remember { mutableStateOf>(persistentListOf()) } val scope = rememberCoroutineScope() - val postViewModel: NewMediaModel = viewModel() + val postViewModel: NewMediaModel = rememberViewModel { NewMediaModel() } postViewModel.onceUploaded { scope.launch(Dispatchers.IO) { delay(500) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt index fedefdccc7..afde8baa53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/longs/datasource/LongsFilterAssemblerSubscription.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.longs.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/NewPictureButton.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/NewPictureButton.kt index c122b7de76..dc735af849 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/NewPictureButton.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.pictures +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -42,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -51,20 +50,22 @@ import com.vitorpamplona.amethyst.commons.resources.new_picture import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaView import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaView import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -93,7 +94,7 @@ fun NewPictureButton( var pickedURIs by remember { mutableStateOf>(persistentListOf()) } val scope = rememberCoroutineScope() - val postViewModel: NewMediaModel = viewModel() + val postViewModel: NewMediaModel = rememberViewModel { NewMediaModel() } postViewModel.onceUploaded { scope.launch(Dispatchers.IO) { delay(500) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureCardCompose.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureCardCompose.kt index d6667840ea..0d12458cec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureCardCompose.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.pictures +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -51,8 +51,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.toImmutableList @@ -165,7 +165,7 @@ private fun PictureCardImage( } @Composable -internal fun PictureCardCaption(event: PictureEvent) { +fun PictureCardCaption(event: PictureEvent) { val title = event.title() val content = event.content diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt index 42e436f495..70737df2c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PictureFeedLoaded.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.pictures +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesScreen.kt index 5814aabfaa..f1354cfeb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesScreen.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.pictures +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssemblerSubscription @Composable fun PicturesScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesTopBar.kt index 27d7412c65..ef13c2663c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PicturesTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/PicturesTopBar.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.pictures +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt index a17d0a403d..3846bc8eef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/pictures/datasource/PicturesFilterAssemblerSubscription.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.pictures.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/NewShortVideoButton.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index 6dbe33d697..b7801a93a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/NewShortVideoButton.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.shorts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -42,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.VideoPostKind @@ -52,20 +51,22 @@ import com.vitorpamplona.amethyst.commons.resources.new_short_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaView import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaView import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -94,7 +95,7 @@ fun NewShortVideoButton( var pickedURIs by remember { mutableStateOf>(persistentListOf()) } val scope = rememberCoroutineScope() - val postViewModel: NewMediaModel = viewModel() + val postViewModel: NewMediaModel = rememberViewModel { NewMediaModel() } postViewModel.onceUploaded { scope.launch(Dispatchers.IO) { delay(500) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt index 7dd7b31049..dca890da99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsFeedLoaded.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.shorts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsScreen.kt index 377a4a52bf..d442e7c9b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsScreen.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.shorts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssemblerSubscription @Composable fun ShortsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsTopBar.kt index e07db0d8ff..ba714a03c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/ShortsTopBar.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.shorts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/VideoCardCompose.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/VideoCardCompose.kt index ca053f5158..4f319b2784 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/VideoCardCompose.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.shorts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW @@ -162,7 +162,7 @@ private fun VideoCardImage( } @Composable -internal fun VideoCardCaption(videoEvent: VideoEvent) { +fun VideoCardCaption(videoEvent: VideoEvent) { val event = (videoEvent as? Event) ?: return val title = videoEvent.title() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt index 9a907d347b..4b83599e2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/shorts/datasource/ShortsFilterAssemblerSubscription.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.shorts.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/FileMetadataCardCompose.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/FileMetadataCardCompose.kt index d70ab26391..0f8d9ade67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/FileMetadataCardCompose.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.video +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -145,7 +145,7 @@ private fun FileMetadataCardImage( } @Composable -internal fun FileMetadataCardCaption(videoEvent: FileMetadataEvent) { +fun FileMetadataCardCaption(videoEvent: FileMetadataEvent) { val event = (videoEvent as? Event) ?: return val title = videoEvent.summary() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/NewVideoFeedButton.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/NewVideoFeedButton.kt index 325cfcfbf0..4e51206e5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/NewVideoFeedButton.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.video +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -42,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -52,21 +51,23 @@ import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaModel +import com.vitorpamplona.amethyst.commons.ui.actions.NewMediaView import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaModel -import com.vitorpamplona.amethyst.ui.actions.NewMediaView import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -101,7 +102,7 @@ fun NewVideoFeedButton( val scope = rememberCoroutineScope() - val postViewModel: NewMediaModel = viewModel() + val postViewModel: NewMediaModel = rememberViewModel { NewMediaModel() } postViewModel.onceUploaded { scope.launch(Dispatchers.IO) { delay(500) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/StoriesTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/StoriesTopBar.kt index 4b17731e8e..280c5876df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/StoriesTopBar.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.video +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/UserCardHeader.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/UserCardHeader.kt index 7f22e04a90..5b27795e71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/UserCardHeader.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.video +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/VideoScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/VideoScreen.kt index 33fd756f72..29178fc558 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/VideoScreen.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.video +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PictureCardCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.VideoCardCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PictureCardCompose -import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.VideoCardCompose -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt index 4d5143848c..c11cd36bb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.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.video.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember From e1e64233c96b6dc7770582672c7e8b59f42d419b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 01:54:12 +0000 Subject: [PATCH 08/45] refactor: voice recordings on okio paths; host ports for the note composer - RecordingResult/AnonymizedResult move to commons with okio Path files (commons now exposes okio as api); VoiceAnonymizationController moves to commonsUI and re-voices through AccountViewModelHost.anonymizeVoice. - mediaUriOfFile expect/actual replaces Uri.fromFile in the voice uploads. - AccountViewModelHost gains scheduledPostStore and createWritingAssistant; ShortNotePostViewModel no longer reads Amethyst.instance, Context, UUID or System time. AMETHYST_CLIENT_TAG_NAME moves to commons. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../model/accountsCache/AccountCacheState.kt | 3 +- .../ui/actions/uploads/RecordAudio.kt | 1 + .../ui/actions/uploads/RecordVoiceButton.kt | 1 + .../ui/actions/uploads/VoiceAnonymizer.kt | 17 ++----- .../ui/actions/uploads/VoiceMessagePreview.kt | 13 +++--- .../actions/uploads/VoiceMessageRecorder.kt | 11 ++--- .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../loggedIn/AndroidAccountViewModelHost.kt | 17 +++++++ .../chats/cordnGroup/CordnComposer.kt | 2 +- .../chats/cordnGroup/CordnGroupChatScreen.kt | 12 ++--- .../loggedIn/home/ShortNotePostScreen.kt | 5 +-- .../loggedIn/home/ShortNotePostViewModel.kt | 45 ++++++++++--------- .../loggedIn/home/VoiceReplyViewModel.kt | 26 ++++++----- commons/build.gradle.kts | 5 ++- .../service/uploads/MediaUri.android.kt | 3 ++ .../amethyst/commons/audio/VoiceRecording.kt | 44 ++++++++++++++++++ .../amethyst/commons/model/ClientTag.kt | 24 ++++++++++ .../commons/service/uploads/MediaUri.kt | 5 +++ .../viewmodels/AccountViewModelHost.kt | 19 ++++++++ .../commons/service/uploads/MediaUri.ios.kt | 4 ++ .../commons/service/uploads/MediaUri.jvm.kt | 4 ++ .../audio}/VoiceAnonymizationController.kt | 26 +++++------ 22 files changed, 198 insertions(+), 91 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceRecording.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ClientTag.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio}/VoiceAnonymizationController.kt (84%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index 4157379fb2..d4b9fac806 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.marmot.EncryptedMarmotMessageStore import com.vitorpamplona.amethyst.commons.marmot.EncryptedMlsGroupStateStore import com.vitorpamplona.amethyst.commons.marmot.EncryptedPublishObligationStore import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore +import com.vitorpamplona.amethyst.commons.model.AMETHYST_CLIENT_TAG_NAME import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AccountSettings import com.vitorpamplona.amethyst.commons.model.GeohashIdentityStore @@ -419,7 +420,7 @@ class AccountCacheState( } companion object { - const val CLIENT_TAG_NAME = "Amethyst" + const val CLIENT_TAG_NAME = AMETHYST_CLIENT_TAG_NAME } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index de9d78d496..7ec1097bb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.platform.LocalContext import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.record_a_message_description import com.vitorpamplona.amethyst.commons.ui.components.ToggleableBox diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt index fe3f79176a..cfc50ac5a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.audio.ExpandingCirclesAnimation import com.vitorpamplona.amethyst.commons.audio.FloatingRecordingIndicator +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 0ea2b1c747..bb8e710f24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -25,6 +25,7 @@ import android.media.MediaCodecInfo import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaMuxer +import com.vitorpamplona.amethyst.commons.audio.AnonymizedResult import com.vitorpamplona.amethyst.commons.audio.PitchShifter import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.quartz.utils.Log @@ -33,24 +34,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import okio.Path.Companion.toOkioPath import java.io.File import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.math.abs -/** - * Result of voice anonymization processing. - * - * @property file The output audio file (AAC in MP4 container) - * @property waveform Amplitude data for waveform visualization (one value per second) - * @property duration Audio duration in seconds - */ -data class AnonymizedResult( - val file: File, - val waveform: List, - val duration: Int, -) - /** * Processes audio files to alter voice characteristics for privacy. * @@ -109,7 +98,7 @@ class VoiceAnonymizer { } onProgress(1f) - Result.success(AnonymizedResult(outputFile, waveform, duration)) + Result.success(AnonymizedResult(outputFile.toOkioPath(), waveform, duration)) } catch (e: CancellationException) { // Let cancellation propagate so structured concurrency works (e.g. the // upload screen being dismissed cancels this job). diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index f43c6b5a19..6150807b7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -64,12 +65,12 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import java.io.File +import okio.Path @Composable fun VoiceMessagePreview( voiceMetadata: AudioMeta, - localFile: File? = null, + localFile: Path? = null, onRemove: () -> Unit, onReRecord: ((RecordingResult) -> Unit)? = null, isUploading: Boolean = false, @@ -263,7 +264,7 @@ private fun ReRecordButton( @Composable private fun ManageMediaPlayer( voiceMetadata: AudioMeta, - localFile: File?, + localFile: Path?, onCompletion: () -> Unit, onPlayerChanged: (MediaPlayer?) -> Unit, onRelease: () -> Unit, @@ -371,12 +372,12 @@ private fun handleWaveformScrub( private fun createMediaPlayer( url: String, - localFile: File?, + localFile: Path?, ): MediaPlayer? = try { MediaPlayer().apply { - if (localFile != null && localFile.exists()) { - setDataSource(localFile.absolutePath) + if (localFile != null && localFile.toFile().exists()) { + setDataSource(localFile.toString()) } else { setDataSource(url) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt index 892b84128b..2181d9cd0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt @@ -24,6 +24,7 @@ import android.content.Context import android.media.MediaRecorder import android.os.Build import androidx.media3.common.MimeTypes +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.TimeUtils @@ -35,15 +36,9 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import okio.Path.Companion.toOkioPath import java.io.File -class RecordingResult( - val file: File, - val mimeType: String, - val amplitudes: List, - val duration: Int, -) - @Suppress("DEPRECATION") class VoiceMessageRecorder { @Volatile @@ -145,7 +140,7 @@ class VoiceMessageRecorder { return if (duration >= 1 && file != null) { RecordingResult( - file, + file.toOkioPath(), MimeTypes.AUDIO_AAC, amplitudesCopy, duration, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 9c2cbc2076..c3e68d1927 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -756,7 +756,7 @@ fun ReplyViaVoiceReaction( nav.nav { Route.VoiceReply( replyToNoteId = baseNote.idHex, - recordingFilePath = audio.file.absolutePath, + recordingFilePath = audio.file.toString(), mimeType = audio.mimeType, duration = audio.duration, amplitudes = Json.encodeToString(audio.amplitudes), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index 4aedc816af..ddd888b277 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -25,7 +25,11 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AppModules import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.commons.audio.AnonymizedResult +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStore +import com.vitorpamplona.amethyst.commons.service.ai.WritingAssistant import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure @@ -33,8 +37,10 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost +import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.dismissNotificationForEvent import com.vitorpamplona.amethyst.service.uploads.AndroidMediaUploader +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -49,6 +55,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import okio.Path /** [AccountViewModelHost] over the app's [AppModules]. Lazy services are read on first use. */ class AndroidAccountViewModelHost( @@ -77,6 +84,16 @@ class AndroidAccountViewModelHost( override val deviceLocation: DeviceLocation get() = modules.locationManager + override val scheduledPostStore: ScheduledPostStore get() = modules.scheduledPostStore + + override suspend fun anonymizeVoice( + input: Path, + presetName: String, + ): Result = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName)) + + // Built on the application context: the assistant outlives any one Activity inside the ViewModel. + override fun createWritingAssistant(): WritingAssistant = WritingAssistantFactory.create(modules.appContext) + override val savedAccounts: Flow> = LocalPreferences .accountsFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt index f65fa5ec3b..5050bf3973 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.model.cordnGroups.CordnGroupChatroom import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState @@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index d6eeb8c94a..d39be4e29d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -68,6 +68,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.chats.ui.AutoScrollToNewest import com.vitorpamplona.amethyst.commons.cordn.CordnGroupManager @@ -121,7 +122,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.cordn.CordnMediaService import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessageRecorder import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog @@ -219,7 +219,7 @@ private fun CordnGroupChat( // it after sending. A note recorded and then abandoned — the screen closed, // the room switched — never reached that, so it stayed on disk: an // unencrypted copy of a message that was never even sent. - pendingVoice?.file?.delete() + pendingVoice?.file?.toFile()?.delete() } } var attachError by remember { mutableStateOf(null) } @@ -595,11 +595,11 @@ private fun CordnGroupChat( onVoiceNote = { recording -> // Re-recording replaces what was there; the old file is a temp // file this screen owns, so it goes now rather than being leaked. - pendingVoice?.file?.delete() + pendingVoice?.file?.toFile()?.delete() pendingVoice = recording }, onRemoveVoice = { - pendingVoice?.file?.delete() + pendingVoice?.file?.toFile()?.delete() pendingVoice = null }, attaching = attaching, @@ -1311,7 +1311,7 @@ private suspend fun sendVoiceNote( ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_send_no_session)) try { - val bytes = withContext(Dispatchers.IO) { recording.file.readBytes() } + val bytes = withContext(Dispatchers.IO) { recording.file.toFile().readBytes() } val tag = CordnMediaService(accountViewModel.account) .upload( @@ -1331,7 +1331,7 @@ private suspend fun sendVoiceNote( // invisible to the person who sent it. room.add(session.manager.send(room.gid, content = caption.trim(), tags = arrayOf(tag))) } finally { - withContext(Dispatchers.IO) { recording.file.delete() } + withContext(Dispatchers.IO) { recording.file.toFile().delete() } } } 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 c6be482894..878c44f549 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 @@ -63,7 +63,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -192,7 +191,6 @@ fun ShortNotePostScreen( postViewModel.init(accountViewModel) postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) - val context = LocalContext.current val mediaResolver = rememberSharedMediaResolver() val proposeAiImprovements by @@ -201,8 +199,7 @@ fun ShortNotePostScreen( LaunchedEffect(proposeAiImprovements) { if (proposeAiImprovements == BooleanType.ALWAYS) { - // The assistant outlives this Activity inside the ViewModel, so it must not hold it. - postViewModel.initWritingAssistant(context.applicationContext) + postViewModel.initWritingAssistant() } } 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 fafc19c91c..b8b42ac905 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -33,8 +32,10 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.RecordingResult +import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController import com.vitorpamplona.amethyst.commons.audio.VoicePreset +import com.vitorpamplona.amethyst.commons.model.AMETHYST_CLIENT_TAG_NAME import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.BooleanType @@ -77,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.mediaUriOfFile import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -89,11 +91,8 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource +import com.vitorpamplona.amethyst.commons.util.platformFileSystem import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState -import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent @@ -188,7 +187,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import java.util.UUID +import okio.Path +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid enum class UserSuggestionAnchor { MAIN_MESSAGE, @@ -287,7 +288,7 @@ open class ShortNotePostViewModel : // Voice Messages var voiceRecording by mutableStateOf(null) - var voiceLocalFile by mutableStateOf(null) + var voiceLocalFile by mutableStateOf(null) var isUploadingVoice by mutableStateOf(false) var voiceMetadata by mutableStateOf(null) var voiceSelectedServer by mutableStateOf(null) @@ -305,9 +306,10 @@ open class ShortNotePostViewModel : error, ) }, + anonymize = { file, preset -> accountViewModel.host.anonymizeVoice(file, preset.name) }, ) - val activeFile: java.io.File? + val activeFile: Path? get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? @@ -478,9 +480,9 @@ open class ShortNotePostViewModel : accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements.value == BooleanType.ALWAYS - fun initWritingAssistant(context: Context) { + fun initWritingAssistant() { if (writingAssistant != null) return - writingAssistant = WritingAssistantFactory.create(context) + writingAssistant = accountViewModel.host.createWritingAssistant() ?: return refreshAiStatus() } @@ -1260,21 +1262,23 @@ open class ShortNotePostViewModel : * Runs on the mining queue's scope when PoW is on, so it must not touch * viewModelScope. */ + @OptIn(ExperimentalUuidApi::class) private suspend fun storeScheduledPost( template: EventTemplate, extraNotesToBroadcast: List, publishAtSec: Long, ) { val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast) - Amethyst.instance.scheduledPostStore.add( + val store = accountViewModel.host.scheduledPostStore ?: return + store.add( ScheduledPost( - id = UUID.randomUUID().toString(), + id = Uuid.random().toString(), accountPubkey = event.pubKey, signedEventJson = event.toJson(), relayUrls = relays.map { it.url }, extraEventsJson = extras.map { it.toJson() }, publishAtSec = publishAtSec, - createdAtSec = System.currentTimeMillis() / 1000, + createdAtSec = TimeUtils.now(), ), ) } @@ -1570,7 +1574,7 @@ open class ShortNotePostViewModel : val replyingToEvent = originalNote?.event ?: return false return replyingToEvent is TextNoteEvent && replyingToEvent.isNewThread() && - replyingToEvent.isClient(AccountCacheState.CLIENT_TAG_NAME) + replyingToEvent.isClient(AMETHYST_CLIENT_TAG_NAME) } fun upload( @@ -1916,14 +1920,13 @@ open class ShortNotePostViewModel : voiceOrchestrator = null } - private fun deleteVoiceLocalFile(toDelete: java.io.File? = voiceLocalFile) { + private fun deleteVoiceLocalFile(toDelete: Path? = voiceLocalFile) { toDelete?.let { file -> try { - if (file.delete()) { - Log.d("ShortNotePostViewModel") { "Deleted voice file: ${file.absolutePath}" } - } + platformFileSystem.delete(file, mustExist = false) + Log.d("ShortNotePostViewModel") { "Deleted voice file: $file" } } catch (e: Exception) { - Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e) + Log.w("ShortNotePostViewModel", "Failed to delete voice file: $file", e) } } } @@ -1942,7 +1945,7 @@ open class ShortNotePostViewModel : isUploadingVoice = true try { - val uri = android.net.Uri.fromFile(fileToUpload) + val uri = mediaUriOfFile(fileToUpload) val orchestrator = UploadOrchestrator() voiceOrchestrator = orchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 1f1e65aca0..287b79bcc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -27,6 +27,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.RecordingResult +import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -41,11 +43,10 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.mediaUriOfFile import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.commons.util.deleteOrWarn +import com.vitorpamplona.amethyst.commons.util.platformFileSystem import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags @@ -61,7 +62,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import java.io.File +import okio.Path +import okio.Path.Companion.toPath @Stable class VoiceReplyViewModel : ViewModel() { @@ -70,7 +72,7 @@ class VoiceReplyViewModel : ViewModel() { var replyToNote: Note? by mutableStateOf(null) var voiceRecording: RecordingResult? by mutableStateOf(null) - var voiceLocalFile: File? by mutableStateOf(null) + var voiceLocalFile: Path? by mutableStateOf(null) var voiceMetadata: AudioMeta? by mutableStateOf(null) var voiceSelectedServer: ServerName? by mutableStateOf(null) var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null) @@ -89,9 +91,10 @@ class VoiceReplyViewModel : ViewModel() { ) } }, + anonymize = { file, preset -> accountViewModel.host.anonymizeVoice(file, preset.name) }, ) - val activeFile: File? + val activeFile: Path? get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? @@ -126,7 +129,7 @@ class VoiceReplyViewModel : ViewModel() { emptyList() } - val file = File(recordingFilePath) + val file = recordingFilePath.toPath() voiceLocalFile = file voiceRecording = RecordingResult( @@ -164,11 +167,10 @@ class VoiceReplyViewModel : ViewModel() { private fun deleteVoiceLocalFile() { voiceLocalFile?.let { file -> try { - if (file.deleteOrWarn("VoiceReplyViewModel", "voice file")) { - Log.d("VoiceReplyViewModel") { "Voice file removed or already gone: ${file.absolutePath}" } - } + platformFileSystem.delete(file, mustExist = false) + Log.d("VoiceReplyViewModel") { "Voice file removed or already gone: $file" } } catch (e: Exception) { - Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e) + Log.w("VoiceReplyViewModel", "Failed to delete voice file: $file", e) } } } @@ -195,7 +197,7 @@ class VoiceReplyViewModel : ViewModel() { try { val result = withContext(Dispatchers.IO) { - val uri = android.net.Uri.fromFile(fileToUpload) + val uri = mediaUriOfFile(fileToUpload) orchestrator.upload( uri = uri, mimeType = recording.mimeType, diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 86c5c10c33..5903154980 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -87,8 +87,9 @@ kotlin { // the ForwardingFileSystem the apps wrap Coil's disk cache in. It // used to arrive transitively through Coil; with Coil in // :commonsUI the Apple targets lost it (JVM still saw it via - // OkHttp), so declare the dependency the file actually has. - implementation(libs.okio) + // OkHttp), so declare the dependency the file actually has. `api` + // because voice recordings cross module boundaries as okio Paths. + api(libs.okio) // DataStore (KMP, Apache-2.0) — the preference storage layer. // Publishes android/jvm/ios/linux/macos variants plus common diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt index b38e335f3f..53d0e23e82 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt @@ -21,7 +21,10 @@ package com.vitorpamplona.amethyst.commons.service.uploads import android.net.Uri +import okio.Path actual typealias MediaUri = Uri actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegment + +actual fun mediaUriOfFile(path: Path): MediaUri = Uri.fromFile(path.toFile()) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceRecording.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceRecording.kt new file mode 100644 index 0000000000..7529c01300 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceRecording.kt @@ -0,0 +1,44 @@ +/* + * 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.commons.audio + +import okio.Path + +/** A finished voice-message recording: the audio file, its type, and its waveform. */ +class RecordingResult( + val file: Path, + val mimeType: String, + val amplitudes: List, + val duration: Int, +) + +/** + * A recording re-voiced for privacy. + * + * @property file The output audio file (AAC in MP4 container) + * @property waveform Amplitude data for waveform visualization (one value per second) + * @property duration Audio duration in seconds + */ +data class AnonymizedResult( + val file: Path, + val waveform: List, + val duration: Int, +) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ClientTag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ClientTag.kt new file mode 100644 index 0000000000..e480bcc672 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ClientTag.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.model + +/** The name this app signs into NIP-89 `client` tags, and recognizes on events it wrote. */ +const val AMETHYST_CLIENT_TAG_NAME = "Amethyst" diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt index b4de3123e9..c11127132b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.service.uploads +import okio.Path + /** * A picked file's address as the platform hands it over: Android's `content://` [android.net.Uri] * (a typealias there, so app code keeps reading it as a `Uri`), a string-backed subclass @@ -30,6 +32,9 @@ expect abstract class MediaUri { abstract override fun toString(): String } +/** The address of a local file, such as a voice recording the app wrote itself. */ +expect fun mediaUriOfFile(path: Path): MediaUri + /** The last path segment of this address, if it has one (on Android, `Uri.lastPathSegment`). */ expect fun MediaUri.lastPathSegmentOrNull(): String? diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 4cbef6742e..6eb5d1830c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.amethyst.commons.viewmodels +import com.vitorpamplona.amethyst.commons.audio.AnonymizedResult import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStore +import com.vitorpamplona.amethyst.commons.service.ai.WritingAssistant import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader @@ -34,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import okio.Path /** * What the account's ViewModel needs from the process it runs in: app-wide services that outlive @@ -74,6 +78,21 @@ interface AccountViewModelHost { /** The device's position as geohashes. */ val deviceLocation: DeviceLocation get() = DeviceLocation.None + /** Posts signed now and published later by a background worker; null where scheduling is unsupported. */ + val scheduledPostStore: ScheduledPostStore? get() = null + + /** + * Re-voices the recording at [input] with the voice preset named [presetName], into a new file + * beside it. + */ + suspend fun anonymizeVoice( + input: Path, + presetName: String, + ): Result = Result.failure(UnsupportedOperationException("Voice anonymization is not available on this platform")) + + /** A new on-device writing assistant, or null where the platform has none. */ + fun createWritingAssistant(): WritingAssistant? = null + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt index a8e33c608e..0fe4f5cc41 100644 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.service.uploads +import okio.Path + actual abstract class MediaUri { actual abstract override fun toString(): String } @@ -38,3 +40,5 @@ class StringMediaUri( } actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegmentOf(toString()) + +actual fun mediaUriOfFile(path: Path): MediaUri = StringMediaUri(path.toString()) diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt index a8e33c608e..0fe4f5cc41 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.service.uploads +import okio.Path + actual abstract class MediaUri { actual abstract override fun toString(): String } @@ -38,3 +40,5 @@ class StringMediaUri( } actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegmentOf(toString()) + +actual fun mediaUriOfFile(path: Path): MediaUri = StringMediaUri(path.toString()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceAnonymizationController.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceAnonymizationController.kt index 3f3a70fb4c..7fe80b0f86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoiceAnonymizationController.kt @@ -18,22 +18,24 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.audio import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.commons.audio.VoicePreset +import com.vitorpamplona.amethyst.commons.util.platformFileSystem import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import java.io.File +import okio.Path class VoiceAnonymizationController( private val scope: CoroutineScope, private val logTag: String, private val onError: (Throwable) -> Unit, + /** Re-voices a recording with a preset; the platform's audio pipeline, reached through the host. */ + private val anonymize: suspend (Path, VoicePreset) -> Result, ) { var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) private set @@ -44,7 +46,7 @@ class VoiceAnonymizationController( private var processingJob: Job? = null - fun activeFile(originalFile: File?): File? = + fun activeFile(originalFile: Path?): Path? = if (selectedPreset == VoicePreset.NONE) { originalFile } else { @@ -60,7 +62,7 @@ class VoiceAnonymizationController( fun selectPreset( preset: VoicePreset, - originalFile: File?, + originalFile: Path?, ) { Log.d(logTag) { "selectPreset called with: ${preset.name}, pitchFactor: ${preset.pitchFactor}" } if (processingPreset != null || preset == selectedPreset) return @@ -82,8 +84,7 @@ class VoiceAnonymizationController( processingJob = scope.launch { try { - val anonymizer = VoiceAnonymizer() - val result = anonymizer.anonymize(file, preset) + val result = anonymize(file, preset) result .onSuccess { anonymizedResult -> @@ -109,15 +110,10 @@ class VoiceAnonymizationController( fun deleteDistortedFiles() { distortedFiles.values.forEach { result -> try { - if (result.file.exists()) { - if (result.file.delete()) { - Log.d(logTag) { "Deleted distorted file: ${result.file.absolutePath}" } - } else { - Log.w(logTag) { "Failed to delete distorted file: ${result.file.absolutePath}" } - } - } + platformFileSystem.delete(result.file, mustExist = false) + Log.d(logTag) { "Deleted distorted file: ${result.file}" } } catch (e: Exception) { - Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e) + Log.w(logTag, "Failed to delete distorted file: ${result.file}", e) } } distortedFiles = emptyMap() From 8d83b26805e28db069d9901133b2ec864bfb17d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 02:15:30 +0000 Subject: [PATCH 09/45] refactor: move the public, ephemeral, geohash and DM chat screens 39 files freed by the voice and composer ports move to commonsUI: PublicChatChannelScreen, EphemeralChatScreen, GeohashChatScreen, ChatroomByAuthorScreen and BuzzForumThreadScreen with their feeds, top bars, message view models, edit rows and upload dialogs, plus ShortNotePostViewModel and IMetaAttachments. The feed views build their view models with rememberViewModel; the upload dialog uses the shared edge-to-edge dialog properties. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/actions/EditPostViewModel.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 10 ++++---- .../location/DisplayLocationObserver.kt | 1 + .../creators/location/GeoHashPostSection.kt | 1 + .../note/creators/location/LocationAsHash.kt | 1 + .../note/creators/polls/PollDeadlinePicker.kt | 2 +- .../note/creators/polls/PollOptionsField.kt | 2 +- .../zappolls/ZapPollConsensusThreshold.kt | 2 +- .../zappolls/ZapPollDeadlinePicker.kt | 2 +- .../ui/note/creators/zappolls/ZapPollField.kt | 2 +- .../note/creators/zappolls/ZapPollOption.kt | 2 +- .../zappolls/ZapPollVoteValueRange.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 6 ++--- .../chats/cordnGroup/CordnGroupChatScreen.kt | 4 ++-- .../chats/marmotGroup/MarmotGroupChatView.kt | 8 +++---- .../marmotGroup/MarmotSystemRowRenderer.kt | 2 +- .../marmotGroup/send/MarmotFileUploader.kt | 2 +- .../send/MarmotNewMessageViewModel.kt | 2 +- .../loggedIn/chats/minichat/MinichatScreen.kt | 6 ++--- .../chats/privateDM/ChatroomScreen.kt | 3 ++- .../chats/privateDM/send/NewGroupDMScreen.kt | 24 +++++++++++-------- .../concord/ConcordChannelScreen.kt | 14 +++++------ .../send/ConcordNewMessageViewModel.kt | 2 +- .../header/EphemeralChatChannelHeader.kt | 1 + .../nip53LiveActivities/ChannelView.kt | 10 ++++---- .../relayGroup/RelayGroupChannelView.kt | 10 ++++---- .../chats/rooms/twopane/MessagesTwoPane.kt | 4 ++-- .../loggedIn/chats/utils/UploadImetas.kt | 4 ++-- .../nip23LongForm/LongFormPostViewModel.kt | 6 ++--- .../nip99Classifieds/NewProductViewModel.kt | 6 ++--- .../loggedIn/home/ShortNotePostScreen.kt | 1 + .../nests/room/chat/NestEditFieldRow.kt | 2 +- .../nests/room/chat/NestFileUploadDialog.kt | 4 ++-- .../room/chat/NestNewMessageViewModel.kt | 8 +++---- .../NewPublicMessageViewModel.kt | 6 ++--- .../screen/loggedIn/polls/PollPostScreen.kt | 2 +- .../publicChannels/concord/ConcordUnread.kt | 2 +- .../model/composer/AudienceSelection.kt | 2 +- .../creators/location/ILocationGrabber.kt | 2 +- .../note/elements/AddInboxRelayForDMCard.kt | 4 ++-- .../loggedIn/buzz/BuzzForumThreadScreen.kt | 8 +++---- .../buzz/ForumReplyNewMessageViewModel.kt | 4 ++-- .../loggedIn/chats/feed/ChatFeedView.kt | 2 +- .../chats/geohashChat/GeohashChatScreen.kt | 21 ++++++++-------- .../chats/geohashChat/GeohashChatViewModel.kt | 3 ++- .../chats/geohashChat/LoadGeohashChannel.kt | 2 +- .../chats/privateDM/ChatroomByAuthorScreen.kt | 4 ++-- .../loggedIn/chats/privateDM/ChatroomView.kt | 20 ++++++++-------- .../privateDM/dal/ChatroomFeedViewModel.kt | 9 +++++-- .../ChatroomFilterAssemblerSubscription.kt | 2 +- .../header/NewChatroomSubjectDialog.kt | 2 +- .../privateDM/header/RenderRoomTopBar.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 15 ++++++------ .../chats/privateDM/send/IMetaAttachments.kt | 2 +- .../send/PrivateMessageEditFieldRow.kt | 10 ++++---- .../privateDM/send/upload/ChatFileSender.kt | 4 ++-- .../send/upload/ChatFileUploadDialog.kt | 8 +++---- .../privateDM/send/upload/ChatFileUploader.kt | 4 ++-- .../send/upload/SuccessfulUploads.kt | 2 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../dal/ChannelFeedViewModel.kt | 9 +++++-- .../ChannelFilterAssemblerSubscription.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 18 +++++++------- .../ephemChat/EphemeralChatScreen.kt | 4 ++-- .../ephemChat/header/EphemeralChatTopBar.kt | 2 +- .../header/ShortEphemeralChatChannelHeader.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 18 +++++++------- .../PublicChatChannelScreen.kt | 4 ++-- .../header/PublicChatTopBar.kt | 2 +- .../send/ChannelFileUploadDialog.kt | 6 ++--- .../send/ChannelNewMessageViewModel.kt | 11 +++++---- .../chats/publicChannels/send/EditFieldRow.kt | 4 ++-- .../chats/utils/ChatFileUploadDialog.kt | 11 +++------ .../chats/utils/ChatFileUploadState.kt | 2 +- .../chats/utils/DisplayReplyingToNote.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 7 +++--- .../relays/dm/AddDMRelayListDialog.kt | 6 ++--- 77 files changed, 216 insertions(+), 196 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/location/ILocationGrabber.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/elements/AddInboxRelayForDMCard.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/feed/ChatFeedView.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt (84%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt (83%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt (85%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt (85%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/ShortNotePostViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt (95%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 68377fd17c..8f24845dad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -48,12 +48,12 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessin import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 1dd170120f..a2bed180ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentWorkBoard import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzCanvasScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzForumPostScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzForumThreadScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen @@ -94,9 +95,11 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoa import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordEditScreen @@ -104,7 +107,9 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteLinksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordMembersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupDiscoveryScreen @@ -250,7 +255,6 @@ import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzForumThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen @@ -260,19 +264,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEv import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.minichat.MinichatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt index 39f808170d..94a7c6320f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions import com.vitorpamplona.amethyst.commons.resources.loading_location import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt index 583b757747..91da97cb38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.resources.geohash_title import com.vitorpamplona.amethyst.commons.resources.location_change_place import com.vitorpamplona.amethyst.commons.resources.location_pick_on_map import com.vitorpamplona.amethyst.commons.resources.remove_location +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt index 73dc3a764e..b29d243949 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.geohash_explainer import com.vitorpamplona.amethyst.commons.resources.geohash_title import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt index 7ec5a1187d..03642f27a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.utils.TimeUtils import java.time.Instant import java.time.ZoneId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt index abb263f2dc..3d071a9286 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt @@ -49,9 +49,9 @@ import com.vitorpamplona.amethyst.commons.resources.poll_multiple_choice import com.vitorpamplona.amethyst.commons.resources.poll_option_description import com.vitorpamplona.amethyst.commons.resources.poll_option_index import com.vitorpamplona.amethyst.commons.resources.poll_single_choice +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType import com.vitorpamplona.quartz.utils.RandomInstance diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt index c05c747b13..4f8273b3e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt @@ -42,9 +42,9 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.poll_consensus_threshold import com.vitorpamplona.amethyst.commons.resources.poll_consensus_threshold_percent +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import kotlinx.coroutines.CancellationException @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index a961fd4d82..5b80cbd6ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.utils.TimeUtils import java.time.Instant import java.time.ZoneId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt index 429ff6d7ff..6227329a10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add_poll_option_button +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag import com.vitorpamplona.quartz.utils.RandomInstance diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt index aa66a0e2a4..c634c455a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt @@ -36,9 +36,9 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.clear import com.vitorpamplona.amethyst.commons.resources.poll_option_description import com.vitorpamplona.amethyst.commons.resources.poll_option_index +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel @Composable fun ZapPollOption( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt index 14787eedab..f9770d7e7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt @@ -42,10 +42,10 @@ import com.vitorpamplona.amethyst.commons.resources.poll_zap_value_max import com.vitorpamplona.amethyst.commons.resources.poll_zap_value_min import com.vitorpamplona.amethyst.commons.resources.poll_zap_value_min_max_explainer import com.vitorpamplona.amethyst.commons.resources.sats +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel @Composable fun ZapPollVoteValueRange(pollViewModel: ShortNotePostViewModel) { 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 5b05122de6..d8b4a06ade 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 @@ -61,10 +61,13 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.appendSignature import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -73,9 +76,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index d39be4e29d..55689c5623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -116,6 +116,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText @@ -124,8 +126,6 @@ import com.vitorpamplona.amethyst.model.cordn.CordnMediaService import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessageRecorder import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnBlobUpload import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaAttachment import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaCipher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 2a05f95f8b..11d5f39f13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -76,6 +76,10 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatIsOneOnOne +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -85,13 +89,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.EditingMessageBanner import com.vitorpamplona.quartz.marmot.protocolCore.LocalOutboundGate import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt index ea5571d5a4..0f4351f6bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt @@ -50,9 +50,9 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_system_member_added_p import com.vitorpamplona.amethyst.commons.resources.marmot_system_member_left import com.vitorpamplona.amethyst.commons.resources.marmot_system_member_removed import com.vitorpamplona.amethyst.commons.resources.marmot_system_member_removed_passive +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatFeedRowRenderer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatFeedRowRenderer import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotAppEvent import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemEvent import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotSystemType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt index 004bfa0a64..371626de80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaReferenceV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaV2Cipher diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt index edc31b853a..1324b88e5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt @@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index 0cb84787cf..b2724acb54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -74,17 +74,17 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalSuppressReplyToNoteId +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toConcordImeta import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toPlainImetas import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt index 76540a7d80..78eefa0c0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -29,10 +29,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.ChatroomView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.call.CallActivity import com.vitorpamplona.amethyst.ui.call.rememberCallWithPermission -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType 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 99d2db483c..cf60a83d21 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 @@ -115,6 +115,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.EncryptedUploadErrorDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.RecipientMissingRelaysWarning +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -133,7 +137,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -622,13 +625,14 @@ fun ShowImageUploadGallery( ) { val isImage = data.result.mimeTypeBeforeEncryption?.startsWith("image/") == true || RichTextParser.isImageUrl(data.result.url) - if (data.cipher != null) { - accountViewModel.account.encryptionKeyCache.add(data.result.url, data.cipher, data.result.mimeTypeBeforeEncryption) + val cipher = data.cipher + if (cipher != null) { + accountViewModel.account.encryptionKeyCache.add(data.result.url, cipher, data.result.mimeTypeBeforeEncryption) } val content by remember(data) { mutableStateOf( - if (data.cipher != null) { + if (cipher != null) { if (isImage) { EncryptedMediaUrlImage( url = data.result.url, @@ -640,9 +644,9 @@ fun ShowImageUploadGallery( dim = data.result.fileHeader.dim, uri = null, mimeType = data.result.mimeTypeBeforeEncryption, - encryptionAlgo = data.cipher.name(), - encryptionKey = data.cipher.keyBytes, - encryptionNonce = data.cipher.nonce, + encryptionAlgo = cipher.name(), + encryptionKey = cipher.keyBytes, + encryptionNonce = cipher.nonce, thumbhash = data.result.fileHeader.thumbHash ?.thumbhash, @@ -658,9 +662,9 @@ fun ShowImageUploadGallery( dim = data.result.fileHeader.dim, uri = null, mimeType = data.result.mimeTypeBeforeEncryption, - encryptionAlgo = data.cipher.name(), - encryptionKey = data.cipher.keyBytes, - encryptionNonce = data.cipher.nonce, + encryptionAlgo = cipher.name(), + encryptionKey = cipher.keyBytes, + encryptionNonce = cipher.nonce, thumbhash = data.result.fileHeader.thumbHash ?.thumbhash, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 17a2b090c3..67681cc99f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -90,12 +90,19 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.concordTimerText +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinDuties import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinnedButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinnedMessagesSheet import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordChannelPins +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -105,15 +112,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.send.ConcordNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.EditingMessageBanner import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toConcordImeta import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt index aa5cf9c9fb..bb518a94e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt index 607b791ad4..d5bb8da50a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.ShortEphemeralChatChannelHeader import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 0c469c4510..cae2d832be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -34,17 +34,17 @@ import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActiviti import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.lobby.NestJoinCard import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamGoalHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamTopZappers -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.quartz.nip01Core.core.Address @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt index 1b2cb77630..ab8d4a4ec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt @@ -57,17 +57,17 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachSentinels import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistorySubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatTailSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 4a6e0a8ddb..4ab05433ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.TwoPaneNav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.ChatroomView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.topbars.AmethystClickableIcon -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.WarmJoinedRelayGroupNip11 @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt index 17ab40f078..820a8b48e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import com.vitorpamplona.quartz.concord.cord03Channels.ChannelChat import com.vitorpamplona.quartz.nip92IMeta.IMetaTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 623388bce5..48c4e79d3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -61,9 +61,12 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.appendSignature import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -72,9 +75,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate 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 a923c1b1c3..2c3d44adee 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 @@ -58,18 +58,18 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl 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 878c44f549..f341fdcddc 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 @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.onUiThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index 1cba3aaacf..d61fcfa288 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.FlowPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt index 111e99080d..dbbcbf63c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt @@ -33,10 +33,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.UserPicture +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 80e8fa567d..a8d888ec2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -54,17 +54,17 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent 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 a7611652b2..11535fc55c 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 @@ -60,18 +60,18 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt index adecdff72b..2aa522582a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -27,9 +27,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.FlowPreview diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/publicChannels/concord/ConcordUnread.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/publicChannels/concord/ConcordUnread.kt index 4e4f6bb5f0..02a6de045d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/publicChannels/concord/ConcordUnread.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/publicChannels/concord/ConcordUnread.kt @@ -100,7 +100,7 @@ fun concordCommunityHasUnreadFlow( /** * True for a note the Concord channel *timeline* actually renders — the same predicate as - * [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedFilter]'s + * [com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedFilter]'s * `isTimelineMessage`: a loaded, acceptable message that is **not** a kind-1111 [CommentEvent]. * * A [CommentEvent] is a *minichat thread reply* that lives inside its parent's thread, not on the diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/AudienceSelection.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/AudienceSelection.kt index e5fdb7dd1f..6b869c1ff6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/AudienceSelection.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/AudienceSelection.kt @@ -31,7 +31,7 @@ import kotlinx.collections.immutable.persistentListOf * of the Account so they can be unit tested on the JVM. * * The picker never mutates anything itself: it produces a set of pubkeys the - * screen hands to [com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel.addAllToReplyList]. + * screen hands to [com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel.addAllToReplyList]. */ enum class AudienceListKind { /** NIP-51 kind 30000 people list. Can carry encrypted (private) members. */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/ILocationGrabber.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/ILocationGrabber.kt index 89945df357..5df7c2f8fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/ILocationGrabber.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/ILocationGrabber.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.note.creators.location +package com.vitorpamplona.amethyst.commons.ui.note.creators.location import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.LocationResult diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/AddInboxRelayForDMCard.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/AddInboxRelayForDMCard.kt index 6113e1c9a1..5cf4605403 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/AddInboxRelayForDMCard.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.note.elements +package com.vitorpamplona.amethyst.commons.ui.note.elements import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.dm_relays_not_found_descript import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm.AddDMRelayListDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BigPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding @@ -53,7 +54,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.AddDMRelayListDialog import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt index c75688b1cf..32b6d57ee1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzForumThreadScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -43,7 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -51,16 +50,17 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_forum_no_replies import com.vitorpamplona.amethyst.commons.resources.buzz_forum_thread_title import com.vitorpamplona.amethyst.commons.resources.chat_minichat_reply_count +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -120,7 +120,7 @@ fun BuzzForumThreadScreen( val canPost = channel.canPost(accountViewModel.userProfile().pubkeyHex) - val replyModel: ForumReplyNewMessageViewModel = viewModel(key = "forum-reply-$rootId") + val replyModel: ForumReplyNewMessageViewModel = rememberViewModel(key = "forum-reply-$rootId") { ForumReplyNewMessageViewModel() } replyModel.init(accountViewModel) replyModel.loadForumThread(channel, rootId) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt index d7845566f9..aef799db52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt @@ -18,11 +18,11 @@ * 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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatFeedView.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index a21898eb65..aea25619aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/ChatFeedView.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.chats.feed +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index 51c8e98202..ff511b6eab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.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.chats.geohashChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -55,7 +55,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.location.LocationResult @@ -63,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserPicture import com.vitorpamplona.amethyst.commons.search.SearchSeed import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold @@ -77,16 +77,17 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalCha import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatReactOverride import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatShowSelfAuthorName import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatSuppressGeohash +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.withContext import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon @@ -129,17 +130,17 @@ private fun GeohashChatRoom( val relays = remember(channel) { channel.relays().toList() } // Identity/reactions helper (per-cell pubkeys + anonymous react). - val identity: GeohashChatViewModel = viewModel(key = "GeohashIdentity/$geohash") + val identity: GeohashChatViewModel = rememberViewModel(key = "GeohashIdentity/$geohash") { GeohashChatViewModel() } identity.init(geohash, accountViewModel) val feedViewModel: ChannelFeedViewModel = - viewModel( + rememberViewModel( key = "geohash:${geohash}ChannelFeedViewModel", factory = ChannelFeedViewModel.Factory(channel, accountViewModel.account), ) // The shared composer, made geohash-aware (per-cell signer + PoW + n/t tags) in its send path. - val newMessageModel: ChannelNewMessageViewModel = viewModel(key = "geohash:${geohash}NewMessage") + val newMessageModel: ChannelNewMessageViewModel = rememberViewModel(key = "geohash:${geohash}NewMessage") { ChannelNewMessageViewModel() } newMessageModel.init(accountViewModel) newMessageModel.load(channel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt index 6f26ea4fdc..e15e1a8e96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.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.chats.geohashChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt index 61988db27d..cff1509ea8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.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.chats.geohashChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt index 5b707cb16e..64b9e055d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.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.chats.privateDM +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -30,9 +30,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.amethyst.commons.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.collections.immutable.persistentSetOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index f9f8b86e19..4852925074 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/ChatroomView.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.chats.privateDM +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -36,7 +36,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -47,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04 import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip17 import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog @@ -56,16 +56,16 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachState import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.DmReportWarningCard +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.PrivateMessageEditFieldRow import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -91,7 +91,7 @@ fun ChatroomView( nav: INav, ) { val feedViewModel: ChatroomFeedViewModel = - viewModel( + rememberViewModel( key = room.hashCode().toString() + "ChatroomViewModels", factory = ChatroomFeedViewModel.Factory( @@ -100,7 +100,7 @@ fun ChatroomView( ), ) - val newPostModel: ChatNewMessageViewModel = viewModel() + val newPostModel: ChatNewMessageViewModel = rememberViewModel { ChatNewMessageViewModel() } newPostModel.init(accountViewModel) newPostModel.load(room) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 71187b8101..3b63ba4dd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.chats.privateDM.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.feeds.ChatroomFeedFilter import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import kotlin.reflect.KClass class ChatroomFeedViewModel( val user: ChatroomKey, @@ -40,6 +42,9 @@ class ChatroomFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = ChatroomFeedViewModel(user, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = ChatroomFeedViewModel(user, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt index 09aa97fc9a..75ec7e2507 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssemblerSubscription.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.chats.privateDM.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt index 0314a3d947..e1ead0a8e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.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.chats.privateDM.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt index 1d39484d2a..75c9e9d497 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.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.chats.privateDM.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 2f7bf5b5f5..b99fa90af4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.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.chats.privateDM.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -49,20 +49,20 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestio import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash @@ -102,6 +102,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index 420543551e..b2110656e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.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.chats.privateDM.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 2095011e6a..dff74285d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.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.chats.privateDM.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -47,7 +47,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -66,12 +65,15 @@ import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformat import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -88,15 +90,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.showCount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Preview @Composable fun PrivateMessageEditFieldRowPreview() { - val channelScreenModel: ChatNewMessageViewModel = viewModel() + val channelScreenModel: ChatNewMessageViewModel = rememberViewModel { ChatNewMessageViewModel() } val accountViewModel = mockAccountViewModel() channelScreenModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index ee873c8af1..4e8adec194 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -18,12 +18,12 @@ * 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.chats.privateDM.send.upload +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 97bacf2bca..178a9aeaa7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.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.chats.privateDM.send.upload +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -31,11 +31,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RoomNameOnlyDisplay +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @Composable fun RoomChatFileUploadDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 65926bac51..00dbfe83e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.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.chats.privateDM.send.upload +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.resources.Res @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.utils.ciphers.AESGCM class ChatFileUploader( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt index f7693c91c6..94019958d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.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.chats.privateDM.send.upload +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index e057199a37..63787cd75d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.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.chats.publicChannels.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt index ccf3ffec0c..503397b49a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt @@ -18,14 +18,16 @@ * 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.chats.publicChannels.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel +import kotlin.reflect.KClass class ChannelFeedViewModel( val channel: Channel, @@ -36,6 +38,9 @@ class ChannelFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = ChannelFeedViewModel(channel, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = ChannelFeedViewModel(channel, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt index 6cac602664..5eb40aeaee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.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.chats.publicChannels.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 5d56b38b0f..8445dcfe4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.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.chats.publicChannels.ephemChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -29,19 +29,19 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @Composable @@ -74,7 +74,7 @@ private fun PrepareChannelViewModels( nav: INav, ) { val feedViewModel: ChannelFeedViewModel = - viewModel( + rememberViewModel( key = baseChannel.roomId.toKey() + "ChannelFeedViewModel", factory = ChannelFeedViewModel.Factory( @@ -83,7 +83,7 @@ private fun PrepareChannelViewModels( ), ) - val channelScreenModel: ChannelNewMessageViewModel = viewModel() + val channelScreenModel: ChannelNewMessageViewModel = rememberViewModel { ChannelNewMessageViewModel() } channelScreenModel.init(accountViewModel) channelScreenModel.load(baseChannel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 2361f1486e..095bbd946a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.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.chats.publicChannels.ephemChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt index eb8e58b8ec..f5f8d4c62e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.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.chats.publicChannels.ephemChat.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.header import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index b34632879d..21a23a28b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.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.chats.publicChannels.ephemChat.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.header import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 9b19514b26..b13681eb89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.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.chats.publicChannels.nip28PublicChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -29,19 +29,19 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow @Composable fun PublicChatChannelView( @@ -73,7 +73,7 @@ fun PrepareChannelViewModels( nav: INav, ) { val feedViewModel: ChannelFeedViewModel = - viewModel( + rememberViewModel( key = baseChannel.idHex + "ChannelFeedViewModel", factory = ChannelFeedViewModel.Factory( @@ -82,7 +82,7 @@ fun PrepareChannelViewModels( ), ) - val channelScreenModel: ChannelNewMessageViewModel = viewModel() + val channelScreenModel: ChannelNewMessageViewModel = rememberViewModel { ChannelNewMessageViewModel() } channelScreenModel.init(accountViewModel) channelScreenModel.load(baseChannel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 6f0133c242..635b3dcab4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.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.chats.publicChannels.nip28PublicChat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -31,8 +31,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar import com.vitorpamplona.quartz.nip01Core.core.HexKey @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index 95534cbd1e..dfcdd1aaa4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.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.chats.publicChannels.nip28PublicChat.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 539f57af85..e1372c277f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.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.chats.publicChannels.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -41,12 +41,12 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @Composable fun ChannelFileUploadDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index c5a59ffdf0..4b3b3499c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.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.chats.publicChannels.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -63,7 +63,11 @@ import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -71,10 +75,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.buzz.stream.mentions @@ -130,6 +130,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index a28d6db47f..fb82bae890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.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.chats.publicChannels.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.FlowPreview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index 488b3b7ea2..31cfea488d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.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.chats.utils +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -52,7 +52,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accessibility_send @@ -76,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGall import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.components.edgeToEdgeDialogProperties import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar @@ -112,12 +112,7 @@ fun ChatFileUploadDialog( Dialog( onDismissRequest = { onCancel() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), + properties = edgeToEdgeDialogProperties(dismissOnClickOutside = false), ) { SetDialogToEdgeToEdge() Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 2703b316e1..03f799130d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/ChatFileUploadState.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.chats.utils +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index d60b71de7d..16802c20c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.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.chats.utils +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index b8b42ac905..261d1d2f9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostViewModel.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -80,10 +80,12 @@ import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.service.uploads.mediaUriOfFile import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.commons.ui.text.appendSignature import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -93,8 +95,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginni import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.util.platformFileSystem import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent @@ -179,6 +179,7 @@ import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index 35c0c20484..be1681d4bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.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.relays.dm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.dm import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -36,7 +36,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.default_relays_longer @@ -44,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.dm_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.dm_relays_not_found_examples2 import com.vitorpamplona.amethyst.commons.resources.dm_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -61,7 +61,7 @@ fun AddDMRelayListDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: DMRelayListViewModel = viewModel() + val postViewModel: DMRelayListViewModel = rememberViewModel { DMRelayListViewModel() } postViewModel.init(accountViewModel) From 2a7145621df4e939e05f3ef16f3b22e04bb4fab6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 02:25:16 +0000 Subject: [PATCH 10/45] refactor: seams for composer link previews, alt-text suggestions and PoW estimates - PreviewUrl reads link previews through the rememberUrlPreviewState slot. - AltTextSuggester port on AppServices; both MLKitImageLabelService flavors implement it, and ImageVideoDescription no longer names the ML Kit class. - The Compose PoW duration formatters and deviceHashesPerSecond move to commonsUI (on a new availableProcessors expect); the Android-resource twins for the mining notification stay in the app. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../service/ai/MLKitImageLabelService.kt | 7 +- .../amethyst/service/pow/PowDuration.kt | 97 -------------- .../amethyst/ui/broadcast/BroadcastBanner.kt | 4 +- .../ui/note/creators/pow/PowOverrideButton.kt | 4 +- .../ui/note/creators/previews/PreviewUrl.kt | 26 +--- .../creators/uploads/ImageVideoDescription.kt | 11 +- .../ui/platform/AndroidAppServices.kt | 4 + .../settings/ComposeSettingsScreen.kt | 4 +- .../service/ai/MLKitImageLabelService.kt | 7 +- .../amethyst/commons/service/AppServices.kt | 4 + .../commons/service/ai/AltTextSuggester.kt | 32 +++++ .../commons/util/AvailableProcessors.kt | 24 ++++ .../commons/util/AvailableProcessors.ios.kt | 25 ++++ .../util/AvailableProcessors.jvmAndroid.kt | 23 ++++ .../amethyst/commons/ui/pow/PowDuration.kt | 123 ++++++++++++++++++ 15 files changed, 257 insertions(+), 138 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/ai/AltTextSuggester.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.ios.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.jvmAndroid.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/pow/PowDuration.kt diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt index f9bd93d747..19935df1a3 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt @@ -22,14 +22,15 @@ package com.vitorpamplona.amethyst.service.ai import android.content.Context import android.net.Uri +import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester @Suppress("UNUSED_PARAMETER") class MLKitImageLabelService( context: Context, -) { +) : AltTextSuggester { suspend fun labelImage(uri: Uri): List> = emptyList() - suspend fun suggestAltText(uri: Uri): String? = null + override suspend fun suggestAltText(uri: Uri): String? = null - fun close() {} + override fun close() {} } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/pow/PowDuration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/pow/PowDuration.kt index bf4189fdba..99135a4755 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/pow/PowDuration.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/pow/PowDuration.kt @@ -21,108 +21,11 @@ package com.vitorpamplona.amethyst.service.pow import android.content.Context -import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.pow_estimate_days -import com.vitorpamplona.amethyst.commons.resources.pow_estimate_hours -import com.vitorpamplona.amethyst.commons.resources.pow_estimate_instant -import com.vitorpamplona.amethyst.commons.resources.pow_estimate_minutes -import com.vitorpamplona.amethyst.commons.resources.pow_estimate_seconds -import com.vitorpamplona.amethyst.commons.resources.pow_time_left -import com.vitorpamplona.amethyst.commons.resources.pow_time_left_soon -import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator -import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy -import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes -import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.stringRes -import org.jetbrains.compose.resources.PluralStringResource import kotlin.math.roundToLong -/** - * This device's effective mining rate: the [PoWEstimator] benchmark run with - * the same worker count the mining queue uses (half the cores, see - * [PoWPolicy.minerWorkers] and AppModules.powPublishQueue). Every UI estimate - * must use this rate, or it would describe a single-threaded miner that no - * longer exists. Cached after the first call (~250 ms). - */ -suspend fun deviceHashesPerSecond(): Double = PoWEstimator.hashesPerSecond(PoWPolicy.minerWorkers(Runtime.getRuntime().availableProcessors())) - -/** - * "45 seconds" / "10 minutes" / "3 hours" — the one human-readable rendering - * of a PoW duration estimate, shared by the settings picker, the composer - * difficulty menu, the mining banner, and the mining notification. - * - * Estimates come from [com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator] - * and are the statistical mean of a memoryless search — any single post can be - * much luckier or unluckier, so always present these as approximations. - */ -@Composable -fun formatApproxDuration(seconds: Double): String { - @Composable - fun quantity( - id: PluralStringResource, - count: Long, - ) = pluralStringRes(id, count.toInt(), count.toInt()) - - return when { - seconds < 1.0 -> stringRes(Res.string.pow_estimate_instant) - seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong()) - seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong()) - seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong()) - else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong()) - } -} - -/** - * "≈ 10 minutes left" while [elapsedSec] is inside the [expectedSec] mean, - * "any moment now" once past it — a memoryless search has no shrinking - * remainder, so past the mean the only honest claim is "soon". - */ -@Composable -fun formatTimeLeft( - expectedSec: Double, - elapsedSec: Long, -): String { - val remaining = expectedSec - elapsedSec - return if (remaining > 1.0) { - stringRes(Res.string.pow_time_left, formatApproxDuration(remaining)) - } else { - stringRes(Res.string.pow_time_left_soon) - } -} - -/** Suspend twin of [formatApproxDuration] for the mining foreground service, - * which builds its notification text outside composition. */ -suspend fun loadApproxDuration(seconds: Double): String { - suspend fun quantity( - id: PluralStringResource, - count: Long, - ) = loadPluralStringRes(id, count.toInt(), count.toInt()) - - return when { - seconds < 1.0 -> loadStringRes(Res.string.pow_estimate_instant) - seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong()) - seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong()) - seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong()) - else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong()) - } -} - -/** Suspend twin of [formatTimeLeft]; see [loadApproxDuration]. */ -suspend fun loadTimeLeft( - expectedSec: Double, - elapsedSec: Long, -): String { - val remaining = expectedSec - elapsedSec - return if (remaining > 1.0) { - loadStringRes(Res.string.pow_time_left, loadApproxDuration(remaining)) - } else { - loadStringRes(Res.string.pow_time_left_soon) - } -} - /** * Android-resource twin of [formatApproxDuration]; see [powKindLabelResId] for why * the mining notification cannot read from the Compose catalog. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt index 37afb26a79..d84ac1db1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/BroadcastBanner.kt @@ -102,12 +102,12 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState import com.vitorpamplona.amethyst.commons.service.pow.powKindLabelRes import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.pow.deviceHashesPerSecond +import com.vitorpamplona.amethyst.commons.ui.pow.formatTimeLeft import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.warningColor -import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond -import com.vitorpamplona.amethyst.service.pow.formatTimeLeft import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt index 984fb6242e..cc2b0bafc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt @@ -53,10 +53,10 @@ import com.vitorpamplona.amethyst.commons.resources.pow_option_off import com.vitorpamplona.amethyst.commons.resources.pow_settings_title import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.pow.deviceHashesPerSecond +import com.vitorpamplona.amethyst.commons.ui.pow.formatApproxDuration import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow -import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond -import com.vitorpamplona.amethyst.service.pow.formatApproxDuration val POW_PRESETS = listOf(16, 20, 24, 28) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index 347e1ee356..f96a54edbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,10 +50,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.WaitAndDisplay import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.platform.VideoView +import com.vitorpamplona.amethyst.commons.ui.note.platform.rememberUrlPreviewState import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview @Composable fun PreviewUrl( @@ -202,16 +200,7 @@ private fun MyLoadUrlPreviewDirect( accountViewModel: AccountViewModel, nav: INav, ) { - @Suppress("ProduceStateDoesNotAssignValue") - val urlPreviewState by - produceState( - initialValue = UrlCachedPreviewer.cache.get(url) ?: UrlPreviewState.Loading, - key1 = url, - ) { - if (value == UrlPreviewState.Loading) { - accountViewModel.urlPreview(url) { value = it } - } - } + val urlPreviewState = rememberUrlPreviewState(url, accountViewModel) CrossfadeIfEnabled( targetState = urlPreviewState, @@ -269,16 +258,7 @@ private fun MyLoadUrlPreviewDirectFillWidth( accountViewModel: AccountViewModel, nav: INav, ) { - @Suppress("ProduceStateDoesNotAssignValue") - val urlPreviewState by - produceState( - initialValue = UrlCachedPreviewer.cache.get(url) ?: UrlPreviewState.Loading, - key1 = url, - ) { - if (value == UrlPreviewState.Loading) { - accountViewModel.urlPreview(url) { value = it } - } - } + val urlPreviewState = rememberUrlPreviewState(url, accountViewModel) CrossfadeIfEnabled( targetState = urlPreviewState, 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 10d312c1a5..ffb756e4c1 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 @@ -55,7 +55,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow @@ -97,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.note.CancelIcon import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder @@ -104,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService import kotlinx.collections.immutable.toImmutableList @Composable @@ -133,7 +132,6 @@ fun ImageVideoDescription( var message by remember { mutableStateOf("") } var sensitiveContent by remember { mutableStateOf(false) } - val context = LocalContext.current val firstImageUri = remember(uris) { uris @@ -142,12 +140,13 @@ fun ImageVideoDescription( ?.media ?.uri } - val labelService = remember { MLKitImageLabelService(context.applicationContext) } + val appServices = LocalAppServices.current + val labelService = remember { appServices.createAltTextSuggester() } var isLabeling by remember { mutableStateOf(false) } var aiSuggested by remember { mutableStateOf(false) } DisposableEffect(labelService) { - onDispose { labelService.close() } + onDispose { labelService?.close() } } LaunchedEffect(firstImageUri) { @@ -155,7 +154,7 @@ fun ImageVideoDescription( isLabeling = true val suggestion = try { - labelService.suggestAltText(firstImageUri) + labelService?.suggestAltText(firstImageUri) } finally { isLabeling = false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index 41062f32af..ae647dadc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -29,7 +29,9 @@ import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.AppServices import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder +import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow +import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver /** @@ -55,6 +57,8 @@ object AndroidAppServices : AppServices { override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager + + override fun createAltTextSuggester(): AltTextSuggester = MLKitImageLabelService(Amethyst.instance.appContext) } /** [BlossomServerFinder] over the app's BUD-10 resolver, read lazily (main process only). */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index d6067bc225..3082746f8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -96,6 +96,8 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.pow.deviceHashesPerSecond +import com.vitorpamplona.amethyst.commons.ui.pow.loadApproxDuration import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider @@ -107,8 +109,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory -import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond -import com.vitorpamplona.amethyst.service.pow.loadApproxDuration import com.vitorpamplona.amethyst.ui.note.creators.pow.POW_PRESETS import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt index 16f827f205..56f3a337c4 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/ai/MLKitImageLabelService.kt @@ -29,6 +29,7 @@ import com.google.mlkit.genai.imagedescription.ImageDescriber import com.google.mlkit.genai.imagedescription.ImageDescriberOptions import com.google.mlkit.genai.imagedescription.ImageDescription import com.google.mlkit.genai.imagedescription.ImageDescriptionRequest +import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -41,7 +42,7 @@ import kotlinx.coroutines.withContext */ class MLKitImageLabelService( private val context: Context, -) { +) : AltTextSuggester { private var describer: ImageDescriber? = null // FeatureStatus is an Int enum. Cached per-instance — describer availability does not flip @@ -58,7 +59,7 @@ class MLKitImageLabelService( null } - suspend fun suggestAltText(uri: Uri): String? = describeWithGenAi(uri) + override suspend fun suggestAltText(uri: Uri): String? = describeWithGenAi(uri) private suspend fun describeWithGenAi(uri: Uri): String? = withContext(Dispatchers.IO) { @@ -116,7 +117,7 @@ class MLKitImageLabelService( return sample } - fun close() { + override fun close() { describer?.close() describer = null cachedGenAiStatus = null diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index ffa7fcea27..2f113e23a5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermi import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver @@ -60,4 +61,7 @@ interface AppServices { /** The device's position as geohashes. */ val deviceLocation: DeviceLocation get() = DeviceLocation.None + + /** A new on-device alt-text suggester for image uploads, or null where the platform has none. */ + fun createAltTextSuggester(): AltTextSuggester? = null } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/ai/AltTextSuggester.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/ai/AltTextSuggester.kt new file mode 100644 index 0000000000..3e97c254b6 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/ai/AltTextSuggester.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.service.ai + +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri + +/** Proposes alt text for an image the user is about to upload, using an on-device model. */ +interface AltTextSuggester { + /** A one-line description of the image at [uri], or null when none could be produced. */ + suspend fun suggestAltText(uri: MediaUri): String? + + /** Releases the model. */ + fun close() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.kt new file mode 100644 index 0000000000..20c1a5e166 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.util + +/** How many processor cores this process may use right now. */ +expect fun availableProcessors(): Int diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.ios.kt new file mode 100644 index 0000000000..ae39103329 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.ios.kt @@ -0,0 +1,25 @@ +/* + * 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.commons.util + +import platform.Foundation.NSProcessInfo + +actual fun availableProcessors(): Int = NSProcessInfo.processInfo.activeProcessorCount.toInt() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.jvmAndroid.kt new file mode 100644 index 0000000000..caa154c409 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/AvailableProcessors.jvmAndroid.kt @@ -0,0 +1,23 @@ +/* + * 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.commons.util + +actual fun availableProcessors(): Int = Runtime.getRuntime().availableProcessors() diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/pow/PowDuration.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/pow/PowDuration.kt new file mode 100644 index 0000000000..322d5188c0 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/pow/PowDuration.kt @@ -0,0 +1,123 @@ +/* + * 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.commons.ui.pow + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.pow_estimate_days +import com.vitorpamplona.amethyst.commons.resources.pow_estimate_hours +import com.vitorpamplona.amethyst.commons.resources.pow_estimate_instant +import com.vitorpamplona.amethyst.commons.resources.pow_estimate_minutes +import com.vitorpamplona.amethyst.commons.resources.pow_estimate_seconds +import com.vitorpamplona.amethyst.commons.resources.pow_time_left +import com.vitorpamplona.amethyst.commons.resources.pow_time_left_soon +import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator +import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy +import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.availableProcessors +import org.jetbrains.compose.resources.PluralStringResource +import kotlin.math.roundToLong + +/** + * This device's effective mining rate: the [PoWEstimator] benchmark run with + * the same worker count the mining queue uses (half the cores, see + * [PoWPolicy.minerWorkers] and the app's PoW publish queue). Every UI estimate + * must use this rate, or it would describe a single-threaded miner that no + * longer exists. Cached after the first call (~250 ms). + */ +suspend fun deviceHashesPerSecond(): Double = PoWEstimator.hashesPerSecond(PoWPolicy.minerWorkers(availableProcessors())) + +/** + * "45 seconds" / "10 minutes" / "3 hours" — the one human-readable rendering + * of a PoW duration estimate, shared by the settings picker, the composer + * difficulty menu, the mining banner, and the mining notification. + * + * Estimates come from [com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator] + * and are the statistical mean of a memoryless search — any single post can be + * much luckier or unluckier, so always present these as approximations. + */ +@Composable +fun formatApproxDuration(seconds: Double): String { + @Composable + fun quantity( + id: PluralStringResource, + count: Long, + ) = pluralStringRes(id, count.toInt(), count.toInt()) + + return when { + seconds < 1.0 -> stringRes(Res.string.pow_estimate_instant) + seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong()) + seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong()) + seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong()) + else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong()) + } +} + +/** + * "≈ 10 minutes left" while [elapsedSec] is inside the [expectedSec] mean, + * "any moment now" once past it — a memoryless search has no shrinking + * remainder, so past the mean the only honest claim is "soon". + */ +@Composable +fun formatTimeLeft( + expectedSec: Double, + elapsedSec: Long, +): String { + val remaining = expectedSec - elapsedSec + return if (remaining > 1.0) { + stringRes(Res.string.pow_time_left, formatApproxDuration(remaining)) + } else { + stringRes(Res.string.pow_time_left_soon) + } +} + +/** Suspend twin of [formatApproxDuration] for the mining foreground service, + * which builds its notification text outside composition. */ +suspend fun loadApproxDuration(seconds: Double): String { + suspend fun quantity( + id: PluralStringResource, + count: Long, + ) = loadPluralStringRes(id, count.toInt(), count.toInt()) + + return when { + seconds < 1.0 -> loadStringRes(Res.string.pow_estimate_instant) + seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong()) + seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong()) + seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong()) + else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong()) + } +} + +/** Suspend twin of [formatTimeLeft]; see [loadApproxDuration]. */ +suspend fun loadTimeLeft( + expectedSec: Double, + elapsedSec: Long, +): String { + val remaining = expectedSec - elapsedSec + return if (remaining > 1.0) { + loadStringRes(Res.string.pow_time_left, loadApproxDuration(remaining)) + } else { + loadStringRes(Res.string.pow_time_left_soon) + } +} From 2270d19f9a29b75cba762191ceb8b847329f6cb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 02:30:55 +0000 Subject: [PATCH 11/45] refactor: drop Android toasts, intents and hand-rolled uploads from more screens - Marmot group info/chat, Minichat and Concord screens show toasts through rememberShortNotice/rememberLongNotice. - The hashtag, geohash, URL, product and group-DM composers resolve shared attachments with SharedMediaResolver; NewGroupDMScreen takes the attachment as a string and MediaUri for pasted content. - Bookmark-group, channel, people-list and follow-pack metadata view models upload through uploadToDefaultServer instead of their own strip/compress/NIP-96/Blossom code and Context. - RelayGroupTopBar shares through rememberTextSharer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../metadata/BookmarkGroupMetadataScreen.kt | 2 +- .../BookmarkGroupMetadataViewModel.kt | 96 +--------------- .../chats/marmotGroup/MarmotGroupChatView.kt | 22 ++-- .../marmotGroup/MarmotGroupInfoScreen.kt | 105 +++++------------- .../loggedIn/chats/minichat/MinichatScreen.kt | 11 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 29 +---- .../concord/ConcordChannelScreen.kt | 14 +-- .../metadata/ChannelMetadataScreen.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 97 +--------------- .../relayGroup/RelayGroupTopBar.kt | 25 +---- .../nip99Classifieds/NewProductScreen.kt | 12 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 15 +-- .../loggedIn/hashtag/HashtagPostScreen.kt | 15 +-- .../list/metadata/FollowPackMetadataScreen.kt | 2 +- .../metadata/FollowPackMetadataViewModel.kt | 97 +--------------- .../list/metadata/PeopleListMetadataScreen.kt | 2 +- .../metadata/PeopleListMetadataViewModel.kt | 96 +--------------- .../ui/screen/loggedIn/url/UrlPostScreen.kt | 15 +-- 18 files changed, 96 insertions(+), 561 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt index c5af527e5e..7efff6c07f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt @@ -230,7 +230,7 @@ private fun Picture( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 2.dp), ) { - bookmarkGroupInfoViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + bookmarkGroupInfoViewModel.uploadForPicture(it, onError = accountViewModel.toastManager::toast) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index ca9386f767..6f6d426359 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -30,28 +29,12 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class BookmarkGroupMetadataViewModel : ViewModel() { @@ -122,13 +105,11 @@ class BookmarkGroupMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, onError: (String, String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { upload( uri, - context, onUploading = { isUploadingImageForPicture = it }, onUploaded = { picture.value = TextFieldValue(it) }, onError = onError, @@ -138,82 +119,13 @@ class BookmarkGroupMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { onUploading(true) - - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onUploading(false) - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } + val url = uploadToDefaultServer(galleryUri, account, accountViewModel.host.mediaUploader, onError) + onUploading(false) + if (url != null) onUploaded(url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 11d5f39f13..17e99b5d38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -71,6 +70,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -232,6 +233,7 @@ fun MarmotGroupMessageComposer( val scope = rememberCoroutineScope() val canPost by remember { derivedStateOf { newMessageModel.canPost() } } val context = LocalContext.current + val shortNotice = rememberShortNotice() var isUploading by remember { mutableStateOf(false) } @@ -306,12 +308,7 @@ fun MarmotGroupMessageComposer( onMessageSent() } catch (e: Exception) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - "Failed to send message: ${e.message}", - Toast.LENGTH_SHORT, - ).show() + shortNotice.show("Failed to send message: ${e.message}") } } } @@ -354,6 +351,8 @@ private fun MarmotGroupFileUploadDialog( onCancel: () -> Unit, ) { val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val scope = rememberCoroutineScope() ChatFileUploadDialog( @@ -370,12 +369,7 @@ private fun MarmotGroupFileUploadDialog( val exporterSecret = accountViewModel.marmotMediaExporterSecret(nostrGroupId) if (exporterSecret == null) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_not_a_member), - Toast.LENGTH_SHORT, - ).show() + shortNotice.show(loadStringRes(Res.string.marmot_not_a_member)) } return@launch } @@ -385,7 +379,7 @@ private fun MarmotGroupFileUploadDialog( exporterSecret = exporterSecret, onError = { title, message -> scope.launch(Dispatchers.Main) { - Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() + longNotice.show("$title: $message") } }, context = context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 99eb376ac8..5a0d64a698 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -62,7 +61,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -127,6 +125,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_unknown_error import com.vitorpamplona.amethyst.commons.resources.marmot_user_fallback_name import com.vitorpamplona.amethyst.commons.resources.members import com.vitorpamplona.amethyst.commons.resources.remove +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes @@ -188,7 +188,8 @@ fun MarmotGroupInfoScreen( var isAdding by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val myPubkey = accountViewModel.account.signer.pubKey - val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val userSuggestions = remember { @@ -348,12 +349,7 @@ fun MarmotGroupInfoScreen( Button( onClick = { if (!accountViewModel.hasBlossomServers()) { - Toast - .makeText( - context, - marmotEnableEncryptedMediaNeedsServerStr, - Toast.LENGTH_LONG, - ).show() + longNotice.show(marmotEnableEncryptedMediaNeedsServerStr) return@Button } isEnablingMedia = true @@ -361,24 +357,16 @@ fun MarmotGroupInfoScreen( try { accountViewModel.enableMarmotEncryptedMediaV2(nostrGroupId) launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_encrypted_media_enabled_toast), - Toast.LENGTH_SHORT, - ).show() + shortNotice.show(loadStringRes(Res.string.marmot_encrypted_media_enabled_toast)) } } catch (e: Exception) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes( - Res.string.marmot_failed_to_enable_encrypted_media, - e.message, - ), - Toast.LENGTH_LONG, - ).show() + longNotice.show( + loadStringRes( + Res.string.marmot_failed_to_enable_encrypted_media, + e.message, + ), + ) } } finally { isEnablingMedia = false @@ -514,12 +502,7 @@ fun MarmotGroupInfoScreen( } catch (e: Exception) { isLeaving = false launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_leave_group, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_leave_group, e.message)) } } } @@ -544,18 +527,15 @@ fun MarmotGroupInfoScreen( // outbound work, so leaving the screen is right. val ended = accountViewModel.disbandMarmotGroup(nostrGroupId) launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes( - if (ended) { - Res.string.marmot_group_disbanded_toast - } else { - Res.string.marmot_group_disbanding_toast - }, - ), - Toast.LENGTH_SHORT, - ).show() + shortNotice.show( + loadStringRes( + if (ended) { + Res.string.marmot_group_disbanded_toast + } else { + Res.string.marmot_group_disbanding_toast + }, + ), + ) } nav.nav(Route.Message) } catch (e: Exception) { @@ -564,12 +544,7 @@ fun MarmotGroupInfoScreen( // navigate away on a change that did not happen. isDisbanding = false launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_disband, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_disband, e.message)) } } } @@ -588,18 +563,11 @@ fun MarmotGroupInfoScreen( try { accountViewModel.removeMarmotGroupMember(nostrGroupId, member.leafIndex) launch(Dispatchers.Main) { - Toast - .makeText(context, loadStringRes(Res.string.marmot_member_removed), Toast.LENGTH_SHORT) - .show() + shortNotice.show(loadStringRes(Res.string.marmot_member_removed)) } } catch (e: Exception) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_remove_member, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_remove_member, e.message)) } } } @@ -618,18 +586,11 @@ fun MarmotGroupInfoScreen( try { accountViewModel.grantMarmotGroupAdmin(nostrGroupId, member.pubkey) launch(Dispatchers.Main) { - Toast - .makeText(context, loadStringRes(Res.string.marmot_admin_granted), Toast.LENGTH_SHORT) - .show() + shortNotice.show(loadStringRes(Res.string.marmot_admin_granted)) } } catch (e: Exception) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_grant_admin, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_grant_admin, e.message)) } } } @@ -648,18 +609,11 @@ fun MarmotGroupInfoScreen( try { accountViewModel.revokeMarmotGroupAdmin(nostrGroupId, member.pubkey) launch(Dispatchers.Main) { - Toast - .makeText(context, loadStringRes(Res.string.marmot_admin_revoked), Toast.LENGTH_SHORT) - .show() + shortNotice.show(loadStringRes(Res.string.marmot_admin_revoked)) } } catch (e: Exception) { launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_revoke_admin, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_revoke_admin, e.message)) } } } @@ -1115,7 +1069,6 @@ private fun RelayHealthRow( isActive: Boolean, nav: INav, ) { - val context = LocalContext.current val dotColor = if (isActive) { MaterialTheme.colorScheme.allGoodColor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index b2724acb54..c901b46f26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.minichat -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight @@ -70,6 +69,8 @@ import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose @@ -152,6 +153,8 @@ fun MinichatScreen( val composer = remember { TextFieldState() } val scope = rememberCoroutineScope() val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val canPost by remember { derivedStateOf { composer.text.isNotBlank() } } val uploadState = remember { @@ -235,10 +238,10 @@ fun MinichatScreen( ChatFileUploader(accountViewModel.account).justUploadNIP17( viewState = uploadState, onError = { title, message -> - launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } + launch(Dispatchers.Main) { longNotice.show("$title: $message") } }, onEncryptedUploadError = { title, message -> - launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } + launch(Dispatchers.Main) { longNotice.show("$title: $message") } }, uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> @@ -301,7 +304,7 @@ fun MinichatScreen( accountViewModel.account.sendMinichatReply(rootNote, text) } catch (e: Exception) { launch(Dispatchers.Main) { - Toast.makeText(context, "Failed to send message: ${e.message}", Toast.LENGTH_SHORT).show() + shortNotice.show("Failed to send message: ${e.message}") } } } 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 cf60a83d21..edeea0251d 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import android.net.Uri import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box @@ -58,11 +57,9 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -87,12 +84,14 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -138,11 +137,9 @@ import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -151,33 +148,19 @@ fun NewGroupDMScreen( attachment: String? = null, accountViewModel: AccountViewModel, nav: INav, -) { - NewGroupDMScreen(message, attachment?.ifBlank { null }?.toUri(), accountViewModel, nav) -} - -@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) -@Composable -fun NewGroupDMScreen( - message: String? = null, - attachment: Uri? = null, - accountViewModel: AccountViewModel, - nav: INav, ) { val postViewModel: ChatNewMessageViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { message?.ifBlank { null }?.let { postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.pickedMedia(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.pickedMedia(persistentListOf(it)) } } @@ -364,7 +347,7 @@ fun MessageFieldRow( postViewModel: IMessageField, accountViewModel: AccountViewModel, requestFocus: Boolean = false, - onContentReceived: ((Uri, String?) -> Unit)? = null, + onContentReceived: ((MediaUri, String?) -> Unit)? = null, ) { Row { BaseUserPicture( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 67681cc99f..9d068fcbcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -80,6 +78,8 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -480,7 +480,7 @@ private fun ConcordMessageComposer( ) { val scope = rememberCoroutineScope() val canPost by remember { derivedStateOf { newMessageModel.canPost() } } - val context = LocalContext.current + val shortNotice = rememberShortNotice() // Throttle typing heartbeats to at most one every few seconds while the field is non-empty. val lastTypingSecs = remember(newMessageModel.channelId) { longArrayOf(0L) } @@ -589,7 +589,7 @@ private fun ConcordMessageComposer( onMessageSent() } catch (e: Exception) { launch(Dispatchers.Main) { - Toast.makeText(context, "Failed to send message: ${e.message}", Toast.LENGTH_SHORT).show() + shortNotice.show("Failed to send message: ${e.message}") } } } @@ -619,7 +619,7 @@ private fun ConcordFileUploadDialog( onUpload: suspend () -> Unit, onCancel: () -> Unit, ) { - val context = LocalContext.current + val longNotice = rememberLongNotice() val scope = rememberCoroutineScope() ChatFileUploadDialog( @@ -634,10 +634,10 @@ private fun ConcordFileUploadDialog( ChatFileUploader(accountViewModel.account).justUploadNIP17( viewState = state, onError = { title, message -> - scope.launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } + scope.launch(Dispatchers.Main) { longNotice.show("$title: $message") } }, onEncryptedUploadError = { title, message -> - scope.launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } + scope.launch(Dispatchers.Main) { longNotice.show("$title: $message") } }, uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 1f1ff69fe0..8ca46eb1ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -304,7 +304,7 @@ private fun Picture( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 2.dp), ) { - postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForPicture(it, onError = accountViewModel.toastManager::toast) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index adac712a2b..788e7562e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -31,28 +30,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -61,7 +45,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class ChannelMetadataViewModel : ViewModel() { @@ -190,13 +173,11 @@ class ChannelMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, onError: (String, String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { upload( uri, - context, onUploading = { isUploadingImageForPicture = it }, onUploaded = { channelPicture.value = TextFieldValue(it) }, onError = onError, @@ -206,83 +187,13 @@ class ChannelMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { - val account = account onUploading(true) - - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onUploading(false) - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } + val url = uploadToDefaultServer(galleryUri, account, accountViewModel.host.mediaUploader, onError) + onUploading(false) + if (url != null) onUploaded(url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index a70e5755f4..4bcd61864e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup -import android.content.Context -import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -46,7 +44,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -86,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_role_moderator import com.vitorpamplona.amethyst.commons.resources.relay_group_threads_title import com.vitorpamplona.amethyst.commons.resources.remove_from_messages import com.vitorpamplona.amethyst.commons.search.SearchSeed +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton @@ -334,14 +332,14 @@ fun RelayGroupTopBar( ) } if (naddr != null) { - val context = LocalContext.current + val textSharer = rememberTextSharer() val shareLinkTitle = stringRes(Res.string.quick_action_share_browser_link) val shareChooserTitle = stringRes(Res.string.quick_action_share) DropdownMenuItem( text = { Text(stringRes(Res.string.quick_action_share)) }, onClick = { menuOpen = false - shareRelayGroup(context, naddr, shareLinkTitle, shareChooserTitle) + textSharer.share(njumpLink(naddr), shareLinkTitle, shareChooserTitle) }, ) } @@ -504,23 +502,6 @@ private fun DmParticipantTitle( ) } -/** Fire the system share sheet with a njump web link to the group's naddr. */ -private fun shareRelayGroup( - context: Context, - naddr: String, - linkTitle: String, - chooserTitle: String, -) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, njumpLink(naddr)) - putExtra(Intent.EXTRA_TITLE, linkTitle) - } - context.startActivity(Intent.createChooser(sendIntent, chooserTitle)) -} - /** A small colored pill naming the user's role/status in the group. */ @Composable private fun RoleBadge(membership: RelayGroupMembership) { 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 02ba7531ad..34dbee5b4d 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 @@ -41,7 +41,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList @@ -57,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -90,9 +90,7 @@ import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -108,6 +106,7 @@ fun NewProductScreen( postViewModel.init(accountViewModel) val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { postViewModel.reloadRelaySet() @@ -121,11 +120,8 @@ fun NewProductScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 1f552025d4..78967cb3c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -23,11 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -35,8 +33,6 @@ import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Composable fun GeoHashPostScreen( @@ -52,7 +48,7 @@ fun GeoHashPostScreen( val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { geohash?.let { @@ -71,11 +67,8 @@ fun GeoHashPostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null) { postViewModel.applySignature() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index 680e6808d6..34c835bd0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -23,11 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -35,8 +33,6 @@ import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Composable fun HashtagPostScreen( @@ -52,7 +48,7 @@ fun HashtagPostScreen( val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { hashtag?.let { @@ -71,11 +67,8 @@ fun HashtagPostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null) { postViewModel.applySignature() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt index db7782814f..3a6763793c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt @@ -254,7 +254,7 @@ private fun Picture( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 2.dp), ) { - postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForPicture(it, onError = accountViewModel.toastManager::toast) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt index 5b66e7921c..631710474a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -30,28 +29,12 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class FollowPackMetadataViewModel : ViewModel() { @@ -121,13 +104,11 @@ class FollowPackMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, onError: (String, String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { upload( uri, - context, onUploading = { isUploadingImageForPicture = it }, onUploaded = { picture.value = TextFieldValue(it) }, onError = onError, @@ -137,83 +118,13 @@ class FollowPackMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { onUploading(true) - - val sourceUri = - if (account.settings.stripLocationOnUpload) { - val result = - try { - MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - } catch (e: AvifMetadataNotVerifiableException) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - onUploading(false) - return - } - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } + val url = uploadToDefaultServer(galleryUri, account, accountViewModel.host.mediaUploader, onError) + onUploading(false) + if (url != null) onUploaded(url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt index c5c9fea019..3bedc2412c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt @@ -254,7 +254,7 @@ private fun Picture( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 2.dp), ) { - postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForPicture(it, onError = accountViewModel.toastManager::toast) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt index d3d833908c..f7146d3853 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -30,28 +29,12 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class PeopleListMetadataViewModel : ViewModel() { @@ -122,13 +105,11 @@ class PeopleListMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, onError: (String, String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { upload( uri, - context, onUploading = { isUploadingImageForPicture = it }, onUploaded = { picture.value = TextFieldValue(it) }, onError = onError, @@ -138,82 +119,13 @@ class PeopleListMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { onUploading(true) - - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onUploading(false) - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } + val url = uploadToDefaultServer(galleryUri, account, accountViewModel.host.mediaUploader, onError) + onUploading(false) + if (url != null) onUploaded(url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt index 03bd66c9cc..9fbc1da2c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -23,11 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.url import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -35,8 +33,6 @@ import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Composable fun UrlPostScreen( @@ -52,7 +48,7 @@ fun UrlPostScreen( val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { url?.let { @@ -73,11 +69,8 @@ fun UrlPostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null) { postViewModel.applySignature() From dced71bc0c570165d278c0a68729b69eda44101e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 02:57:33 +0000 Subject: [PATCH 12/45] refactor: move the note composers, metadata editors and group chat screens 50 files freed by the previous seams move to commonsUI: the hashtag, geohash, URL, comment, product, public-message and group-DM composers with their view models, the bookmark-group/channel/people-list/follow-pack metadata editors, MarmotGroupInfoScreen, MinichatScreen, ConcordChannelScreen and RelayGroupChatScreen. AudienceSheet sizes against LocalWindowInfo; CommentPostViewModel counts UTF-8 bytes with encodeToByteArray, and its test moves to commonsUI. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/actions/EditPostView.kt | 4 +-- .../amethyst/ui/navigation/AppNavigation.kt | 30 +++++++++---------- .../ui/screen/loggedIn/LoggedInPage.kt | 6 ++-- .../screen/loggedIn/buzz/BuzzDmDiscovery.kt | 2 +- .../chats/marmotGroup/MarmotGroupChatView.kt | 2 +- .../nip23LongForm/LongFormPostScreen.kt | 10 +++---- .../loggedIn/home/ShortNotePostScreen.kt | 20 ++++++------- .../loggedIn/nests/create/CreateNestSheet.kt | 2 +- .../settings/ComposeSettingsScreen.kt | 2 +- .../expiration/ExpirationDatePicker.kt | 2 +- .../note/creators/invoice/InvoiceRequest.kt | 2 +- .../creators/invoice/NewPostInvoiceRequest.kt | 2 +- .../location/DisplayLocationObserver.kt | 2 +- .../creators/location/GeoHashPostSection.kt | 2 +- .../note/creators/location/LocationAsHash.kt | 2 +- .../ui/note/creators/notify/AudienceFlap.kt | 2 +- .../ui/note/creators/notify/AudienceSheet.kt | 19 +++++++++--- .../ui/note/creators/pow/PowOverrideButton.kt | 4 +-- .../note/creators/previews/DisplayPreviews.kt | 2 +- .../ui/note/creators/previews/PreviewUrl.kt | 2 +- .../creators/uploads/ImageVideoDescription.kt | 2 +- .../note/creators/zapsplits/ForwardZapTo.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 5 ++-- .../nip22Comments/GenericCommentPostScreen.kt | 26 ++++++++-------- .../metadata/BookmarkGroupMetadataScreen.kt | 8 ++--- .../BookmarkGroupMetadataViewModel.kt | 3 +- .../marmotGroup/MarmotGroupInfoScreen.kt | 8 +++-- .../chats/minichat/MinichatFeedViewModel.kt | 9 ++++-- .../loggedIn/chats/minichat/MinichatScreen.kt | 15 +++++----- .../chats/privateDM/send/NewGroupDMScreen.kt | 18 +++++------ .../concord/ConcordChannelScreen.kt | 17 ++++++----- .../ConcordChannelHistorySubscription.kt | 2 +- .../send/ConcordNewMessageViewModel.kt | 2 +- .../metadata/ChannelMetadataScreen.kt | 10 +++---- .../metadata/ChannelMetadataViewModel.kt | 3 +- .../relayGroup/BuzzTypingIndicator.kt | 2 +- .../relayGroup/InviteRelayGroupDialog.kt | 2 +- .../relayGroup/JoinRelayGroupDialog.kt | 2 +- .../relayGroup/RelayGroupChannelView.kt | 12 ++++---- .../relayGroup/RelayGroupChatScreen.kt | 2 +- .../relayGroup/RelayGroupMigrationBar.kt | 2 +- .../relayGroup/RelayGroupPinnedBar.kt | 2 +- .../relayGroup/RelayGroupSubgroupsBar.kt | 2 +- .../relayGroup/RelayGroupTopBar.kt | 2 +- .../datasource/RelayGroupChatSubscriptions.kt | 2 +- .../chats/utils/EditingMessageBanner.kt | 2 +- .../loggedIn/chats/utils/UploadImetas.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 23 ++++++-------- .../nip99Classifieds/NewProductViewModel.kt | 3 +- .../discover/nip99Classifieds/SellProduct.kt | 2 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 10 +++---- .../loggedIn/hashtag/HashtagPostScreen.kt | 10 +++---- .../list/metadata/FollowPackMetadataScreen.kt | 10 +++---- .../metadata/FollowPackMetadataViewModel.kt | 3 +- .../list/metadata/PeopleListMetadataScreen.kt | 10 +++---- .../metadata/PeopleListMetadataViewModel.kt | 3 +- .../publicMessages/NewPublicMessageScreen.kt | 21 ++++++------- .../NewPublicMessageViewModel.kt | 3 +- .../ui/screen/loggedIn/url/UrlPostScreen.kt | 10 +++---- .../nip22Comments/CommentPostViewModelTest.kt | 2 +- 60 files changed, 202 insertions(+), 191 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/expiration/ExpirationDatePicker.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/invoice/InvoiceRequest.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/invoice/NewPostInvoiceRequest.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/location/DisplayLocationObserver.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/location/GeoHashPostSection.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/location/LocationAsHash.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/notify/AudienceFlap.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/notify/AudienceSheet.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/pow/PowOverrideButton.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/previews/DisplayPreviews.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/previews/PreviewUrl.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/uploads/ImageVideoDescription.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/zapsplits/ForwardZapTo.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/nip22Comments/CommentPostViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/nip22Comments/GenericCommentPostScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/utils/UploadImetas.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/url/UrlPostScreen.kt (88%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/nip22Comments/CommentPostViewModelTest.kt (98%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 77bad9e23b..56faf351c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -89,6 +89,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.InvoiceRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadUrlPreview import com.vitorpamplona.amethyst.commons.ui.note.platform.VideoView @@ -105,8 +107,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index a2bed180ef..51bbe94e9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen @@ -74,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.profile.Prof import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen @@ -99,8 +101,12 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.G import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.minichat.MinichatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordEditScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordHomeScreen @@ -110,7 +116,9 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupDiscoveryScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupEditScreen @@ -122,6 +130,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.Co import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.contactList.ContactListUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.drafts.DraftListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.favorites.FavoriteAlgoFeedsListScreen @@ -136,8 +145,10 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.Fo import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen @@ -148,6 +159,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.m import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.lists.PeopleListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.packs.FollowPackScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.LiveStreamsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.LongsScreen @@ -157,6 +170,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.NewMusicPlayl import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.ConnectedAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.NsitesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen @@ -204,6 +218,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.ShortsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.ThreadScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.UrlPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url.UrlScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddClinkDebitWalletScreen @@ -247,13 +262,11 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen -import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen @@ -267,22 +280,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroup import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.minichat.MinichatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader @@ -293,19 +299,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPack import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppDetailScreen @@ -313,7 +315,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen @@ -336,7 +337,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 8150638e42..d1f1967776 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -48,6 +48,9 @@ import com.vitorpamplona.amethyst.commons.resources.error_opening_external_signe import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreload +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.BuzzDmJoinedChatTailPreload +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedChatTailPreload +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedStatePreload import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription @@ -57,9 +60,6 @@ import com.vitorpamplona.amethyst.service.resourceusage.innermostSigner import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmDiscoveryPreload -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.BuzzDmJoinedChatTailPreload -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedChatTailPreload -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedStatePreload import com.vitorpamplona.quartz.nip55AndroidSigner.client.IActivityLauncher import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt index f0835ff2e2..01da8dee98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmDiscovery.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.flow.map * owns the one `#p=me` REQ per workspace relay — so this reads them back out of [LocalCache], fetches * each discovered channel's 39000-39003 directory (so its `t`=dm marker + participants land), and * records the ones that turn out to be DMs into [BuzzDmChannels]. The companion - * [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.BuzzDmJoinedChatTailPreload] + * [com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.BuzzDmJoinedChatTailPreload] * then keeps those channels' messages warm app-wide — which is what lets a Buzz DM show on the * Notifications tab and in push without the viewer opening the conversation first. * diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 17e99b5d38..4fee3c10b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.Refreshi import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.EditingMessageBanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -93,7 +94,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.EditingMessageBanner import com.vitorpamplona.quartz.marmot.protocolCore.LocalOutboundGate import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 642b90c5c5..f28daa91bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -117,13 +117,18 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -135,11 +140,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf 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 f341fdcddc..2e787f757f 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 @@ -120,15 +120,25 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists +import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel @@ -153,20 +163,10 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap -import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet -import com.vitorpamplona.amethyst.ui.note.creators.notify.rememberAudienceLists import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField -import com.vitorpamplona.amethyst.ui.note.creators.pow.PowOverrideButton -import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt index 2ea4775057..1ef0ed505c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt @@ -250,7 +250,7 @@ fun CreateNestSheet( /** * Schedule-start picker. Shows the currently-selected start time * as a button; tapping it opens a Material3 DatePicker → TimePicker - * chain (same shape as [com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker] + * chain (same shape as [com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker] * — hour + minute resolved in the local zone, then converted to * UTC unix seconds via the system zone offset). */ diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 3082746f8c..34b9228931 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.POW_PRESETS import com.vitorpamplona.amethyst.commons.ui.pow.deviceHashesPerSecond import com.vitorpamplona.amethyst.commons.ui.pow.loadApproxDuration import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile @@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory -import com.vitorpamplona.amethyst.ui.note.creators.pow.POW_PRESETS import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/expiration/ExpirationDatePicker.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/expiration/ExpirationDatePicker.kt index 714bb5defb..19784c96ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/expiration/ExpirationDatePicker.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.note.creators.expiration +package com.vitorpamplona.amethyst.commons.ui.note.creators.expiration import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/InvoiceRequest.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/InvoiceRequest.kt index 7b8673f993..35418e08bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/InvoiceRequest.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.note.creators.invoice +package com.vitorpamplona.amethyst.commons.ui.note.creators.invoice import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/NewPostInvoiceRequest.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/NewPostInvoiceRequest.kt index fabde4f787..3b73a95426 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/invoice/NewPostInvoiceRequest.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.note.creators.invoice +package com.vitorpamplona.amethyst.commons.ui.note.creators.invoice import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/DisplayLocationObserver.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/DisplayLocationObserver.kt index 94a7c6320f..fae9760e1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/DisplayLocationObserver.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.note.creators.location +package com.vitorpamplona.amethyst.commons.ui.note.creators.location import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/GeoHashPostSection.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/GeoHashPostSection.kt index 91da97cb38..7e36b79445 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeoHashPostSection.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/GeoHashPostSection.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.note.creators.location +package com.vitorpamplona.amethyst.commons.ui.note.creators.location import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/LocationAsHash.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/LocationAsHash.kt index b29d243949..ed2077e964 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LocationAsHash.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/location/LocationAsHash.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.note.creators.location +package com.vitorpamplona.amethyst.commons.ui.note.creators.location import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceFlap.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceFlap.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceFlap.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceFlap.kt index 2e140db2f9..ba8be71638 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceFlap.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceFlap.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.note.creators.notify +package com.vitorpamplona.amethyst.commons.ui.note.creators.notify import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceSheet.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceSheet.kt index d0078663bc..33fb90a02c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/notify/AudienceSheet.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.note.creators.notify +package com.vitorpamplona.amethyst.commons.ui.note.creators.notify import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -53,7 +53,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -184,7 +185,7 @@ private fun AudienceCatalog( // Fixed dp caps (220 + 400) overflow a short screen or a large font scale, // and a bottom sheet clips rather than scrolls — the confirm button would be // unreachable. Budgeting against the actual screen keeps it in view. - val screenHeight = LocalConfiguration.current.screenHeightDp + val screenHeight = windowHeightDp() val query = searchState.text.toString() val sets = @@ -336,7 +337,7 @@ private fun AudienceReview( // Leaves room for the header, the select-all row, the cap notes and the // confirm button, which all have to stay on screen for the sheet to work. - val listMaxHeight = (LocalConfiguration.current.screenHeightDp * 0.42f).dp + val listMaxHeight = (windowHeightDp() * 0.42f).dp val members = remember(list, alreadyInAudience, hidden) { @@ -567,3 +568,13 @@ private fun PeopleList.toAudienceList(kind: AudienceListKind) = publicMembers = publicMembersList, privateMembers = privateMembersList, ) + +/** The window's height in dp, which the sheet budgets its lists against. */ +@Composable +private fun windowHeightDp(): Int = + with(LocalDensity.current) { + LocalWindowInfo.current.containerSize.height + .toDp() + .value + .toInt() + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/pow/PowOverrideButton.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/pow/PowOverrideButton.kt index cc2b0bafc8..cc48bfe61b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/pow/PowOverrideButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/pow/PowOverrideButton.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.note.creators.pow +package com.vitorpamplona.amethyst.commons.ui.note.creators.pow import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -38,7 +38,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -120,7 +119,6 @@ fun PowOverrideButton( ) { // benchmarked on first open (~250 ms, cached after): each option // shows what it would cost on THIS device, e.g. "24 bits · ≈ 45 seconds". - val context = LocalContext.current val hashRate by produceState(initialValue = null) { value = deviceHashesPerSecond() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/DisplayPreviews.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/DisplayPreviews.kt index e0bbc50d32..bc0f04b909 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/DisplayPreviews.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.note.creators.previews +package com.vitorpamplona.amethyst.commons.ui.note.creators.previews import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/PreviewUrl.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/PreviewUrl.kt index f96a54edbf..a92fd8c1b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/previews/PreviewUrl.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.note.creators.previews +package com.vitorpamplona.amethyst.commons.ui.note.creators.previews import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/uploads/ImageVideoDescription.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/uploads/ImageVideoDescription.kt index ffb756e4c1..c33833bb2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/uploads/ImageVideoDescription.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.note.creators.uploads +package com.vitorpamplona.amethyst.commons.ui.note.creators.uploads import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zapsplits/ForwardZapTo.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zapsplits/ForwardZapTo.kt index aea74ca131..04e6ef42c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zapsplits/ForwardZapTo.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.note.creators.zapsplits +package com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModel.kt index d8b4a06ade..c5786ce0a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModel.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.note.nip22Comments +package com.vitorpamplona.amethyst.commons.ui.note.nip22Comments import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -135,6 +135,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.thumbhash import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -451,7 +452,7 @@ open class CommentPostViewModel : CommunityRulesValidator(rules).validate( author = author, kind = CommentEvent.KIND, - sizeBytes = draftContent.toByteArray(Charsets.UTF_8).size, + sizeBytes = draftContent.encodeToByteArray().size, postsTodayByKind = null, wot = null, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/GenericCommentPostScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/GenericCommentPostScreen.kt index b9fd9818e7..57eb0904c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/GenericCommentPostScreen.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.note.nip22Comments +package com.vitorpamplona.amethyst.commons.ui.note.nip22Comments import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -52,7 +52,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote @@ -78,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -87,14 +87,24 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet +import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists +import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayExternalId import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName @@ -110,16 +120,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap -import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet -import com.vitorpamplona.amethyst.ui.note.creators.notify.rememberAudienceLists -import com.vitorpamplona.amethyst.ui.note.creators.pow.PowOverrideButton -import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.collections.immutable.persistentListOf @@ -136,7 +136,7 @@ fun ReplyCommentPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: CommentPostViewModel = viewModel() + val postViewModel: CommentPostViewModel = rememberViewModel { CommentPostViewModel() } postViewModel.init(accountViewModel) val mediaResolver = rememberSharedMediaResolver() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt index 7efff6c07f..2a56e23561 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.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.bookmarkgroups.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.metadata import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -36,11 +36,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.about_us import com.vitorpamplona.amethyst.commons.resources.bookmark_list_creation_screen_title @@ -54,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.optional_picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar @@ -72,7 +71,7 @@ fun BookmarkGroupMetadataScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel = viewModel() + val bookmarkGroupInfoViewModel: BookmarkGroupMetadataViewModel = rememberViewModel { BookmarkGroupMetadataViewModel() } bookmarkGroupInfoViewModel.init(accountViewModel) if (bookmarkGroupIdentifier != null) { @@ -224,7 +223,6 @@ private fun Picture( ) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = bookmarkGroupInfoViewModel.isUploadingImageForPicture, tint = MaterialTheme.colorScheme.placeholderText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index 6f6d426359..c2a74b30e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.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.bookmarkgroups.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.metadata import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 5a0d64a698..67f2da937c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -151,7 +151,9 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -936,7 +938,7 @@ fun GroupRelayStrip( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - val nowSeconds = System.currentTimeMillis() / 1000L + val nowSeconds = TimeUtils.now() normalized.forEach { (raw, relay) -> val lastSeen = relayActivity[relay] val isActive = lastSeen != null && (nowSeconds - lastSeen) <= RELAY_ACTIVITY_WINDOW_SECS @@ -1046,7 +1048,7 @@ fun RelayHealthSection( fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), ) - val nowSeconds = System.currentTimeMillis() / 1000L + val nowSeconds = TimeUtils.now() normalized.forEach { (raw, relay) -> val lastSeen = relayActivity[relay] val isActive = lastSeen != null && (nowSeconds - lastSeen) <= RELAY_ACTIVITY_WINDOW_SECS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt index 99a108d88f..910a496a4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatFeedViewModel.kt @@ -18,11 +18,12 @@ * 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.chats.minichat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.minichat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import kotlin.reflect.KClass /** * The reactive read-model of a message's minichat: the root's kind-1111 [CommentEvent] @@ -65,6 +67,9 @@ class MinichatFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = MinichatFeedViewModel(rootNote, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = MinichatFeedViewModel(rootNote, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index c901b46f26..40665994d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/minichat/MinichatScreen.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.chats.minichat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.minichat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets @@ -48,10 +48,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.publicChannels.concord.datasource.ConcordChannelHistorySubAssembler import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -71,25 +69,27 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalSuppressReplyToNoteId import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.toConcordImeta +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.toPlainImetas import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toConcordImeta -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toPlainImetas import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -138,7 +138,7 @@ fun MinichatScreen( EventFinderFilterAssemblerSubscription(rootNote, accountViewModel) val feedViewModel: MinichatFeedViewModel = - viewModel( + rememberViewModel( key = rootId + "MinichatFeedViewModel", factory = MinichatFeedViewModel.Factory(rootNote, accountViewModel.account), ) @@ -152,7 +152,6 @@ fun MinichatScreen( val composer = remember { TextFieldState() } val scope = rememberCoroutineScope() - val context = LocalContext.current val shortNotice = rememberShortNotice() val longNotice = rememberLongNotice() val canPost by remember { derivedStateOf { composer.text.isNotBlank() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index edeea0251d..9ad5b744af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.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.chats.privateDM.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy @@ -61,7 +61,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList @@ -94,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage @@ -103,15 +103,21 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField +import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.PreviewUrl import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableContentView import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.ChatNewMessageViewModel @@ -130,12 +136,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay @@ -149,7 +149,7 @@ fun NewGroupDMScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ChatNewMessageViewModel = viewModel() + val postViewModel: ChatNewMessageViewModel = rememberViewModel { ChatNewMessageViewModel() } postViewModel.init(accountViewModel) val mediaResolver = rememberSharedMediaResolver() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index 9d068fcbcf..70ef339a73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.publicChannels.concord.datasource.ConcordChannelHistorySubAssembler import com.vitorpamplona.amethyst.commons.chats.ui.ReplyModeToggle import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton @@ -80,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -97,12 +97,16 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.sen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinDuties import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinnedButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPinnedMessagesSheet +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordChannelPins +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.send.ConcordNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.EditingMessageBanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.toConcordImeta import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -112,10 +116,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.send.ConcordNewMessageViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.EditingMessageBanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toConcordImeta import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -123,6 +123,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -158,7 +159,7 @@ fun ConcordChannelScreen( val channel = remember(account, communityId, channelId) { LocalCache.getOrCreateConcordChannel(ConcordChannelId(communityId, channelId)) } val feedViewModel: ChannelFeedViewModel = - viewModel( + rememberViewModel( key = channel.channelId.toKey() + "ConcordFeedViewModel", factory = ChannelFeedViewModel.Factory(channel, account), ) @@ -182,7 +183,7 @@ fun ConcordChannelScreen( } ConcordBackfillHistoryToWindow(feedViewModel.feedState, history) - val newMessageModel: ConcordNewMessageViewModel = viewModel(key = channel.channelId.toKey() + "ConcordNewMessageViewModel") + val newMessageModel: ConcordNewMessageViewModel = rememberViewModel(key = channel.channelId.toKey() + "ConcordNewMessageViewModel") { ConcordNewMessageViewModel() } newMessageModel.init(accountViewModel) newMessageModel.load(communityId, channelId) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt index c836316b36..55cd044e91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistorySubscription.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.chats.publicChannels.concord.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt index bb518a94e7..34bc85cb2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.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.chats.publicChannels.concord.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.send import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 8ca46eb1ab..7a0c7fff6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.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.chats.publicChannels.nip28PublicChat.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -38,13 +38,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel @@ -62,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.public_chat_relays_title import com.vitorpamplona.amethyst.commons.resources.public_chat_title import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -106,7 +105,7 @@ fun ChannelMetadataScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ChannelMetadataViewModel = viewModel() + val postViewModel: ChannelMetadataViewModel = rememberViewModel { ChannelMetadataViewModel() } postViewModel.init(accountViewModel) if (channel != null) { @@ -130,7 +129,7 @@ fun ChannelMetadataScreen( @Composable private fun DialogContentPreview() { val accountViewModel = mockAccountViewModel() - val postViewModel: ChannelMetadataViewModel = viewModel() + val postViewModel: ChannelMetadataViewModel = rememberViewModel { ChannelMetadataViewModel() } postViewModel.init(accountViewModel) ThemeComparisonColumn { @@ -298,7 +297,6 @@ private fun Picture( ) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = postViewModel.isUploadingImageForPicture, tint = MaterialTheme.colorScheme.placeholderText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 788e7562e9..49b7c1e079 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.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.chats.publicChannels.nip28PublicChat.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt index d5b02fa0ef..ec5b6374e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.RepeatMode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt index b46ce30b80..a0f7f2e91e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/InviteRelayGroupDialog.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Column import androidx.compose.material3.AlertDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt index 9e4fe9deb4..5064d4cba3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/JoinRelayGroupDialog.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt index ab8d4a4ec8..063c1b3d75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,7 +38,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistorySubAssembler import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState @@ -50,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_invite_only_to_post import com.vitorpamplona.amethyst.commons.resources.relay_group_join_to_post +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -61,13 +61,13 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.Refreshi import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistorySubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatTailSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistorySubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatTailSubscription import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -110,7 +110,7 @@ private fun PrepareChannelViewModels( nav: INav, ) { val feedViewModel: ChannelFeedViewModel = - viewModel( + rememberViewModel( key = baseChannel.groupId.toKey() + "ChannelFeedViewModel", factory = ChannelFeedViewModel.Factory( @@ -119,7 +119,7 @@ private fun PrepareChannelViewModels( ), ) - val channelScreenModel: ChannelNewMessageViewModel = viewModel() + val channelScreenModel: ChannelNewMessageViewModel = rememberViewModel { ChannelNewMessageViewModel() } channelScreenModel.init(accountViewModel) channelScreenModel.load(baseChannel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt index 3ff4589c70..54e7738278 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt index 506393d4d8..c07ac9acc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMigrationBar.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt index 055e627de8..5fe0e0c971 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt index 6f70b39896..658832f07a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index 4bcd61864e..8457370fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt index d334e18e90..6f08516496 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupChatSubscriptions.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.chats.publicChannels.relayGroup.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt index a000abf5ec..587837a70c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/EditingMessageBanner.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/EditingMessageBanner.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.chats.utils +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/UploadImetas.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/UploadImetas.kt index 820a8b48e2..5fbb0f4198 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/UploadImetas.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/utils/UploadImetas.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.chats.utils +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 34dbee5b4d..5f03485d4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.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.discover.nip99Classifieds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column @@ -39,9 +39,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res @@ -58,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -66,14 +65,20 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageField +import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -82,12 +87,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.FlowPreview @@ -102,10 +101,8 @@ fun NewProductScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: NewProductViewModel = viewModel() + val postViewModel: NewProductViewModel = rememberViewModel { NewProductViewModel() } postViewModel.init(accountViewModel) - - val context = LocalContext.current val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { @@ -280,8 +277,6 @@ private fun NewProductBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val context = LocalContext.current - ImageVideoDescription( uris = it, defaultServer = accountViewModel.account.settings.defaultFileServer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 2c3d44adee..aa7d6a171b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.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.discover.nip99Classifieds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt index 99c07a6bab..42608ab10f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.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.discover.nip99Classifieds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 78967cb3c8..ea8ad3d43e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -18,18 +18,18 @@ * 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.geohash +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.CommentPostViewModel +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.collections.immutable.persistentListOf @@ -45,7 +45,7 @@ fun GeoHashPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: CommentPostViewModel = viewModel() + val postViewModel: CommentPostViewModel = rememberViewModel { CommentPostViewModel() } postViewModel.init(accountViewModel) val mediaResolver = rememberSharedMediaResolver() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index 34c835bd0a..fcd8259fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -18,18 +18,18 @@ * 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.hashtag +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.CommentPostViewModel +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import kotlinx.collections.immutable.persistentListOf @@ -45,7 +45,7 @@ fun HashtagPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: CommentPostViewModel = viewModel() + val postViewModel: CommentPostViewModel = rememberViewModel { CommentPostViewModel() } postViewModel.init(accountViewModel) val mediaResolver = rememberSharedMediaResolver() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt index 3a6763793c..a11ef7505c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.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.lists.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -36,12 +36,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.about_us import com.vitorpamplona.amethyst.commons.resources.follow_pack_copy_name_label @@ -55,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -76,7 +75,7 @@ fun FollowPackMetadataScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: FollowPackMetadataViewModel = viewModel() + val postViewModel: FollowPackMetadataViewModel = rememberViewModel { FollowPackMetadataViewModel() } postViewModel.init(accountViewModel) if (selectedDTag != null) { @@ -100,7 +99,7 @@ fun FollowPackMetadataScreen( @Composable private fun DialogContentPreview() { val accountViewModel = mockAccountViewModel() - val postViewModel: FollowPackMetadataViewModel = viewModel() + val postViewModel: FollowPackMetadataViewModel = rememberViewModel { FollowPackMetadataViewModel() } postViewModel.init(accountViewModel) ThemeComparisonRow { @@ -248,7 +247,6 @@ private fun Picture( ) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = postViewModel.isUploadingImageForPicture, tint = MaterialTheme.colorScheme.placeholderText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt index 631710474a..3ed2c9821a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.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.lists.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt index 3bedc2412c..5fea3b1f21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.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.lists.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -36,12 +36,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.about_us import com.vitorpamplona.amethyst.commons.resources.follow_set_copy_name_label @@ -55,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.people_list_title import com.vitorpamplona.amethyst.commons.resources.picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -76,7 +75,7 @@ fun PeopleListMetadataScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: PeopleListMetadataViewModel = viewModel() + val postViewModel: PeopleListMetadataViewModel = rememberViewModel { PeopleListMetadataViewModel() } postViewModel.init(accountViewModel) if (selectedDTag != null) { @@ -100,7 +99,7 @@ fun PeopleListMetadataScreen( @Composable private fun DialogContentPreview() { val accountViewModel = mockAccountViewModel() - val postViewModel: PeopleListMetadataViewModel = viewModel() + val postViewModel: PeopleListMetadataViewModel = rememberViewModel { PeopleListMetadataViewModel() } postViewModel.init(accountViewModel) ThemeComparisonRow { @@ -248,7 +247,6 @@ private fun Picture( ) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = postViewModel.isUploadingImageForPicture, tint = MaterialTheme.colorScheme.placeholderText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt index f7146d3853..16e5bd53a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.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.lists.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.metadata import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 69cfe5127e..8c009bb682 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.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.notifications.publicMessages +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.publicMessages import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy @@ -50,7 +50,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res @@ -67,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -75,15 +75,22 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Conten import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.location.GeoHashPostSection +import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.MessageFieldRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -95,17 +102,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker -import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews -import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.MessageFieldRow import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -119,7 +120,7 @@ fun NewPublicMessageScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: NewPublicMessageViewModel = viewModel() + val postViewModel: NewPublicMessageViewModel = rememberViewModel { NewPublicMessageViewModel() } postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 11535fc55c..8dd70edb6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.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.notifications.publicMessages +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.publicMessages import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -119,6 +119,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlPostScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlPostScreen.kt index 9fbc1da2c3..b8c704fcd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -18,18 +18,18 @@ * 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.url +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.url import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.CommentPostViewModel +import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel -import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId import kotlinx.collections.immutable.persistentListOf @@ -45,7 +45,7 @@ fun UrlPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: CommentPostViewModel = viewModel() + val postViewModel: CommentPostViewModel = rememberViewModel { CommentPostViewModel() } postViewModel.init(accountViewModel) val mediaResolver = rememberSharedMediaResolver() diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModelTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModelTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModelTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModelTest.kt index f493318dcf..777cc259dd 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModelTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/nip22Comments/CommentPostViewModelTest.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.note.nip22Comments +package com.vitorpamplona.amethyst.commons.ui.note.nip22Comments import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip72ModCommunities.rules.CommunityRulesEvent From 3a1e7ec5ba33f8985a228c70d707bb8c03a48bd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 03:07:59 +0000 Subject: [PATCH 13/45] refactor: wallet, filename, language and local-time seams for more screens - WalletAppLauncher opens any payment URI; payInvoice and payBolt12Offer are extensions, so Bolt12OffersDialog stops launching intents itself. - MediaUploader.displayName replaces MarmotFileUploader's content-resolver query; it no longer takes a Context. - LanguageOption/availableLanguages/languageDisplayName expects in commons replace java.util.Locale in the language settings. - RequestToVanishScreen uses LocalClock and rememberIs24HourClock. - Toasts in MarmotInviteDeviceBanner and Bolt12OffersDialog go through the shared notices; PodcastTopSupporters uses the KMP BigDecimal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../service/uploads/AndroidMediaUploader.kt | 7 ++++ .../chats/marmotGroup/MarmotGroupChatView.kt | 4 --- .../marmotGroup/MarmotInviteDeviceBanner.kt | 13 +++---- .../marmotGroup/send/MarmotFileUploader.kt | 19 ++-------- .../loggedIn/podcasts/PodcastTopSupporters.kt | 4 +-- .../profile/header/Bolt12OffersDialog.kt | 23 +++++------- .../relays/vanish/RequestToVanishScreen.kt | 22 +++++------- .../loggedIn/settings/UserSettingsScreen.kt | 30 +++++++--------- .../commons/service/uploads/MediaUploader.kt | 3 ++ .../amethyst/commons/util/LanguageNames.kt | 33 +++++++++++++++++ .../commons/util/LanguageNames.ios.kt | 35 +++++++++++++++++++ .../commons/util/LanguageNames.jvmAndroid.kt | 33 +++++++++++++++++ .../ui/wallet/WalletAppLauncher.android.kt | 4 +-- .../dvms/DvmContentDiscoveryScreen.kt | 1 + .../commons/ui/wallet/WalletAppLauncher.kt | 25 ++++++++++--- .../ui/wallet/WalletAppLauncher.ios.kt | 6 ++-- .../ui/wallet/WalletAppLauncher.jvm.kt | 6 ++-- 17 files changed, 182 insertions(+), 86 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.ios.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.jvmAndroid.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt index 9615badac2..f0b9ed5f4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads import android.content.Context import android.net.Uri +import android.provider.OpenableColumns import android.webkit.MimeTypeMap import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account @@ -605,4 +606,10 @@ class AndroidMediaUploader( val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault())) FileHeader.prepare(url, mimeType, null) { httpClients.okHttpClientForImage(it) }.getOrNull() } + + override fun displayName(uri: MediaUri): String? = + appContext.contentResolver.query(uri, null, null, null, null)?.use { + val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 4fee3c10b5..b5ed1e7750 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -45,7 +45,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -232,7 +231,6 @@ fun MarmotGroupMessageComposer( ) { val scope = rememberCoroutineScope() val canPost by remember { derivedStateOf { newMessageModel.canPost() } } - val context = LocalContext.current val shortNotice = rememberShortNotice() var isUploading by remember { mutableStateOf(false) } @@ -350,7 +348,6 @@ private fun MarmotGroupFileUploadDialog( onUpload: suspend () -> Unit, onCancel: () -> Unit, ) { - val context = LocalContext.current val shortNotice = rememberShortNotice() val longNotice = rememberLongNotice() val scope = rememberCoroutineScope() @@ -382,7 +379,6 @@ private fun MarmotGroupFileUploadDialog( longNotice.show("$title: $message") } }, - context = context, uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> MarmotFileSender(nostrGroupId, accountViewModel).send(uploads) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt index d8655ff66b..9309f23d35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -45,7 +44,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -57,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_failure import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_publish import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_rejected import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_success +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -96,7 +96,8 @@ fun MarmotInviteDeviceBanner( // has already read and waved away. var dismissed by rememberSaveable { mutableStateOf(false) } val scope = rememberCoroutineScope() - val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() // A read-only login cannot publish at all, so the only action this banner // offers is impossible for it. Better to say nothing than to offer a button // that silently does nothing. @@ -178,9 +179,9 @@ fun MarmotInviteDeviceBanner( val accepted = accountViewModel.republishKeyPackage() launch(Dispatchers.Main) { if (accepted) { - Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show() + shortNotice.show(successMessage) } else { - Toast.makeText(context, rejectedMessage, Toast.LENGTH_LONG).show() + longNotice.show(rejectedMessage) owner = LatestKeyPackageOwner.OTHER_DEVICE } } @@ -194,7 +195,7 @@ fun MarmotInviteDeviceBanner( val failureMessage = loadStringRes(Res.string.marmot_invite_device_failure, e.message ?: "") launch(Dispatchers.Main) { - Toast.makeText(context, failureMessage, Toast.LENGTH_LONG).show() + longNotice.show(failureMessage) // The warning was right after all, so put it back. owner = LatestKeyPackageOwner.OTHER_DEVICE } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt index 371626de80..e5cd237070 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send -import android.content.Context -import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details @@ -79,7 +77,6 @@ class MarmotFileUploader( viewState: ChatFileUploadState, exporterSecret: ByteArray, onError: (title: String, message: String) -> Unit, - context: Context, uploader: MediaUploader, /** * Produce `encrypted-media-v2` references instead of MIP-04 ones. @@ -98,7 +95,7 @@ class MarmotFileUploader( val item = multiOrchestrator.get(i) val media = item.media val mimeType = media.mimeType ?: "application/octet-stream" - val filename = resolveFilename(context, media.uri, mimeType) + val filename = uploader.displayName(media.uri) ?: fallbackFilename(mimeType) // v2 puts `m` inside both the key derivation and the AEAD // associated data, so it has to be the canonical form and not @@ -197,19 +194,7 @@ class MarmotFileUploader( viewState.mediaUploadTracker.finishUpload() } - private fun resolveFilename( - context: Context, - uri: Uri, - mimeType: String, - ): String { - val cursor = context.contentResolver.query(uri, null, null, null, null) - val name = - cursor?.use { - val idx = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) - if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null - } - if (name != null) return name - + private fun fallbackFilename(mimeType: String): String { val ext = when { mimeType.startsWith("image/jpeg") -> "jpg" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index 2ab7bcab5f..5bb2f5a567 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent -import java.math.BigDecimal +import com.vitorpamplona.quartz.utils.BigDecimal private val Gold = Color(0xFFFFC300) private val Silver = Color(0xFFB0B7C0) @@ -151,7 +151,7 @@ private fun SupporterRow( ZapIcon(Size16Modifier, BitcoinOrange) Text( - text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)), + text = showAmountInteger(BigDecimal(entry.totalSats)), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt index 0c89d5d432..667cde6e94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -45,7 +44,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow @@ -62,13 +60,15 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard import com.vitorpamplona.amethyst.commons.resources.no_wallet_found import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.commons.ui.wallet.payBolt12Offer +import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.payViaBolt12Intent import kotlinx.coroutines.launch @Composable @@ -78,7 +78,8 @@ fun Bolt12OffersDialog( onDismiss: () -> Unit, ) { val noWalletFoundStr = stringRes(Res.string.no_wallet_found) - val context = LocalContext.current + val walletApp = rememberWalletAppLauncher() + val shortNotice = rememberShortNotice() val clipboardManager = LocalClipboard.current val scope = rememberCoroutineScope() @@ -101,22 +102,16 @@ fun Bolt12OffersDialog( onCopy = { scope.launch { clipboardManager.setText(offer) - Toast - .makeText( - context, - loadStringRes(Res.string.copied_to_clipboard), - Toast.LENGTH_SHORT, - ).show() + shortNotice.show(loadStringRes(Res.string.copied_to_clipboard)) } }, onPayViaIntent = { - payViaBolt12Intent( + walletApp.payBolt12Offer( offer = offer, - context = context, - noWalletFoundStr, + noWalletFound = noWalletFoundStr, onPaid = { onDismiss() }, onError = { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + shortNotice.show(msg) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt index ef923afa9c..334c456b0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish -import android.text.format.DateFormat import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -59,7 +58,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -89,12 +87,14 @@ import com.vitorpamplona.amethyst.commons.resources.vanish_select_date import com.vitorpamplona.amethyst.commons.resources.vanish_select_time import com.vitorpamplona.amethyst.commons.resources.vanish_send_request import com.vitorpamplona.amethyst.commons.resources.vanish_target_relay +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -108,9 +108,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableList -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -149,14 +146,13 @@ fun RequestToVanishScreen( initialSelectedDateMillis = vanishDate * 1000, ) - val currentTime = Instant.ofEpochMilli(vanishDate * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() - - val context = LocalContext.current + // The vanish time as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSecondOfDay = (vanishDate + LocalClock.utcOffsetSeconds(vanishDate)).mod(TimeUtils.ONE_DAY) val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) val relayOptions = @@ -387,9 +383,7 @@ fun RequestToVanishScreen( (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.now() - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - - vanishDate = datetimeLocalTimeZone - offset.totalSeconds + vanishDate = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) showTimePicker = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 0b8f3f2a38..c4bf9d3bc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -91,9 +91,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy10dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow +import com.vitorpamplona.amethyst.commons.util.LanguageOption +import com.vitorpamplona.amethyst.commons.util.availableLanguages +import com.vitorpamplona.amethyst.commons.util.languageDisplayName import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import java.util.Locale as JavaLocale @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @Composable @@ -134,18 +136,12 @@ fun UserSettingsScreen( } } -private fun getAllLanguagesSorted(): List { - val seen = mutableSetOf() - return JavaLocale - .getAvailableLocales() - .filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) } - .sortedBy { it.displayName.lowercase() } -} +private fun getAllLanguagesSorted(): List = availableLanguages().sortedBy { it.displayName.lowercase() } @Composable private fun SearchableLanguageList( - languages: List, - onSelect: (JavaLocale) -> Unit, + languages: List, + onSelect: (LanguageOption) -> Unit, modifier: Modifier = Modifier, ) { var searchQuery by remember { mutableStateOf("") } @@ -225,7 +221,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = JavaLocale.forLanguageTag(currentTranslateTo).displayName, + text = languageDisplayName(currentTranslateTo), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) @@ -277,7 +273,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { InputChip( selected = true, onClick = { accountViewModel.removeDontTranslateFrom(languageCode) }, - label = { Text(JavaLocale.forLanguageTag(languageCode).displayName) }, + label = { Text(languageDisplayName(languageCode)) }, trailingIcon = { Icon( symbol = MaterialSymbols.Close, @@ -379,8 +375,8 @@ private fun LanguagePreferenceCard( preference: String, accountViewModel: AccountViewModel, ) { - val sourceName = JavaLocale.forLanguageTag(source).displayName - val targetName = JavaLocale.forLanguageTag(target).displayName + val sourceName = languageDisplayName(source) + val targetName = languageDisplayName(target) OutlinedCard(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(12.dp)) { @@ -464,9 +460,9 @@ private fun AddLanguagePairCard( onDismiss: () -> Unit, ) { val allLanguages = remember { getAllLanguagesSorted() } - var selectedSource by remember { mutableStateOf(null) } - var selectedTarget by remember { mutableStateOf(null) } - var selectedPreference by remember { mutableStateOf(null) } + var selectedSource by remember { mutableStateOf(null) } + var selectedTarget by remember { mutableStateOf(null) } + var selectedPreference by remember { mutableStateOf(null) } var pickingSource by remember { mutableStateOf(false) } var pickingTarget by remember { mutableStateOf(false) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt index 285902e8a0..1165156444 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt @@ -86,6 +86,9 @@ interface MediaUploader { url: String, httpClients: IRoleBasedHttpClientBuilder, ): FileHeader? + + /** The file name the picker reported for [uri] (Android's `DISPLAY_NAME`), if any. */ + fun displayName(uri: MediaUri): String? = uri.lastPathSegmentOrNull() } /** No media pipeline on this front end yet: every upload fails with a clear message. */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.kt new file mode 100644 index 0000000000..8be7292975 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.util + +/** A language the user can pick: its ISO code and its name in the user's own language. */ +data class LanguageOption( + val language: String, + val displayName: String, +) + +/** Every language the platform can name, one entry per language code (no regional variants). */ +expect fun availableLanguages(): List + +/** The name of the language [tag] (a BCP-47 tag such as `pt` or `pt-BR`) in the user's language. */ +expect fun languageDisplayName(tag: String): String diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.ios.kt new file mode 100644 index 0000000000..15d0fe71f1 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.ios.kt @@ -0,0 +1,35 @@ +/* + * 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.commons.util + +import platform.Foundation.NSLocale +import platform.Foundation.availableLocaleIdentifiers +import platform.Foundation.currentLocale +import platform.Foundation.localizedStringForLanguageCode + +actual fun availableLanguages(): List = + NSLocale.availableLocaleIdentifiers + .mapNotNull { (it as? String)?.substringBefore('_')?.substringBefore('-') } + .filter { it.isNotBlank() } + .distinct() + .map { LanguageOption(it, languageDisplayName(it)) } + +actual fun languageDisplayName(tag: String): String = NSLocale.currentLocale.localizedStringForLanguageCode(tag) ?: tag diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.jvmAndroid.kt new file mode 100644 index 0000000000..e3ee56e4e0 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/LanguageNames.jvmAndroid.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.util + +import java.util.Locale + +actual fun availableLanguages(): List { + val seen = mutableSetOf() + return Locale + .getAvailableLocales() + .filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) } + .map { LanguageOption(it.language, it.displayName) } +} + +actual fun languageDisplayName(tag: String): String = Locale.forLanguageTag(tag).displayName diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt index 2977a00f85..aba51de5b0 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt @@ -31,9 +31,9 @@ import kotlin.coroutines.cancellation.CancellationException actual fun rememberWalletAppLauncher(): WalletAppLauncher { val context = LocalContext.current return remember(context) { - WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + WalletAppLauncher { uri, noWalletFound, onPaid, onError -> try { - val intent = Intent(Intent.ACTION_VIEW, "lightning:$invoice".toUri()) + val intent = Intent(Intent.ACTION_VIEW, uri.toUri()) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 403708c1cc..829d030b12 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage75Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp +import com.vitorpamplona.amethyst.commons.ui.wallet.payInvoice import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.lightning.LnInvoiceUtil diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt index 3b37a76947..6b9074bc97 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt @@ -25,16 +25,33 @@ import androidx.compose.runtime.Composable /** Hands a payment to a wallet app installed on the device. */ fun interface WalletAppLauncher { /** - * Opens [invoice] (BOLT-11) in the wallet app. [onPaid] runs once the wallet took it, which is - * not a payment confirmation; [onError] gets [noWalletFound] when no wallet can open it. + * Opens the payment [uri] (`lightning:` or `bitcoin:`) in the wallet app. [onPaid] runs once + * the wallet took it, which is not a payment confirmation; [onError] gets [noWalletFound] when + * no wallet can open it. */ - fun payInvoice( - invoice: String, + fun openPaymentUri( + uri: String, noWalletFound: String, onPaid: () -> Unit, onError: (String) -> Unit, ) } +/** Opens a BOLT-11 [invoice] in the wallet app; see [WalletAppLauncher.openPaymentUri]. */ +fun WalletAppLauncher.payInvoice( + invoice: String, + noWalletFound: String, + onPaid: () -> Unit, + onError: (String) -> Unit, +) = openPaymentUri("lightning:$invoice", noWalletFound, onPaid, onError) + +/** Opens a BOLT-12 [offer] in the wallet app; see [WalletAppLauncher.openPaymentUri]. */ +fun WalletAppLauncher.payBolt12Offer( + offer: String, + noWalletFound: String, + onPaid: () -> Unit, + onError: (String) -> Unit, +) = openPaymentUri("bitcoin:?lno=$offer", noWalletFound, onPaid, onError) + @Composable expect fun rememberWalletAppLauncher(): WalletAppLauncher diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt index 9ecd3a05f9..e189462710 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt @@ -25,14 +25,14 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalUriHandler import kotlin.coroutines.cancellation.CancellationException -/** Opens the `lightning:` link with whatever the system registered for it. */ +/** Opens the payment link with whatever the system registered for it. */ @Composable actual fun rememberWalletAppLauncher(): WalletAppLauncher { val uriHandler = LocalUriHandler.current return remember(uriHandler) { - WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + WalletAppLauncher { uri, noWalletFound, onPaid, onError -> try { - uriHandler.openUri("lightning:$invoice") + uriHandler.openUri(uri) onPaid() } catch (e: Exception) { if (e is CancellationException) throw e diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt index 9ecd3a05f9..e189462710 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt @@ -25,14 +25,14 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalUriHandler import kotlin.coroutines.cancellation.CancellationException -/** Opens the `lightning:` link with whatever the system registered for it. */ +/** Opens the payment link with whatever the system registered for it. */ @Composable actual fun rememberWalletAppLauncher(): WalletAppLauncher { val uriHandler = LocalUriHandler.current return remember(uriHandler) { - WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + WalletAppLauncher { uri, noWalletFound, onPaid, onError -> try { - uriHandler.openUri("lightning:$invoice") + uriHandler.openUri(uri) onPaid() } catch (e: Exception) { if (e is CancellationException) throw e From bea66127c6770d1a0fb7a74c0a7bf6d92de8a656 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 03:29:58 +0000 Subject: [PATCH 14/45] refactor: move the Marmot group list/chat, podcast, Bolt12 offers and settings screens 22 files freed by the wallet, filename, language and local-time seams move to commonsUI: MarmotGroupListScreen and MarmotGroupChatScreen with their views and uploader, PodcastScreen with its list and supporters, Bolt12OffersScreen, RequestToVanishScreen and UserSettingsScreen. The vanish preview builds its NIP-11 retriever from Nip11Fetcher.Offline so it compiles for iOS. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/navigation/AppNavigation.kt | 12 ++++++------ .../profile/header/ProfilePaymentRailChips.kt | 2 ++ .../ui/actions/bolt12Offers/Bolt12OffersScreen.kt | 8 ++++---- .../actions/bolt12Offers/Bolt12OffersViewModel.kt | 2 +- .../chats/marmotGroup/MarmotGroupChatScreen.kt | 2 +- .../chats/marmotGroup/MarmotGroupChatView.kt | 15 ++++++++------- .../chats/marmotGroup/MarmotGroupFeedViewModel.kt | 9 +++++++-- .../chats/marmotGroup/MarmotGroupListScreen.kt | 2 +- .../chats/marmotGroup/MarmotInviteDeviceBanner.kt | 3 ++- .../chats/marmotGroup/MarmotSystemRowRenderer.kt | 2 +- .../chats/marmotGroup/send/MarmotFileSender.kt | 2 +- .../chats/marmotGroup/send/MarmotFileUploader.kt | 2 +- .../marmotGroup/send/MarmotNewMessageViewModel.kt | 2 +- .../ui/screen/loggedIn/podcasts/PodcastAuthors.kt | 2 +- .../loggedIn/podcasts/PodcastEpisodeListItem.kt | 4 +--- .../ui/screen/loggedIn/podcasts/PodcastHeader.kt | 2 +- .../ui/screen/loggedIn/podcasts/PodcastScreen.kt | 10 +++++----- .../loggedIn/podcasts/PodcastTopSupporters.kt | 2 +- .../podcasts/dal/OnePodcastEpisodesFeedFilter.kt | 2 +- .../podcasts/dal/OnePodcastFeedViewModel.kt | 9 +++++++-- .../OnePodcastFilterAssemblerSubscription.kt | 2 +- .../loggedIn/profile/header/Bolt12OffersDialog.kt | 2 +- .../relays/vanish/RequestToVanishScreen.kt | 5 +++-- .../loggedIn/settings/UserSettingsScreen.kt | 2 +- 24 files changed, 59 insertions(+), 46 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/bolt12Offers/Bolt12OffersScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt (84%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/PodcastAuthors.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/PodcastHeader.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/PodcastScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt (84%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/settings/UserSettingsScreen.kt (99%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 51bbe94e9a..6016c74acf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri_description import com.vitorpamplona.amethyst.commons.ui.actions.NewUserMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.actions.bolt12Offers.Bolt12OffersScreen import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetsScreen import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator @@ -101,7 +102,9 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.G import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.minichat.MinichatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen @@ -175,6 +178,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.NsitesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastEpisodesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen @@ -193,6 +197,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.RelayInforma import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.eventsync.EventSyncScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip43.RelayMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions.ActiveSubscriptionsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.VanishEventsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen @@ -208,6 +213,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ProfileUiS import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SpammingUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.VideoPlayerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnCoordinatorsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnHubScreen @@ -245,7 +251,6 @@ import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayA import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages import com.vitorpamplona.amethyst.service.resourceusage.DisplayResourceUsageAlert import com.vitorpamplona.amethyst.service.resourceusage.ScreenTimeIntegrator -import com.vitorpamplona.amethyst.ui.actions.bolt12Offers.Bolt12OffersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomBlobManagerScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomImportScreen @@ -279,8 +284,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreat import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen @@ -315,14 +318,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen @@ -333,7 +334,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ResourceUsageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt index fef65c0796..e8c363be58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt @@ -71,6 +71,8 @@ import com.vitorpamplona.amethyst.commons.resources.send_payment_method_onchain import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.Bolt12OffersDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersScreen.kt index 115cf97b94..6539326b30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersScreen.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.actions.bolt12Offers +package com.vitorpamplona.amethyst.commons.ui.actions.bolt12Offers import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -51,7 +51,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -63,9 +62,11 @@ import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_section_explai import com.vitorpamplona.amethyst.commons.resources.delete_bolt12_offer import com.vitorpamplona.amethyst.commons.resources.invalid_bolt12_offer import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder @@ -76,14 +77,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer @Composable fun Bolt12OffersScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: Bolt12OffersViewModel = viewModel() + val viewModel: Bolt12OffersViewModel = rememberViewModel { Bolt12OffersViewModel() } viewModel.init(accountViewModel) LaunchedEffect(key1 = accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt index 4ea27a19aa..9cdcc19f2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/bolt12Offers/Bolt12OffersViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/bolt12Offers/Bolt12OffersViewModel.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.actions.bolt12Offers +package com.vitorpamplona.amethyst.commons.ui.actions.bolt12Offers import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt index fcd934f5f4..4fff094bc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index b5ed1e7750..c7793cca4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -48,7 +48,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute @@ -71,12 +70,16 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatIsOneOnOne import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send.MarmotNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote @@ -90,14 +93,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileSender -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotNewMessageViewModel import com.vitorpamplona.quartz.marmot.protocolCore.LocalOutboundGate import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Composable @@ -109,7 +110,7 @@ fun MarmotGroupChatView( nav: INav, ) { val feedViewModel: MarmotGroupFeedViewModel = - viewModel( + rememberViewModel( key = nostrGroupId + "MarmotGroupFeedViewModel", factory = MarmotGroupFeedViewModel.Factory( @@ -125,7 +126,7 @@ fun MarmotGroupChatView( accountViewModel.account.marmotGroupList.getOrCreateGroup(nostrGroupId) } - val newMessageModel: MarmotNewMessageViewModel = viewModel(key = nostrGroupId + "MarmotNewMessageViewModel") + val newMessageModel: MarmotNewMessageViewModel = rememberViewModel(key = nostrGroupId + "MarmotNewMessageViewModel") { MarmotNewMessageViewModel() } newMessageModel.init(accountViewModel) newMessageModel.load(nostrGroupId) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt index 8db68d763e..50ef95e08c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.feeds.MarmotGroupFeedFilter import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.viewmodels.ListChangeFeedViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlin.reflect.KClass class MarmotGroupFeedViewModel( nostrGroupId: HexKey, @@ -40,6 +42,9 @@ class MarmotGroupFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = MarmotGroupFeedViewModel(nostrGroupId, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = MarmotGroupFeedViewModel(nostrGroupId, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index ead2bc081e..48651ebfcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt index 9309f23d35..c1f7a69a3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotInviteDeviceBanner.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.cancellation.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt index 0f4351f6bf..c2e1a2b8c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt index 1309eb280b..3f21a77082 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileSender.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.chats.marmotGroup.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.marmot.mip04EncryptedMedia.buildMip04IMetaTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt index e5cd237070..015cdba5db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.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.chats.marmotGroup.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt index 1324b88e5b..8cf84d4ea2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.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.chats.marmotGroup.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastAuthors.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastAuthors.kt index 4eaace58e1..2fcecfcf68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastAuthors.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.podcasts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index 563182ba93..16c1e827cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.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.podcasts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -79,7 +78,6 @@ fun PodcastEpisodeListItem( val season = remember(noteEvent) { episode.episodeSeason() } val episodeNumber = remember(noteEvent) { episode.episodeNumber() } - val context = LocalContext.current val timeLabels = rememberTimeAgoLabels() val dateStr = remember(noteEvent, timeLabels) { timeAgoWith(noteEvent.createdAt, timeLabels, prefix = "") } val seasonEpisodeLabel = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastHeader.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastHeader.kt index 16432a19d7..6899f5e005 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastHeader.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.podcasts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastScreen.kt index 83e79a7e3e..e300ea83fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastScreen.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.podcasts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer @@ -41,7 +41,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User @@ -50,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_no_episodes import com.vitorpamplona.amethyst.commons.resources.route_podcasts +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel @@ -61,14 +61,14 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastBookmarkButton import com.vitorpamplona.amethyst.commons.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastTrailerListItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.dal.OnePodcastFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource.OnePodcastFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal.OnePodcastFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.OnePodcastFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent @@ -98,7 +98,7 @@ fun PodcastScreen( } val feedViewModel: OnePodcastFeedViewModel = - viewModel( + rememberViewModel( key = pubkey + "OnePodcastFeedViewModel", factory = OnePodcastFeedViewModel.Factory(pubkey, accountViewModel.account), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index 5bb2f5a567..2884af5d8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.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.podcasts +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt index dbe4d2c532..7b727a48dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastEpisodesFeedFilter.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.podcasts.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt index 14f1ead528..ec01106027 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/dal/OnePodcastFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.podcasts.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlin.reflect.KClass @Stable class OnePodcastFeedViewModel( @@ -40,6 +42,9 @@ class OnePodcastFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = OnePodcastFeedViewModel(podcastPubkey, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = OnePodcastFeedViewModel(podcastPubkey, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt index c60c68521b..79e08319a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/datasource/OnePodcastFilterAssemblerSubscription.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.podcasts.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt index 667cde6e94..9c25c74a52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/header/Bolt12OffersDialog.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.profile.header +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt index 334c456b0c..0281177223 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.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.relays.vanish +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11CachedRetriever +import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11Fetcher import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.confirm @@ -481,7 +482,7 @@ private fun ConfirmVanishDialog( fun RequestToVanishScreenPreview() { ThemeComparisonColumn { RequestToVanishScreen( - nip11CachedRetriever = Nip11CachedRetriever { TODO() }, + nip11CachedRetriever = Nip11CachedRetriever(Nip11Fetcher.Offline), accountViewModel = mockAccountViewModel(), nav = EmptyNav(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UserSettingsScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UserSettingsScreen.kt index c4bf9d3bc3..39c7f00e8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UserSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement From cbb18379f04f11ae2f12f9d388f776d94a2430ca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 03:30:37 +0000 Subject: [PATCH 15/45] docs: record step 6 wave 8 in the one-UI plan Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../2026-09-27-one-ui-android-desktop.md | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index 7f5e5d7428..6c28cd910a 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -446,12 +446,26 @@ assemblers, `EventSync`, …). Packages are renamed on the way: pooled media3 `MediaController` on Android. Do it when Desktop gets a player, and pick that engine through the license gate. VLCJ's README says "GPL, version 3 or later", with no linking exception (a commercial license is sold separately), so it is a stop. - - **Next:** the remaining screens exit mostly through navigation (`RememberNavs`, `Nav`, - step 7). After that: `Amethyst.instance` reads, the relay exporters, `TakePicture`, - `ReactionsRow`, `ZoomableContentView`, `UrlCachedPreviewer`, `MLKitImageLabelService` - and `ShortNotePostViewModel`. Smaller exits remain too: the Android-only - `AccountViewModel` actions, `CalendarTimeFormat`, `NestUiState`, `NappletLauncher`, - `UpdateZapAmountDialog` (biometrics), the `java.time` pickers and the language list. + - **Wave 8 (2026-09-30):** the composers. + - Ports: `FileSharer` (the relay ZIP export), `TakePicture`/`TakeVideo`, + `rememberCoarseLocationPermission`, `SharedMediaResolver` + `OnIncomingShare` (SEND + intents), `DeviceLocation` (on `AccountViewModelHost` and `AppServices`), + `AltTextSuggester`, `BlossomServerFinder`, `mediaUriOfFile`, `availableProcessors`, + the language names, `LocalClock.utcOffsetSeconds` and `rememberIs24HourClock`. + - `MediaUploader` gained `remoteFileHeader` (imeta for pasted links) and `displayName`. + The host gained `scheduledPostStore`, `anonymizeVoice` and `createWritingAssistant`. + - Voice recordings are okio `Path`s. `commons` exposes okio as `api`. + - `WalletAppLauncher` opens any payment URI; BOLT-11 and BOLT-12 are extensions. + - Toasts go through `rememberShortNotice`/`rememberLongNotice`. + - Moved: every note composer and `ShortNotePostViewModel`; the public, ephemeral, + geohash, DM, Marmot, Concord, Minichat and relay-group chats; the media feeds; the + relay settings; the metadata editors; podcasts; Bolt12 offers; the vanish and + language settings. 180 of 252 screens shared. + - **Next:** 72 screens remain. `Amethyst.instance` reads block 16 of them, and + `LocalPreferences` another 5. Smaller exits: `UpdateZapAmountDialog` (biometrics), + `NestActivity` (PiP), `CalendarTimeFormat`, `GalleryThumb` (media3), + `AccountSessionManager`, the Blossom health probe, `ReactionsRow`, `NappletLauncher` + and the call permissions. 7. **Navigation**: the library swap, then `AppNavigation` + rail + drawer + bottom bar. 8. **The app root port** and the new JVM shim. Then the Desktop feature inventory, and retiring the old `desktopApp`. From 05bbe21f8792027654268a7ca80a3f746d368d17 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 03:51:43 +0000 Subject: [PATCH 16/45] refactor: replace Amethyst.instance reads in screens with app services and host ports - AppServices gains appStores (the preference DataStores) and torBootstrapped; the chess view-model factory, search history and the relay-group Tor banner read them instead of Amethyst.instance. - The relay-group banner reads Tor routing from the host; ClinkOfferPayer takes the host's MoneyOpRelayRouting; the napplet manifest lookup reads LocalCache directly. - AccountViewModelHost gains hasBackedUpKeys/setHasBackedUpKeys for the backup nudge, over LocalPreferences on Android. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/napplet/NappletManifestLookup.kt | 4 ++-- .../amethyst/service/ClinkOfferPayer.kt | 5 +++-- .../note/creators/invoice/ClinkOfferPreview.kt | 2 +- .../amethyst/ui/note/types/Chess.kt | 3 ++- .../amethyst/ui/platform/AndroidAppServices.kt | 9 +++++++++ .../loggedIn/AndroidAccountViewModelHost.kt | 7 +++++++ .../relayGroup/RelayGroupChannelListScreen.kt | 9 ++++----- .../loggedIn/chess/ChessDismissedGamesData.kt | 5 ++--- .../screen/loggedIn/chess/ChessGameScreen.kt | 3 ++- .../screen/loggedIn/chess/ChessLobbyScreen.kt | 3 ++- .../loggedIn/chess/ChessViewModelFactory.kt | 4 +++- .../screen/loggedIn/home/NewChessGameButton.kt | 3 ++- .../loggedIn/keyBackup/BackupKeysNudge.kt | 18 +++++++++--------- .../profile/payment/SendPaymentScreen.kt | 2 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 5 +++-- .../amethyst/commons/service/AppServices.kt | 8 ++++++++ .../commons/viewmodels/AccountViewModelHost.kt | 13 +++++++++++++ 17 files changed, 73 insertions(+), 30 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt index 067661e93e..2e97d994fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletManifestLookup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.napplet -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent @@ -38,7 +38,7 @@ fun resolveNappletMeta( untitled: String, ): Pair { val events = - Amethyst.instance.cache + LocalCache .filter( Filter( kinds = listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND, RootSiteEvent.KIND, NamedSiteEvent.KIND), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt index 711c20d74e..d317102733 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.quartz.experimental.clink.client.OfferClient import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent import com.vitorpamplona.quartz.experimental.clink.offers.OfferResponse @@ -57,6 +57,7 @@ object ClinkOfferPayer { */ suspend fun requestInvoice( account: Account, + moneyOpRelays: MoneyOpRelayRouting, offer: NOffer, amountSats: Long? = null, timeoutMs: Long = DEFAULT_TIMEOUT_MS, @@ -103,7 +104,7 @@ object ClinkOfferPayer { // pushing the payment through Tor (and failing on services that block Tor exits) even when // the user disabled Tor for money operations. The subscribe() below triggers a reconnect, and // BasicRelayClient rebuilds any socket left on the now-wrong (Tor) transport onto clearnet. - val torState = Amethyst.instance.torEvaluatorFlow + val torState = moneyOpRelays torState.registerMoneyOpRelays(relays) account.client.subscribe(subId, filters, listener) try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt index 9fbe8d74f1..fb26066669 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt @@ -182,7 +182,7 @@ fun ClinkOfferPreview( ) { val amount = if (amountRequired) amountInput.toLongOrNull() else useOffer.price - val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, useOffer, amountSats = amount) + val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, accountViewModel.host.moneyOpRelays, useOffer, amountSats = amount) val bolt11 = response?.bolt11 val movedTo = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index d1afd49999..4648940283 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory @@ -127,7 +128,7 @@ fun RenderLiveChessChallenge( val chessViewModel: ChessViewModel = viewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) val isOpenChallenge = event.opponentPubkey() == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index ae647dadc1..9b56a1e0a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.AppServices import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder @@ -33,6 +34,8 @@ import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * [AppServices] over the main process's app modules. Every member is a getter, so installing @@ -52,6 +55,12 @@ object AndroidAppServices : AppServices { override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value + override val torBootstrapped: Flow get() = + Amethyst.instance.torManager.status + .map { it.isFullyBootstrapped } + + override val appStores: AppPreferenceStores get() = Amethyst.instance.appStores + override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index ddd888b277..e1c361343c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -86,6 +86,13 @@ class AndroidAccountViewModelHost( override val scheduledPostStore: ScheduledPostStore get() = modules.scheduledPostStore + override suspend fun hasBackedUpKeys(npub: String): StateFlow = LocalPreferences.hasBackedUpKeys(npub) + + override suspend fun setHasBackedUpKeys( + npub: String, + value: Boolean, + ) = LocalPreferences.setHasBackedUpKeys(value, npub) + override suspend fun anonymizeVoice( input: Path, presetName: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index d227ec69ec..bb46aad431 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.buzz.ui.PresenceDot import com.vitorpamplona.amethyst.commons.chats.publicChannels.relayGroup.newestTimelineNote import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -107,6 +106,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListViewModel @@ -366,7 +366,7 @@ fun RelayGroupChannelListScreen( // was never consulted, and neither was whether this relay is Tor-routed at all (Tor being *on* // doesn't mean this relay goes through it; the per-role presets decide). val torEvaluation = - Amethyst.instance.torEvaluatorFlow.flow + accountViewModel.host.torRelayEvaluation .collectAsStateWithLifecycle() // The same predicate the relay pool itself dials with, so the banner can't claim Tor for a relay // the app is reaching over clearnet (onion / localhost / trusted-off-Tor are all folded in here). @@ -374,9 +374,8 @@ fun RelayGroupChannelListScreen( // While Tor is bootstrapping every Tor-routed relay is silent — that's Tor's own failure (and its // own dialog), so don't let it read as "this relay blocks Tor exits". - val torStatus by Amethyst.instance.torManager.status - .collectAsStateWithLifecycle() - val torIsUp = torStatus.isFullyBootstrapped + val torIsUp by LocalAppServices.current.torBootstrapped + .collectAsStateWithLifecycle(false) // The offer adds the relay to the kind-10089 Trusted list, which only moves it to clearnet while // trusted relays are *off* Tor. Under the Small-Payloads / Full-Privacy presets they are on Tor, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt index a0cba559e4..8aa803bd54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores /** * Where Android keeps the dismissed-chess-games store. @@ -32,5 +32,4 @@ import com.vitorpamplona.amethyst.Amethyst * data over is not worth the code. Anyone who had dismissed a game sees it once * more and dismisses it again. */ -internal val chessDismissedGamesData: DataStore - get() = Amethyst.instance.appStores.getDataStore("chess_dismissed_games_v2") +internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore = stores.getDataStore("chess_dismissed_games_v2") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index 183dfb2e18..11af042705 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.chess_loading_game import com.vitorpamplona.amethyst.commons.resources.go_back import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -104,7 +105,7 @@ fun ChessGameScreen( viewModel( viewModelStoreOwner = activity, key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) val activeGames by chessViewModel.activeGames.collectAsState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index a41afb483a..89e93c15d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.resources.route_chess import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription @@ -102,7 +103,7 @@ fun ChessLobbyScreen( viewModel( viewModelStoreOwner = activity, key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) // Subscribe to chess events when screen is visible diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index ab7d7a646e..ab27e660cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel @@ -34,6 +35,7 @@ import kotlin.reflect.KClass */ class ChessViewModelFactory( private val account: Account, + private val appStores: AppPreferenceStores, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( @@ -47,7 +49,7 @@ class ChessViewModelFactory( fetcher = AndroidRelayFetcher(account), metadataProvider = AndroidMetadataProvider(), pollingConfig = ChessPollingDefaults.android, - dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData), + dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)), ) as T } throw IllegalArgumentException("Unknown ViewModel class") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt index 22c74905d0..a0e91364df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.nip64Chess.ui.NewChessGameDialog import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_chess_game import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -56,7 +57,7 @@ fun NewChessGameButton( val chessViewModel: ChessViewModel = viewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account), + factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) FloatingActionButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt index 7dd5fce323..0054503041 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -58,7 +57,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip19Bech32.toNpub -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** @@ -66,7 +65,7 @@ import kotlinx.coroutines.launch * generated accounts that haven't saved their secret key yet. It never blocks * navigation: the user either backs up (navigates to [Route.AccountBackup]) or * confirms they already saved the key. Both actions flip the per-account - * [LocalPreferences.setHasBackedUpKeys] flag so the nudge stops appearing. + * backed-up flag so the nudge stops appearing. */ @Composable fun BackupKeysNudge( @@ -81,19 +80,20 @@ fun BackupKeysNudge( // Seed the reactive flag off a background read. Rendering only proceeds once the // flow resolves, so the observing composable never conditionally calls hooks. - val flow by produceState?>(initialValue = null, key1 = npub) { - value = LocalPreferences.hasBackedUpKeys(npub) + val flow by produceState?>(initialValue = null, key1 = npub) { + value = accountViewModel.host.hasBackedUpKeys(npub) } flow?.let { stateFlow -> - WatchBackupKeysNudge(stateFlow, npub, nav, modifier) + WatchBackupKeysNudge(stateFlow, npub, accountViewModel, nav, modifier) } } @Composable private fun WatchBackupKeysNudge( - stateFlow: MutableStateFlow, + stateFlow: StateFlow, npub: String, + accountViewModel: AccountViewModel, nav: INav, modifier: Modifier, ) { @@ -106,11 +106,11 @@ private fun WatchBackupKeysNudge( onBackupNow = { // Best-effort: opening the backup screen counts as backing up so the // nudge doesn't linger after the user follows through. - scope.launch { LocalPreferences.setHasBackedUpKeys(true, npub) } + scope.launch { accountViewModel.host.setHasBackedUpKeys(npub, true) } nav.nav(Route.AccountBackup) }, onAlreadySaved = { - scope.launch { LocalPreferences.setHasBackedUpKeys(true, npub) } + scope.launch { accountViewModel.host.setHasBackedUpKeys(npub, true) } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index f30f5a1bbe..c695660415 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -488,7 +488,7 @@ private fun SendPaymentLoaded( amount: Long?, followMoved: Boolean, ) { - val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, useOffer, amountSats = amount) + val response = ClinkOfferPayer.requestInvoice(accountViewModel.account, accountViewModel.host.moneyOpRelays, useOffer, amountSats = amount) val bolt11 = response?.bolt11 val movedTo = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 1417b7553f..34f42ff0fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -79,7 +79,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -130,6 +129,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserCompose import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow @@ -168,7 +168,8 @@ fun SearchScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val historyStorage = remember { DataStoreSearchHistoryStorage(Amethyst.instance.appStores.getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) } + val appStores = LocalAppServices.current.appStores + val historyStorage = remember { DataStoreSearchHistoryStorage(appStores.getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) } val searchBarViewModel: SearchBarViewModel = viewModel( // Keyed on the seed: navigating from one screen's search button to another's has to diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index 2f113e23a5..542bd89360 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -25,10 +25,12 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver +import kotlinx.coroutines.flow.Flow /** * App-wide stores that outlive any one account and that screens read directly: the favorites @@ -53,6 +55,12 @@ interface AppServices { val torSettings: TorSettingsFlow + /** Whether the embedded Tor service has fully bootstrapped, so Tor-routed relays can answer. */ + val torBootstrapped: Flow + + /** The app's preference files, one DataStore per name. */ + val appStores: AppPreferenceStores + /** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */ val namecoinResolver: NamecoinNameResolver diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 6eb5d1830c..4e6967c763 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import okio.Path @@ -93,6 +94,18 @@ interface AccountViewModelHost { /** A new on-device writing assistant, or null where the platform has none. */ fun createWritingAssistant(): WritingAssistant? = null + /** + * Whether the account [npub] has saved its secret key, as a live flag. Absent reads as true, + * so a platform that does not track it never shows the backup nudge. + */ + suspend fun hasBackedUpKeys(npub: String): StateFlow = MutableStateFlow(true) + + /** Records whether the account [npub] has saved its secret key. */ + suspend fun setHasBackedUpKeys( + npub: String, + value: Boolean, + ) = Unit + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> From a6e6e8d2902eeba95d00b0ce461ce92efb3e2ad9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:03:27 +0000 Subject: [PATCH 17/45] refactor: shared calendar date formatting, markdown preview slot and cached place names - CalendarTimeFormat moves to commonsUI on DateSkeletonFormatter, LocalClock and calendarYearAndDay; range and time-of-day formatting take the composable's rememberTimeOfDayFormatter instead of a Context. - LongFormPostScreen previews through LocalRichTextPlatform.Markdown. - AppServices.cachedPlaceName replaces SearchScreen's direct read of the reverse-geocoding cache. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../ui/note/types/CalendarRsvpRender.kt | 9 +- .../ui/platform/AndroidAppServices.kt | 3 + .../loggedIn/calendars/CalendarDayView.kt | 8 +- .../calendars/CalendarEventListCard.kt | 7 +- .../loggedIn/calendars/CalendarMonthView.kt | 3 + .../loggedIn/calendars/CalendarTimeFormat.kt | 146 ------------------ .../loggedIn/calendars/CalendarWeekView.kt | 3 + .../create/NewCalendarCollectionScreen.kt | 2 +- .../detail/CalendarEventDetailScreen.kt | 8 +- .../nip23LongForm/LongFormPostScreen.kt | 5 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 4 +- .../amethyst/commons/service/AppServices.kt | 3 + .../loggedIn/calendars/CalendarTimeFormat.kt | 98 ++++++++++++ 13 files changed, 134 insertions(+), 165 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt index 9acc19fe61..60428dea67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.AddressableNote @@ -47,13 +46,14 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.CalendarRsvpCard import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarAppointmentLines import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarDateBadge import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detailRouteFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -133,10 +133,9 @@ private fun RsvpWithAppointment( CalendarRsvpCard(event) return } - - val context = LocalContext.current + val timeFormatter = rememberTimeOfDayFormatter() val appointmentEvent = appointment.event - val range = remember(appointmentEvent) { formatCalendarRange(appointment, context) } + val range = remember(appointmentEvent) { formatCalendarRange(appointment, timeFormatter) } val relative = rememberRelativeTimeLabel(view, appointmentEvent?.id) val route = remember(appointment) { detailRouteFor(appointment) } // Past a week the relative formatter falls back to a plain date ("October 31"), which the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt index 9b56a1e0a5..f6f1375a9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService +import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -67,6 +68,8 @@ object AndroidAppServices : AppServices { override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager + override fun cachedPlaceName(geohash: String): String? = CachedReversedGeoLocations.cached(geohash) + override fun createAltTextSuggester(): AltTextSuggester = MLKitImageLabelService(Amethyst.instance.appContext) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt index aa4f0a7981..03701e97df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt @@ -43,7 +43,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -64,6 +63,9 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_day import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_day import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatTimeOfDay import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -161,7 +163,7 @@ private fun DayRow( } val startSeconds = view.startSeconds - val context = LocalContext.current + val timeFormatter = rememberTimeOfDayFormatter() val timeLabel = when { view.isAllDay -> stringRes(Res.string.calendar_all_day) @@ -170,7 +172,7 @@ private fun DayRow( // misleading on day 2 since the event has been ongoing overnight. Show a // continuation marker so the user reads it as "still happening". stringRes(Res.string.calendar_continues) - startSeconds != null -> formatTimeOfDay(startSeconds, context) + startSeconds != null -> formatTimeOfDay(startSeconds, timeFormatter) else -> "—" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt index 1d611b18a5..b08505e1bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -53,6 +52,8 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.CalendarAppointmen import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent @@ -80,8 +81,8 @@ fun CalendarEventListCard( footer: (@Composable () -> Unit)? = null, ) { val view = note.appointmentView() ?: return - val context = LocalContext.current - val range = remember(note.idHex) { formatCalendarRange(note, context) } + val timeFormatter = rememberTimeOfDayFormatter() + val range = remember(note.idHex) { formatCalendarRange(note, timeFormatter) } val relative = rememberRelativeTimeLabel(view, note.idHex) val event = note.event ?: return val detailRoute = remember(event.id) { detailRouteFor(note) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt index d07e649d06..dad7066cc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt @@ -61,6 +61,9 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_month import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_month import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatMonthYear +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatShortWeekday import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt deleted file mode 100644 index 7f026605ab..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt +++ /dev/null @@ -1,146 +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.calendars - -import android.content.Context -import android.text.format.DateFormat -import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale - -// Skeletons follow Unicode LDML — getBestDateTimePattern picks locale ordering. -private const val DAY_MONTH_SKELETON = "EEEMMMd" // "Mon, May 28" / "Mon 28 May" -private const val FULL_DATE_SKELETON = "EEEEMMMMdy" // "Monday, May 28, 2026" / "Monday, 28 May 2026" -private const val MONTH_YEAR_SKELETON = "MMMMy" // "May 2026" / "Mai 2026" -private const val WEEKDAY_SHORT_SKELETON = "EEE" // "Mon" — order doesn't matter - -/** - * Per-thread cached [SimpleDateFormat] keyed off the current default [Locale]. - * - * Cached because these run per cell in the calendar grid (≈42 cells per month, - * 7 per weekday header) — allocating a fresh SimpleDateFormat each call - * showed up as scroll-time GC churn. Per-thread because SimpleDateFormat - * isn't thread-safe; calendars today are read on the UI thread but future - * callers could differ. ThreadLocal gives both — no locks, no contention, - * lazy rebuild on locale change. - */ -private class LocaleAwareFormatter( - private val skeleton: String, -) { - private val cache = ThreadLocal>() - - fun get(): SimpleDateFormat { - val current = Locale.getDefault() - val cached = cache.get() - if (cached != null && cached.first == current) return cached.second - val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current) - cache.set(current to fresh) - return fresh - } -} - -private val dayMonthFormat = LocaleAwareFormatter(DAY_MONTH_SKELETON) -private val fullDateFormat = LocaleAwareFormatter(FULL_DATE_SKELETON) -private val monthYearFormat = LocaleAwareFormatter(MONTH_YEAR_SKELETON) -private val weekdayShortFormat = LocaleAwareFormatter(WEEKDAY_SHORT_SKELETON) - -// Time format respects the user's Android 12/24-hour system setting. -private fun timeFormat(context: Context): java.text.DateFormat = DateFormat.getTimeFormat(context) - -fun formatCalendarRange( - note: Note, - context: Context, -): String? { - val view = note.appointmentView() ?: return null - val start = view.startSeconds ?: return null - return if (view.isAllDay) { - formatDateRange(start, view.endSeconds) - } else { - formatTimeRange(start, view.endSeconds, context) - } -} - -private fun formatTimeRange( - start: Long, - end: Long?, - context: Context, -): String { - val dayMonth = dayMonthFormat.get() - val time = timeFormat(context) - val startMs = start * 1000 - val startStr = "${dayMonth.format(Date(startMs))} · ${time.format(Date(startMs))}" - if (end == null || end == start) return startStr - val endMs = end * 1000 - return if (isSameDay(startMs, endMs)) { - "$startStr – ${time.format(Date(endMs))}" - } else { - "$startStr – ${dayMonth.format(Date(endMs))} · ${time.format(Date(endMs))}" - } -} - -private fun formatDateRange( - start: Long, - end: Long?, -): String { - val dayMonth = dayMonthFormat.get() - val startStr = dayMonth.format(Date(start * 1000)) - if (end == null || end == start) return startStr - return "$startStr – ${dayMonth.format(Date(end * 1000))}" -} - -fun formatLongDate(unixSeconds: Long): String = fullDateFormat.get().format(Date(unixSeconds * 1000)) - -fun formatMonthYear( - year: Int, - monthZeroBased: Int, -): String { - val cal = Calendar.getInstance() - cal.clear() - cal.set(year, monthZeroBased, 1) - return monthYearFormat.get().format(cal.time) -} - -fun formatTimeOfDay( - unixSeconds: Long, - context: Context, -): String = timeFormat(context).format(Date(unixSeconds * 1000)) - -fun formatShortWeekday(weekdayZeroBased: Int): String { - val cal = Calendar.getInstance() - cal.clear() - cal.firstDayOfWeek = Calendar.SUNDAY - cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY) - cal.add(Calendar.DAY_OF_YEAR, weekdayZeroBased) - return weekdayShortFormat.get().format(cal.time) -} - -private fun isSameDay( - aMs: Long, - bMs: Long, -): Boolean { - val ca = Calendar.getInstance().apply { timeInMillis = aMs } - val cb = Calendar.getInstance().apply { timeInMillis = bMs } - return ca.get(Calendar.YEAR) == cb.get(Calendar.YEAR) && - ca.get(Calendar.DAY_OF_YEAR) == cb.get(Calendar.DAY_OF_YEAR) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt index 4b504f1c73..6e048018da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt @@ -61,6 +61,9 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_nav_next_week import com.vitorpamplona.amethyst.commons.resources.calendar_nav_previous_week import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatMonthYear +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatShortWeekday import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt index d98058b5f3..ac69a49acc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt @@ -71,9 +71,9 @@ import com.vitorpamplona.amethyst.commons.resources.new_calendar_collection import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatLongDate @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index 9915e54662..08e1939341 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -97,16 +97,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatCalendarRange -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatLongDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.shareIcs import com.vitorpamplona.quartz.nip01Core.core.Address @@ -347,7 +348,8 @@ private fun EventBody( ) } val context = LocalContext.current - formatCalendarRange(note, context)?.let { range -> + val timeFormatter = rememberTimeOfDayFormatter() + formatCalendarRange(note, timeFormatter)?.let { range -> Text( text = range, style = MaterialTheme.typography.bodyLarge, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index f28daa91bb..b3ede6e110 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -130,6 +130,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton +import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation @@ -139,7 +140,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf @@ -387,12 +387,13 @@ private fun MarkdownPostScreenBody( .fillMaxWidth() .padding(Size10dp), ) { - RenderContentAsMarkdown( + LocalRichTextPlatform.current.Markdown( content = postViewModel.message.text.toString(), tags = EmptyTagList, canPreview = true, quotesLeft = 1, backgroundColor = backgroundColor, + callbackUri = null, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 34f42ff0fa..f9f9250491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -146,7 +146,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.collections.immutable.toImmutableList @@ -584,6 +583,7 @@ private fun SearchTextField( // fields rather than search terms. The value stays the plain text, so a query can still // be copied out and pasted back. val fieldState = remember { SearchFieldState(searchBarViewModel.searchValue) } + val appServices = LocalAppServices.current // Reported up rather than read down: the scaffold that has to stop moving is composed // above this field, and it is the only thing that can keep a picker's scroll off the bars. @@ -650,7 +650,7 @@ private fun SearchTextField( groupName = { groupNames[it] }, // The same reverse-geocode cache the feed spinner and thread view already read // through LoadCityName; null until it resolves, which leaves the geohash showing. - scopeName = { field, value -> if (field == "geo") CachedReversedGeoLocations.cached(value) else null }, + scopeName = { field, value -> if (field == "geo") appServices.cachedPlaceName(value) else null }, groups = groupCandidates, onPeopleQuery = { userSuggestions.processCurrentWord(it) }, onGroupQuery = { partial -> groupQuery = partial }, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt index 542bd89360..ec851b8f1e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -70,6 +70,9 @@ interface AppServices { /** The device's position as geohashes. */ val deviceLocation: DeviceLocation get() = DeviceLocation.None + /** The place name already reverse-geocoded for [geohash], or null when none is cached yet. */ + fun cachedPlaceName(geohash: String): String? = null + /** A new on-device alt-text suggester for image uploads, or null where the platform has none. */ fun createAltTextSuggester(): AltTextSuggester? = null } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt new file mode 100644 index 0000000000..f8448b1788 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarTimeFormat.kt @@ -0,0 +1,98 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.DateSkeletonFormatter +import com.vitorpamplona.amethyst.commons.ui.note.calendarYearAndDay + +// Skeletons follow Unicode LDML — the platform picks the locale's field order. +private val dayMonthFormat = DateSkeletonFormatter("EEEMMMd") // "Mon, May 28" / "Mon 28 May" +private val fullDateFormat = DateSkeletonFormatter("EEEEMMMMdy") // "Monday, May 28, 2026" / "Monday, 28 May 2026" +private val monthYearFormat = DateSkeletonFormatter("MMMMy") // "May 2026" / "Mai 2026" +private val weekdayShortFormat = DateSkeletonFormatter("EEE") // "Mon" — order doesn't matter + +/** + * The appointment's date or time range. [formatTime] renders a time of day under the user's + * 12/24-hour setting; composables get it from `rememberTimeOfDayFormatter()`. + */ +fun formatCalendarRange( + note: Note, + formatTime: (epochMillis: Long) -> String, +): String? { + val view = note.appointmentView() ?: return null + val start = view.startSeconds ?: return null + return if (view.isAllDay) { + formatDateRange(start, view.endSeconds) + } else { + formatTimeRange(start, view.endSeconds, formatTime) + } +} + +private fun formatTimeRange( + start: Long, + end: Long?, + formatTime: (epochMillis: Long) -> String, +): String { + val startMs = start * 1000 + val startStr = "${dayMonthFormat.format(startMs)} · ${formatTime(startMs)}" + if (end == null || end == start) return startStr + val endMs = end * 1000 + return if (isSameDay(startMs, endMs)) { + "$startStr – ${formatTime(endMs)}" + } else { + "$startStr – ${dayMonthFormat.format(endMs)} · ${formatTime(endMs)}" + } +} + +private fun formatDateRange( + start: Long, + end: Long?, +): String { + val startStr = dayMonthFormat.format(start * 1000) + if (end == null || end == start) return startStr + return "$startStr – ${dayMonthFormat.format(end * 1000)}" +} + +fun formatLongDate(unixSeconds: Long): String = fullDateFormat.format(unixSeconds * 1000) + +fun formatMonthYear( + year: Int, + monthZeroBased: Int, +): String = monthYearFormat.format(LocalClock.startOfDay(SearchDate(year, monthZeroBased + 1, 1)) * 1000) + +fun formatTimeOfDay( + unixSeconds: Long, + formatTime: (epochMillis: Long) -> String, +): String = formatTime(unixSeconds * 1000) + +/** The short name of a weekday, 0 = Sunday. */ +fun formatShortWeekday(weekdayZeroBased: Int): String = + // 2026-01-04 was a Sunday; any known Sunday works. + weekdayShortFormat.format(LocalClock.startOfDay(SearchDate(2026, 1, 4 + weekdayZeroBased)) * 1000) + +private fun isSameDay( + aMs: Long, + bMs: Long, +): Boolean = calendarYearAndDay(aMs) == calendarYearAndDay(bMs) From 9e4ca4a9c80f1ed449c8511964841e6d0edbb32b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:22:44 +0000 Subject: [PATCH 18/45] refactor: move the search, long-form composer and calendar-collection screens 13 files move to commonsUI: SearchScreen with its bar view model and relay dialogs, LongFormPostScreen with its view model, and NewCalendarCollectionScreen. SearchScreen opens its history through a new AppPreferenceStores.searchHistoryStorage helper, so commonsUI never names the DataStore type. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/navigation/AppNavigation.kt | 6 +++--- .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 1 + .../loggedIn/notifications/NotificationScreen.kt | 1 + .../commons/search/DataStoreSearchHistoryStorage.kt | 4 ++++ .../TextSearchDataSourceSubscription.kt | 4 ++-- .../calendars/create/NewCalendarCollectionScreen.kt | 6 +++--- .../create/NewCalendarCollectionViewModel.kt | 3 ++- .../discover/nip23LongForm/LongFormPostScreen.kt | 8 ++++---- .../discover/nip23LongForm/LongFormPostViewModel.kt | 3 ++- .../ui/screen/loggedIn/home/AddOutboxRelayCard.kt | 4 ++-- .../loggedIn/home/AddOutboxRelayListDialog.kt | 6 +++--- .../loggedIn/notifications/AddInboxRelayCard.kt | 2 +- .../notifications/AddInboxRelayListDialog.kt | 6 +++--- .../loggedIn/search/AddInboxRelayForSearchCard.kt | 2 +- .../loggedIn/search/AddSearchRelayListDialog.kt | 6 +++--- .../ui/screen/loggedIn/search/SearchBarViewModel.kt | 10 ++++++++-- .../ui/screen/loggedIn/search/SearchScreen.kt | 13 +++++++------ 17 files changed, 50 insertions(+), 35 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/relayClient/searchCommand/TextSearchDataSourceSubscription.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/AddOutboxRelayCard.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/search/SearchBarViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/search/SearchScreen.kt (99%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 6016c74acf..cbee4def18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen @@ -133,6 +134,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.Co import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.contactList.ContactListUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.drafts.DraftListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen @@ -199,6 +201,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip43.RelayM import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.subscriptions.ActiveSubscriptionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.VanishEventsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen @@ -276,7 +279,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen @@ -291,7 +293,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader @@ -325,7 +326,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPayment import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen 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 2e787f757f..7a412dcaad 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 @@ -141,6 +141,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRe import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 784402841a..c50272a55c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DataStoreSearchHistoryStorage.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DataStoreSearchHistoryStorage.kt index 80f9de7353..0039f1160a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DataStoreSearchHistoryStorage.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DataStoreSearchHistoryStorage.kt @@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import kotlinx.coroutines.flow.first /** @@ -54,3 +55,6 @@ class DataStoreSearchHistoryStorage( const val FILE_NAME = "search_history" } } + +/** The search history kept in this app's own preference file. */ +fun AppPreferenceStores.searchHistoryStorage(): DataStoreSearchHistoryStorage = DataStoreSearchHistoryStorage(getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/searchCommand/TextSearchDataSourceSubscription.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/searchCommand/TextSearchDataSourceSubscription.kt index bd1d0545e7..3d7586b0c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/TextSearchDataSourceSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/searchCommand/TextSearchDataSourceSubscription.kt @@ -18,13 +18,13 @@ * 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.service.relayClient.searchCommand +package com.vitorpamplona.amethyst.commons.relayClient.searchCommand import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.relayClient.searchCommand.SearchFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search.SearchBarViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel @Composable fun TextSearchDataSourceSubscription( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt index ac69a49acc..046e62b0be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionScreen.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.calendars.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -53,7 +53,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_all_day import com.vitorpamplona.amethyst.commons.resources.calendar_collection_delete @@ -68,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_untitled import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.edit_calendar_collection import com.vitorpamplona.amethyst.commons.resources.new_calendar_collection +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar @@ -82,7 +82,7 @@ fun NewCalendarCollectionScreen( accountViewModel: AccountViewModel, editDTag: String? = null, ) { - val vm: NewCalendarCollectionViewModel = viewModel() + val vm: NewCalendarCollectionViewModel = rememberViewModel { NewCalendarCollectionViewModel() } vm.init(accountViewModel, editDTag) Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt index 8cf614ea2b..f25719d6d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.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.calendars.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateListOf @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index b3ede6e110..5f2b9b727a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.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.discover.nip23LongForm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border @@ -75,7 +75,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.EmptyTagList @@ -110,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRe import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation @@ -140,7 +141,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf @@ -152,7 +152,7 @@ fun LongFormPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: LongFormPostViewModel = viewModel() + val postViewModel: LongFormPostViewModel = rememberViewModel { LongFormPostViewModel() } postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 48c4e79d3f..59ab9d30e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.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.discover.nip23LongForm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -115,6 +115,7 @@ import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayCard.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayCard.kt index acc89b1249..a11f3f2a18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayCard.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found_description import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.AddInboxRelayCard import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BigPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding @@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.AddInboxRelayCard @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt index 942fc7dd47..5e533cca8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AddOutboxRelayListDialog.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,12 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.outbox_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65OutboxRelayList @@ -57,7 +57,7 @@ fun AddOutboxRelayListDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: Nip65RelayListViewModel = viewModel() + val postViewModel: Nip65RelayListViewModel = rememberViewModel { Nip65RelayListViewModel() } postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt index 55fc8e2b05..bce955e4c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayCard.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.notifications +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt index 807c74558d..9bce19575e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.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.notifications +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,12 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.inbox_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.inbox_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.inbox_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip65.Nip65InboxRelayList @@ -57,7 +57,7 @@ fun AddInboxRelayListDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: Nip65RelayListViewModel = viewModel() + val postViewModel: Nip65RelayListViewModel = rememberViewModel { Nip65RelayListViewModel() } postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt index 5adaf1521c..29ef722364 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.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.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index 6ab9ffd93e..49385ba613 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/AddSearchRelayListDialog.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.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -36,7 +36,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.default_relays_longer @@ -44,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.search_relays_not_found_edit import com.vitorpamplona.amethyst.commons.resources.search_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.search_relays_title import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @@ -62,7 +62,7 @@ fun AddSearchRelayListDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: SearchRelayListViewModel = viewModel() + val postViewModel: SearchRelayListViewModel = rememberViewModel { SearchRelayListViewModel() } postViewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt index 57280cf21e..9d6b69413e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.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.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Stable @@ -30,6 +30,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.actions.ConcordActions import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent import com.vitorpamplona.amethyst.commons.model.Account @@ -77,6 +78,7 @@ import com.vitorpamplona.quartz.utils.startsWithAny import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -97,6 +99,7 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlin.reflect.KClass @Stable @OptIn(FlowPreview::class) @@ -688,6 +691,9 @@ class SearchBarViewModel( val initialQuery: String? = null, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = SearchBarViewModel(account, nip05, historyStorage, initialQuery) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = SearchBarViewModel(account, nip05, historyStorage, initialQuery) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchScreen.kt index f9f9250491..fd63b492d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchScreen.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.search +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -78,13 +78,13 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo +import com.vitorpamplona.amethyst.commons.relayClient.searchCommand.TextSearchDataSourceSubscription import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.ephemeral_relay_chat import com.vitorpamplona.amethyst.commons.resources.npub_hex_username @@ -111,12 +111,13 @@ import com.vitorpamplona.amethyst.commons.resources.search_source_relays import com.vitorpamplona.amethyst.commons.resources.search_type_to_begin import com.vitorpamplona.amethyst.commons.resources.search_type_to_begin_explainer import com.vitorpamplona.amethyst.commons.resources.search_waiting_on_relays -import com.vitorpamplona.amethyst.commons.search.DataStoreSearchHistoryStorage import com.vitorpamplona.amethyst.commons.search.QuerySerializer import com.vitorpamplona.amethyst.commons.search.SearchScope import com.vitorpamplona.amethyst.commons.search.SearchSortOrder import com.vitorpamplona.amethyst.commons.search.SearchSource +import com.vitorpamplona.amethyst.commons.search.searchHistoryStorage import com.vitorpamplona.amethyst.commons.ui.components.namecoin.NamecoinResolutionRow +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding @@ -146,11 +147,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -168,9 +169,9 @@ fun SearchScreen( nav: INav, ) { val appStores = LocalAppServices.current.appStores - val historyStorage = remember { DataStoreSearchHistoryStorage(appStores.getDataStore(DataStoreSearchHistoryStorage.FILE_NAME)) } + val historyStorage = remember { appStores.searchHistoryStorage() } val searchBarViewModel: SearchBarViewModel = - viewModel( + rememberViewModel( // Keyed on the seed: navigating from one screen's search button to another's has to // build a model holding that screen's filter, not hand back the previous one. key = "SearchBarViewModel${initialQuery?.let { " $it" }.orEmpty()}", From b8ad9c72f3696565e9c4ac58fb10b786c3daed2c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:31:21 +0000 Subject: [PATCH 19/45] refactor: shared device-credential prompt for key access rememberDeviceAuthenticator (commonsUI) replaces the app's authenticate() helper: Android keeps BiometricPrompt with the keyguard fallback in its actual; desktop and iOS approve at once for now, as Android does on a device without a screen lock. UpdateZapAmountDialog no longer carries the prompt code, and AccountBackupScreen asks through the port. commonsUI's Android source set takes androidx.biometric (Apache-2.0, already shipped). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 130 ----------- .../loggedIn/keyBackup/AccountBackupScreen.kt | 37 +--- commonsUI/build.gradle.kts | 2 + .../DeviceAuthenticator.android.kt | 207 ++++++++++++++++++ .../ui/privacylock/DeviceAuthenticator.kt | 44 ++++ .../ui/privacylock/DeviceAuthenticator.ios.kt | 29 +++ .../ui/privacylock/DeviceAuthenticator.jvm.kt | 29 +++ 7 files changed, 315 insertions(+), 163 deletions(-) create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.android.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.jvm.kt 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 d2f83d0952..397193c7a7 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 @@ -20,14 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note -import android.app.KeyguardManager -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.result.ActivityResult -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -59,7 +51,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateMapOf @@ -86,10 +77,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add -import com.vitorpamplona.amethyst.commons.resources.app_name -import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed -import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer -import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer_with_error import com.vitorpamplona.amethyst.commons.resources.new_amount_in_sats import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts_explainer @@ -121,10 +108,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @OptIn(ExperimentalLayoutApi::class) @Composable @@ -449,118 +434,3 @@ private fun ZapAmountPresetChip( } } } - -/** - * Labels the device-credential and biometric prompts need. Resolved in - * composition and passed down because [authenticate] runs from an onClick - * lambda and from Android's own biometric callbacks, neither of which can - * reach a Compose resource. - */ -@Immutable -data class AuthPromptLabels( - val appName: String, - val failedTitle: String, - val failedExplainer: String, - val failedExplainerWithError: String, -) - -@Composable -fun rememberAuthPromptLabels(): AuthPromptLabels = - AuthPromptLabels( - appName = stringRes(Res.string.app_name), - failedTitle = stringRes(Res.string.biometric_authentication_failed), - failedExplainer = stringRes(Res.string.biometric_authentication_failed_explainer), - failedExplainerWithError = stringRes(Res.string.biometric_authentication_failed_explainer_with_error), - ) - -fun authenticate( - title: String, - context: Context, - labels: AuthPromptLabels, - keyguardLauncher: ManagedActivityResultLauncher, - onApproved: () -> Unit, - onError: (String, String) -> Unit, -) { - val fragmentContext = context.getFragmentActivity()!! - val keyguardManager = - fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - - if (!keyguardManager.isDeviceSecure) { - onApproved() - return - } - - @Suppress("DEPRECATION") - fun keyguardPrompt() { - val intent = - keyguardManager.createConfirmDeviceCredentialIntent( - labels.appName, - title, - ) - - keyguardLauncher.launch(intent) - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - keyguardPrompt() - return - } - - val biometricManager = BiometricManager.from(context) - val authenticators = - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL - - val promptInfo = - BiometricPrompt.PromptInfo - .Builder() - .setTitle(labels.appName) - .setSubtitle(title) - .setAllowedAuthenticators(authenticators) - .build() - - val biometricPrompt = - BiometricPrompt( - fragmentContext, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence, - ) { - super.onAuthenticationError(errorCode, errString) - - when (errorCode) { - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> { - keyguardPrompt() - } - - BiometricPrompt.ERROR_LOCKOUT -> { - keyguardPrompt() - } - - else -> { - onError( - labels.failedTitle, - labels.failedExplainerWithError.format(errString.toString()), - ) - } - } - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onError(labels.failedTitle, labels.failedExplainer) - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - onApproved() - } - }, - ) - - when (biometricManager.canAuthenticate(authenticators)) { - BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo) - else -> keyguardPrompt() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt index cee705213a..f8652808a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt @@ -20,16 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup -import android.app.Activity import android.content.ClipData import android.content.Context import android.content.ContextWrapper import android.os.PersistableBundle import android.view.WindowManager import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -136,14 +132,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.privacylock.rememberDeviceAuthenticator import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.note.authenticate -import com.vitorpamplona.amethyst.ui.note.rememberAuthPromptLabels import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import kotlinx.coroutines.CoroutineScope @@ -701,13 +696,6 @@ private class KeyAccessGate { var isUnlocked by mutableStateOf(false) private set - /** - * The action waiting on the keyguard fallback activity to return. Only a new request or - * that activity's result replaces it: the activity stops this screen (so ON_STOP must not - * clear it), and a wrong fingerprint before the lockout fallback is not a final failure. - */ - var pending: (() -> Unit)? = null - var prompt: ((onApproved: () -> Unit) -> Unit)? = null fun withAccess(action: () -> Unit) { @@ -728,32 +716,15 @@ private class KeyAccessGate { @Composable private fun rememberKeyAccessGate(accountViewModel: AccountViewModel): KeyAccessGate { - val context = LocalContext.current - val authLabels = rememberAuthPromptLabels() + val authenticator = rememberDeviceAuthenticator() val authTitle = stringRes(Res.string.backup_keys) val gate = remember { KeyAccessGate() } - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - val action = gate.pending - gate.pending = null - if (result.resultCode == Activity.RESULT_OK) { - action?.invoke() - } - } - SideEffect { gate.prompt = { onApproved -> - gate.pending = onApproved - authenticate( + authenticator.authenticate( title = authTitle, - context = context, - labels = authLabels, - keyguardLauncher = keyguardLauncher, - onApproved = { - gate.pending = null - onApproved() - }, + onApproved = onApproved, onError = { title, message -> accountViewModel.toastManager.toast(title, message) }, ) } diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 1551fcaeff..d592bac103 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -216,6 +216,8 @@ kotlin { implementation(libs.androidx.activity.compose) // Camera permission prompt for TakePicture. Apache-2.0, already in the app. implementation(libs.accompanist.permissions) + // BiometricPrompt behind rememberDeviceAuthenticator. Apache-2.0, already in the app. + implementation(libs.androidx.biometric.ktx) } } diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.android.kt new file mode 100644 index 0000000000..d4ef5180a1 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.android.kt @@ -0,0 +1,207 @@ +/* + * 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.commons.ui.privacylock + +import android.app.Activity +import android.app.KeyguardManager +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.os.Build +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.fragment.app.FragmentActivity +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.app_name +import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed +import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer +import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer_with_error +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** The action waiting on the keyguard activity, which only its result may run or replace. */ +private class PendingApproval { + var action: (() -> Unit)? = null +} + +@Composable +actual fun rememberDeviceAuthenticator(): DeviceAuthenticator { + val context = LocalContext.current + val labels = rememberAuthPromptLabels() + val pending = remember { PendingApproval() } + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + val action = pending.action + pending.action = null + if (result.resultCode == Activity.RESULT_OK) { + action?.invoke() + } + } + + return remember(context, labels, keyguardLauncher) { + DeviceAuthenticator { title, onApproved, onError -> + pending.action = onApproved + authenticate( + title = title, + context = context, + labels = labels, + keyguardLauncher = keyguardLauncher, + onApproved = { + pending.action = null + onApproved() + }, + onError = onError, + ) + } + } +} + +private fun Context.findFragmentActivity(): FragmentActivity? { + var current: Context = this + while (current is ContextWrapper) { + if (current is FragmentActivity) return current + current = current.baseContext + } + return null +} + +/** + * Labels the device-credential and biometric prompts need. Resolved in + * composition and passed down because [authenticate] runs from an onClick + * lambda and from Android's own biometric callbacks, neither of which can + * reach a Compose resource. + */ +@Immutable +private data class AuthPromptLabels( + val appName: String, + val failedTitle: String, + val failedExplainer: String, + val failedExplainerWithError: String, +) + +@Composable +private fun rememberAuthPromptLabels(): AuthPromptLabels = + AuthPromptLabels( + appName = stringRes(Res.string.app_name), + failedTitle = stringRes(Res.string.biometric_authentication_failed), + failedExplainer = stringRes(Res.string.biometric_authentication_failed_explainer), + failedExplainerWithError = stringRes(Res.string.biometric_authentication_failed_explainer_with_error), + ) + +private fun authenticate( + title: String, + context: Context, + labels: AuthPromptLabels, + keyguardLauncher: ManagedActivityResultLauncher, + onApproved: () -> Unit, + onError: (String, String) -> Unit, +) { + val fragmentContext = context.findFragmentActivity() ?: return onApproved() + val keyguardManager = + fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + + if (!keyguardManager.isDeviceSecure) { + onApproved() + return + } + + @Suppress("DEPRECATION") + fun keyguardPrompt() { + val intent = + keyguardManager.createConfirmDeviceCredentialIntent( + labels.appName, + title, + ) + + keyguardLauncher.launch(intent) + } + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + keyguardPrompt() + return + } + + val biometricManager = BiometricManager.from(context) + val authenticators = + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + + val promptInfo = + BiometricPrompt.PromptInfo + .Builder() + .setTitle(labels.appName) + .setSubtitle(title) + .setAllowedAuthenticators(authenticators) + .build() + + val biometricPrompt = + BiometricPrompt( + fragmentContext, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence, + ) { + super.onAuthenticationError(errorCode, errString) + + when (errorCode) { + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> { + keyguardPrompt() + } + + BiometricPrompt.ERROR_LOCKOUT -> { + keyguardPrompt() + } + + else -> { + onError( + labels.failedTitle, + labels.failedExplainerWithError.format(errString.toString()), + ) + } + } + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onError(labels.failedTitle, labels.failedExplainer) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + onApproved() + } + }, + ) + + when (biometricManager.canAuthenticate(authenticators)) { + BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo) + else -> keyguardPrompt() + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.kt new file mode 100644 index 0000000000..c06fee8655 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.kt @@ -0,0 +1,44 @@ +/* + * 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.commons.ui.privacylock + +import androidx.compose.runtime.Composable + +/** Asks the user to prove it is them (biometrics or the device PIN) before a sensitive action. */ +fun interface DeviceAuthenticator { + /** + * Prompts with [title] and runs [onApproved] once the user passes. A device without a screen + * lock approves at once. [onError] gets a title and a message for a failed attempt. + */ + fun authenticate( + title: String, + onApproved: () -> Unit, + onError: (title: String, message: String) -> Unit, + ) +} + +/** + * The platform's device-credential prompt. Android shows BiometricPrompt (strong biometrics or + * the device credential) and falls back to the keyguard screen; desktop and iOS have no prompt + * wired yet and approve at once, as Android does on a device without a screen lock. + */ +@Composable +expect fun rememberDeviceAuthenticator(): DeviceAuthenticator diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.ios.kt new file mode 100644 index 0000000000..ab75c29bcc --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.ios.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.privacylock + +import androidx.compose.runtime.Composable + +private val ApproveAtOnce = DeviceAuthenticator { _, onApproved, _ -> onApproved() } + +/** No device-credential prompt wired on this platform yet; see the common declaration. */ +@Composable +actual fun rememberDeviceAuthenticator(): DeviceAuthenticator = ApproveAtOnce diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.jvm.kt new file mode 100644 index 0000000000..ab75c29bcc --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/privacylock/DeviceAuthenticator.jvm.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.privacylock + +import androidx.compose.runtime.Composable + +private val ApproveAtOnce = DeviceAuthenticator { _, onApproved, _ -> onApproved() } + +/** No device-credential prompt wired on this platform yet; see the common declaration. */ +@Composable +actual fun rememberDeviceAuthenticator(): DeviceAuthenticator = ApproveAtOnce From 0e51725327b92bdbd4fc3e24971901fe628e1b56 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:33:38 +0000 Subject: [PATCH 20/45] refactor: split the zap-rail preview helpers out of ReactionsRow ZapRail, the preferred-rail thresholds, previewRailsFor and ZapRailIcon move to their own file; they only need Compose, so the zap-amount settings stop depending on the reactions row. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/note/ReactionsRow.kt | 108 ------------- .../vitorpamplona/amethyst/ui/note/ZapRail.kt | 142 ++++++++++++++++++ 2 files changed, 142 insertions(+), 108 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index c3e68d1927..f8adc03180 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -90,7 +90,6 @@ import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboard @@ -112,11 +111,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.commons.audio.FloatingRecordingIndicator import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.hashtags.Cashu -import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ReactionRowAction import com.vitorpamplona.amethyst.commons.model.ReactionRowItem @@ -153,14 +149,12 @@ import com.vitorpamplona.amethyst.commons.resources.no_payment_app_found_for_typ import com.vitorpamplona.amethyst.commons.resources.no_reaction_type_setup_long_press_to_change import com.vitorpamplona.amethyst.commons.resources.no_reactions_setup import com.vitorpamplona.amethyst.commons.resources.no_wallet_found -import com.vitorpamplona.amethyst.commons.resources.nutzap import com.vitorpamplona.amethyst.commons.resources.open_all_reactions_to_this_post import com.vitorpamplona.amethyst.commons.resources.payment_targets import com.vitorpamplona.amethyst.commons.resources.payto_amount_set_in_app import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts import com.vitorpamplona.amethyst.commons.resources.quote import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.reload_mint_title import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox @@ -196,7 +190,6 @@ import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.HalfDoubleVertSpacer @@ -262,7 +255,6 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -import androidx.compose.material3.Icon as Material3Icon @Composable fun ReactionsRow( @@ -2585,106 +2577,6 @@ private fun UnifiedZapAmountChip( } } -/** Payment rails a zap-amount chip can offer, in canonical display order. */ -internal enum class ZapRail { CASHU, RELOAD, LIGHTNING, ONCHAIN } - -/** Below this, the default rail is cashu (Lightning min/fees make tiny zaps awkward). */ -internal const val CASHU_PREFERRED_BELOW_SATS = 10L - -/** Above this, the default rail is an on-chain transaction. */ -internal const val ONCHAIN_PREFERRED_ABOVE_SATS = 10_000L - -/** - * Default rail for a preset amount with no recipient context (the settings - * preview): cashu under [CASHU_PREFERRED_BELOW_SATS], on-chain over - * [ONCHAIN_PREFERRED_ABOVE_SATS] (and at/above the on-chain minimum), else - * Lightning. Mirrors the live chip's tiering so the preview matches the feed. - */ -internal fun previewPreferredRail(amountInSats: Long): ZapRail = - when { - amountInSats < CASHU_PREFERRED_BELOW_SATS -> ZapRail.CASHU - amountInSats > ONCHAIN_PREFERRED_ABOVE_SATS && amountInSats >= MIN_ONCHAIN_ZAP_SATS -> ZapRail.ONCHAIN - else -> ZapRail.LIGHTNING - } - -/** Rails a preset of [amountInSats] could use, default first then the rest. */ -internal fun previewRailsFor(amountInSats: Long): List { - val preferred = previewPreferredRail(amountInSats) - val all = - buildList { - add(ZapRail.CASHU) - add(ZapRail.LIGHTNING) - if (amountInSats >= MIN_ONCHAIN_ZAP_SATS) add(ZapRail.ONCHAIN) - } - return listOf(preferred) + all.filter { it != preferred } -} - -/** - * Just the rail's logo (no click target), drawn at [size]. [colored] renders it - * in its accent colour (the preferred rail); otherwise it's flattened to the - * on-surface monochrome tint. The cashu mark is drawn ~0.86× the symbol size — - * the new monochrome outline carries less weight than the old multi-tone mark, - * so it needs less shrinking to read at a matching optical size. - */ -@Composable -internal fun ZapRailIcon( - rail: ZapRail, - colored: Boolean, - size: Dp = 18.dp, -) { - val mono = MaterialTheme.colorScheme.onSurface - val cashuSize = size * 0.86f - when (rail) { - ZapRail.CASHU -> { - Material3Icon( - imageVector = CustomHashTagIcons.Cashu, - contentDescription = stringRes(Res.string.nutzap), - modifier = Modifier.size(cashuSize), - // The cashu mark is a monochrome, tintable outline — colour it the - // same brand orange as the other rails when selected, else mono. - tint = if (colored) BitcoinOrange else mono, - ) - } - - ZapRail.RELOAD -> { - // Funds exist but in the wrong mint — a dimmed cashu logo with a - // small "+" badge; tapping it opens the top-up screen. - Box(contentAlignment = Alignment.BottomEnd) { - Material3Icon( - imageVector = CustomHashTagIcons.Cashu, - contentDescription = stringRes(Res.string.reload_mint_title), - modifier = Modifier.size(cashuSize).alpha(0.5f), - tint = if (colored) BitcoinOrange else mono, - ) - Icon( - symbol = MaterialSymbols.AddCircle, - contentDescription = null, - modifier = Modifier.size(size * 0.5f), - tint = if (colored) BitcoinOrange else mono, - ) - } - } - - ZapRail.LIGHTNING -> { - Icon( - symbol = MaterialSymbols.Bolt, - contentDescription = null, - modifier = Modifier.size(size), - tint = if (colored) BitcoinOrange else mono, - ) - } - - ZapRail.ONCHAIN -> { - Icon( - symbol = MaterialSymbols.CurrencyBitcoin, - contentDescription = null, - modifier = Modifier.size(size), - tint = if (colored) BitcoinOrange else mono, - ) - } - } -} - /** * Exercises the chip across rail combinations and amount bands so the default * (coloured, left of the amount) and the alternative circular buttons can be diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt new file mode 100644 index 0000000000..77cbaebee2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt @@ -0,0 +1,142 @@ +/* + * 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.note + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.hashtags.Cashu +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.MIN_ONCHAIN_ZAP_SATS +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.nutzap +import com.vitorpamplona.amethyst.commons.resources.reload_mint_title +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange +import androidx.compose.material3.Icon as Material3Icon + +/** Payment rails a zap-amount chip can offer, in canonical display order. */ +enum class ZapRail { CASHU, RELOAD, LIGHTNING, ONCHAIN } + +/** Below this, the default rail is cashu (Lightning min/fees make tiny zaps awkward). */ +const val CASHU_PREFERRED_BELOW_SATS = 10L + +/** Above this, the default rail is an on-chain transaction. */ +const val ONCHAIN_PREFERRED_ABOVE_SATS = 10_000L + +/** + * Default rail for a preset amount with no recipient context (the settings + * preview): cashu under [CASHU_PREFERRED_BELOW_SATS], on-chain over + * [ONCHAIN_PREFERRED_ABOVE_SATS] (and at/above the on-chain minimum), else + * Lightning. Mirrors the live chip's tiering so the preview matches the feed. + */ +fun previewPreferredRail(amountInSats: Long): ZapRail = + when { + amountInSats < CASHU_PREFERRED_BELOW_SATS -> ZapRail.CASHU + amountInSats > ONCHAIN_PREFERRED_ABOVE_SATS && amountInSats >= MIN_ONCHAIN_ZAP_SATS -> ZapRail.ONCHAIN + else -> ZapRail.LIGHTNING + } + +/** Rails a preset of [amountInSats] could use, default first then the rest. */ +fun previewRailsFor(amountInSats: Long): List { + val preferred = previewPreferredRail(amountInSats) + val all = + buildList { + add(ZapRail.CASHU) + add(ZapRail.LIGHTNING) + if (amountInSats >= MIN_ONCHAIN_ZAP_SATS) add(ZapRail.ONCHAIN) + } + return listOf(preferred) + all.filter { it != preferred } +} + +/** + * Just the rail's logo (no click target), drawn at [size]. [colored] renders it + * in its accent colour (the preferred rail); otherwise it's flattened to the + * on-surface monochrome tint. The cashu mark is drawn ~0.86× the symbol size — + * the new monochrome outline carries less weight than the old multi-tone mark, + * so it needs less shrinking to read at a matching optical size. + */ +@Composable +fun ZapRailIcon( + rail: ZapRail, + colored: Boolean, + size: Dp = 18.dp, +) { + val mono = MaterialTheme.colorScheme.onSurface + val cashuSize = size * 0.86f + when (rail) { + ZapRail.CASHU -> { + Material3Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = stringRes(Res.string.nutzap), + modifier = Modifier.size(cashuSize), + // The cashu mark is a monochrome, tintable outline — colour it the + // same brand orange as the other rails when selected, else mono. + tint = if (colored) BitcoinOrange else mono, + ) + } + + ZapRail.RELOAD -> { + // Funds exist but in the wrong mint — a dimmed cashu logo with a + // small "+" badge; tapping it opens the top-up screen. + Box(contentAlignment = Alignment.BottomEnd) { + Material3Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = stringRes(Res.string.reload_mint_title), + modifier = Modifier.size(cashuSize).alpha(0.5f), + tint = if (colored) BitcoinOrange else mono, + ) + Icon( + symbol = MaterialSymbols.AddCircle, + contentDescription = null, + modifier = Modifier.size(size * 0.5f), + tint = if (colored) BitcoinOrange else mono, + ) + } + } + + ZapRail.LIGHTNING -> { + Icon( + symbol = MaterialSymbols.Bolt, + contentDescription = null, + modifier = Modifier.size(size), + tint = if (colored) BitcoinOrange else mono, + ) + } + + ZapRail.ONCHAIN -> { + Icon( + symbol = MaterialSymbols.CurrencyBitcoin, + contentDescription = null, + modifier = Modifier.size(size), + tint = if (colored) BitcoinOrange else mono, + ) + } + } +} From 32edf278a6fb3fbf340957044d4a5989ae1fa028 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:58:02 +0000 Subject: [PATCH 21/45] refactor: move the NWC setup and zap-amount settings screens NIP47SetupScreen and UpdateZapAmountScreen move to commonsUI with the zap-amount dialog, its view model and the zap-rail helpers. The preset chip formats its amount from the KMP BigDecimal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/navigation/AppNavigation.kt | 4 ++-- .../vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 4 ++++ .../commons}/ui/note/UpdateZapAmountDialog.kt | 9 +++++---- .../commons}/ui/note/UpdateZapAmountViewModel.kt | 2 +- .../amethyst/commons}/ui/note/ZapRail.kt | 2 +- .../screen/loggedIn/settings/NIP47SetupScreen.kt | 14 +++++++------- .../loggedIn/settings/UpdateZapAmountScreen.kt | 10 +++++----- 7 files changed, 25 insertions(+), 20 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/UpdateZapAmountDialog.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/UpdateZapAmountViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/ZapRail.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/settings/NIP47SetupScreen.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt (88%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index cbee4def18..420db239df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -212,10 +212,12 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWord import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MessagesSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MutedThreadsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ProfileUiSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SpammingUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UpdateZapAmountScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.VideoPlayerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnCoordinatorsScreen @@ -327,13 +329,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ResourceUsageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index f8adc03180..bba98ca0a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -164,6 +164,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBas import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeReplyTo +import com.vitorpamplona.amethyst.commons.ui.note.CASHU_PREFERRED_BELOW_SATS import com.vitorpamplona.amethyst.commons.ui.note.ChangeReactionIcon import com.vitorpamplona.amethyst.commons.ui.note.CommentIcon import com.vitorpamplona.amethyst.commons.ui.note.ExpandLessIcon @@ -171,6 +172,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ExpandMoreIcon import com.vitorpamplona.amethyst.commons.ui.note.LikeIcon import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.note.ONCHAIN_PREFERRED_ABOVE_SATS import com.vitorpamplona.amethyst.commons.ui.note.ObserveZapAmountText import com.vitorpamplona.amethyst.commons.ui.note.OutlinedZapIcon import com.vitorpamplona.amethyst.commons.ui.note.RenderBoostGallery @@ -183,6 +185,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.RepostedIcon import com.vitorpamplona.amethyst.commons.ui.note.ShareIcon import com.vitorpamplona.amethyst.commons.ui.note.VoiceReplyIcon import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon +import com.vitorpamplona.amethyst.commons.ui.note.ZapRail +import com.vitorpamplona.amethyst.commons.ui.note.ZapRailIcon import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomSheet import com.vitorpamplona.amethyst.commons.ui.note.types.EditState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountDialog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountDialog.kt index 397193c7a7..cd525449a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountDialog.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.note +package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring @@ -72,7 +72,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -97,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer import com.vitorpamplona.amethyst.commons.resources.zap_type_section_explainer import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder @@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.utils.BigDecimal import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalLayoutApi::class) @@ -116,7 +117,7 @@ import kotlinx.collections.immutable.toImmutableList @Preview(device = "spec:width=1900px,height=2340px,dpi=440") fun UpdateZapAmountContentPreview() { val accountViewModel = mockAccountViewModel() - val vm: UpdateZapAmountViewModel = viewModel() + val vm: UpdateZapAmountViewModel = rememberViewModel { UpdateZapAmountViewModel() } vm.init(accountViewModel) ThemeComparisonRow { @@ -413,7 +414,7 @@ private fun ZapAmountPresetChip( ZapRailIcon(rails.first(), colored = true) Spacer(Modifier.width(5.dp)) Text( - text = showAmount(amountInSats.toBigDecimal().setScale(1)), + text = showAmount(BigDecimal(amountInSats)), color = MaterialTheme.colorScheme.onPrimaryContainer, fontWeight = FontWeight.SemiBold, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountViewModel.kt index d55e9e0ba5..1edf8e604d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/UpdateZapAmountViewModel.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.note +package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapRail.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapRail.kt index 77cbaebee2..21398ce8c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapRail.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/ZapRail.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.note +package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NIP47SetupScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index e960d9d1e4..b1bb92d8d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NIP47SetupScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -40,7 +40,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -52,8 +51,11 @@ import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_expl import com.vitorpamplona.amethyst.commons.resources.wallet_connect import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetAddField import com.vitorpamplona.amethyst.commons.ui.actions.paymentTargets.PaymentTargetsViewModel +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.note.UpdateZapAmountContent +import com.vitorpamplona.amethyst.commons.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory @@ -62,8 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifi import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent -import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable @@ -72,13 +72,13 @@ fun NIP47SetupScreen( nav: INav, nip47: String?, ) { - val postViewModel: UpdateZapAmountViewModel = viewModel() + val postViewModel: UpdateZapAmountViewModel = rememberViewModel { UpdateZapAmountViewModel() } postViewModel.init(accountViewModel) - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } walletViewModel.init(accountViewModel) - val paymentTargetsViewModel: PaymentTargetsViewModel = viewModel() + val paymentTargetsViewModel: PaymentTargetsViewModel = rememberViewModel { PaymentTargetsViewModel() } paymentTargetsViewModel.init(accountViewModel) LaunchedEffect(accountViewModel, postViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt index 058b3f1d82..90baf9b27c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UpdateZapAmountScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/UpdateZapAmountScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -27,14 +27,14 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.zaps +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.note.UpdateZapAmountContent +import com.vitorpamplona.amethyst.commons.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent -import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel @Composable fun UpdateZapAmountScreen( @@ -42,7 +42,7 @@ fun UpdateZapAmountScreen( nav: INav, nip47: String? = null, ) { - val postViewModel: UpdateZapAmountViewModel = viewModel() + val postViewModel: UpdateZapAmountViewModel = rememberViewModel { UpdateZapAmountViewModel() } postViewModel.init(accountViewModel) LaunchedEffect(accountViewModel, postViewModel) { From 9d9f2dcfae467ccbace71a942646427b3a38e0ff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 04:58:31 +0000 Subject: [PATCH 22/45] docs: record step 6 wave 9 in the one-UI plan Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../2026-09-27-one-ui-android-desktop.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index 6c28cd910a..bdbbf41bfc 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -461,11 +461,24 @@ assemblers, `EventSync`, …). Packages are renamed on the way: geohash, DM, Marmot, Concord, Minichat and relay-group chats; the media feeds; the relay settings; the metadata editors; podcasts; Bolt12 offers; the vanish and language settings. 180 of 252 screens shared. - - **Next:** 72 screens remain. `Amethyst.instance` reads block 16 of them, and - `LocalPreferences` another 5. Smaller exits: `UpdateZapAmountDialog` (biometrics), - `NestActivity` (PiP), `CalendarTimeFormat`, `GalleryThumb` (media3), - `AccountSessionManager`, the Blossom health probe, `ReactionsRow`, `NappletLauncher` - and the call permissions. + - **Wave 9 (2026-09-30):** `AppServices` gained `appStores`, `torBootstrapped` and + `cachedPlaceName`; the host gained the backed-up-keys flag. `rememberDeviceAuthenticator` + wraps BiometricPrompt with the keyguard fallback. `CalendarTimeFormat` is shared on + `DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection + editor, NWC setup and the zap-amount settings. 185 of 252 screens shared. + - **Next:** 67 screens remain, and what blocks them is the platform itself: + - `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents). + - The Nests activity and PiP. + - The call screens. + - The WebView browser and napplet launcher. + - The osmdroid geocache map. + - Health Connect workouts. + - The Cordn backup/migrate file flows. + - Share-as-image (bitmaps). + - `AccountSessionManager` (login and sign-up). + - The Blossom health probe. + - `AppSettingsScreen` (`BuildConfig`). + Each needs a slot or a port of its own, so step 7 can start alongside them. 7. **Navigation**: the library swap, then `AppNavigation` + rail + drawer + bottom bar. 8. **The app root port** and the new JVM shim. Then the Desktop feature inventory, and retiring the old `desktopApp`. From 5b9bd82a14ff94444ceb190d363d3f5fdab5b9f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 12:20:32 +0000 Subject: [PATCH 23/45] refactor: voice recorder slots, shared poll/live helpers and the Marmot icon picker - RecordVoiceButton and VoiceMessagePreview become NotePlatform slots (the Android recorder and MediaPlayer preview stay in the app); MAX_VOICE_RECORD_SECONDS moves beside them. ShortNotePostScreen reads saved accounts from the host instead of LocalPreferences. - LiveStatusIndicator probes streams and relays through isVideoOnline. - PollDeadlinePicker uses LocalClock and rememberIs24HourClock. - MarmotGroupIconEditor picks through GallerySelectSingle; the icon uploader takes a MediaUri and drops its temp copy through the new MediaUploader.discardTempFile, with uploadMarmotGroupIcon beside it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../service/uploads/AndroidMediaUploader.kt | 8 +++++ .../ui/actions/uploads/RecordAudio.kt | 2 -- .../ui/actions/uploads/VoiceMessagePreview.kt | 1 + .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../note/creators/polls/PollDeadlinePicker.kt | 31 +++++++++---------- .../ui/note/platform/AndroidNotePlatform.kt | 21 +++++++++++++ .../AccountViewModelAndroidActions.kt | 17 ---------- .../chats/marmotGroup/CreateGroupScreen.kt | 2 +- .../chats/marmotGroup/EditGroupInfoScreen.kt | 2 +- .../marmotGroup/MarmotGroupIconEditor.kt | 23 ++++++++------ .../send/MarmotGroupIconUploader.kt | 27 +++++++++------- .../loggedIn/home/ShortNotePostScreen.kt | 13 ++++---- .../screen/loggedIn/home/VoiceReplyScreen.kt | 2 +- .../loggedIn/home/live/LiveStatusIndicator.kt | 9 +++--- .../commons/service/uploads/MediaUploader.kt | 3 ++ .../commons/ui/note/platform/NotePlatform.kt | 27 ++++++++++++++++ .../ui/note/platform/NotePlatformCalls.kt | 22 +++++++++++++ 17 files changed, 140 insertions(+), 72 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt index f0b9ed5f4b..b21073ed51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -612,4 +612,12 @@ class AndroidMediaUploader( val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null } + + override fun discardTempFile(uri: MediaUri) { + try { + uri.path?.let { path -> File(path).takeIf { it.exists() }?.delete() } + } catch (e: Exception) { + Log.w("AndroidMediaUploader", "Failed to delete temp file", e) + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index 7ec1097bb4..7df22cea86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -44,8 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -const val MAX_VOICE_RECORD_SECONDS = 600 - @OptIn(ExperimentalPermissionsApi::class) @Composable fun RecordAudioBox( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index 6150807b7d..8ed32a442b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.resources.record_a_message import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description import com.vitorpamplona.amethyst.commons.resources.remove import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly +import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.util.formatSecondsToTime import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index bba98ca0a4..38be6838c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -189,6 +189,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ZapRail import com.vitorpamplona.amethyst.commons.ui.note.ZapRailIcon import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomSheet +import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog @@ -229,7 +230,6 @@ import com.vitorpamplona.amethyst.model.zap.CashuRailStatus import com.vitorpamplona.amethyst.model.zap.RailCapability import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability -import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt index 03642f27a3..db77c424f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.polls -import android.text.format.DateFormat import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -45,7 +44,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -57,13 +55,14 @@ import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset +import com.vitorpamplona.quartz.utils.currentTimeMillis @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,26 +71,28 @@ fun PollDeadlinePicker(model: ShortNotePostViewModel) { var showTimePicker by remember { mutableStateOf(false) } // Get current time details - val currentTime = Instant.ofEpochMilli(model.closedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() + // The deadline as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = model.closedAt + LocalClock.utcOffsetSeconds(model.closedAt) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = model.closedAt * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { // Only allow today and future dates - return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer + return utcTimeMillis >= currentTimeMillis() - 86400000 // minus 24h buffer } }, ) - val context = LocalContext.current val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) OutlinedCard( @@ -148,10 +149,8 @@ fun PollDeadlinePicker(model: ShortNotePostViewModel) { (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - // Get the offset from UTC for the current instant in the local time zone - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - - model.closedAt = datetimeLocalTimeZone - offset.totalSeconds + // Subtract this zone's current offset from UTC + model.closedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) showTimePicker = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt index 88f9dd929c..9a1b12e86e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent @@ -45,13 +46,17 @@ import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner import com.vitorpamplona.amethyst.ui.note.creators.location.DEFAULT_MAP_ZOOM import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.podcasts.PodcastAudio import kotlinx.collections.immutable.ImmutableList +import okio.Path import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingRoomEvent as AppRenderMeetingRoomEvent import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingRoomPresence as AppRenderMeetingRoomPresence import com.vitorpamplona.amethyst.commons.ui.note.types.RenderMeetingSpaceEvent as AppRenderMeetingSpaceEvent import com.vitorpamplona.amethyst.service.playback.composable.VideoView as AppVideoView import com.vitorpamplona.amethyst.ui.actions.EditPostView as AppEditPostView +import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton as AppRecordVoiceButton +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview as AppVoiceMessagePreview import com.vitorpamplona.amethyst.ui.components.GifVideoView as AppGifVideoView import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview as AppLoadUrlPreview import com.vitorpamplona.amethyst.ui.components.ReusableZapButton as AppReusableZapButton @@ -622,6 +627,22 @@ object AndroidNotePlatform : NotePlatform { accountViewModel = accountViewModel, ) + @Composable + override fun RecordVoiceButton( + onVoiceTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int?, + ) = AppRecordVoiceButton(onVoiceTaken, maxDurationSeconds) + + @Composable + override fun VoiceMessagePreview( + voiceMetadata: AudioMeta, + localFile: Path?, + onRemove: () -> Unit, + onReRecord: ((RecordingResult) -> Unit)?, + isUploading: Boolean, + modifier: Modifier, + ) = AppVoiceMessagePreview(voiceMetadata, localFile, onRemove, onReRecord, isUploading, modifier) + @Composable override fun QuickZapAmountRow( note: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt index d4d9d0c2c5..ff7f245dd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt @@ -21,24 +21,20 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context -import android.net.Uri import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_save_the_video import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery import com.vitorpamplona.amethyst.commons.service.isVideoOnline -import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUploader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -56,19 +52,6 @@ fun AccountViewModel.urlPreview( suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean = httpClientBuilder.isVideoOnline(videoUrl) -/** - * Encrypt + upload a picked image as a group avatar (canonical `marmot-group-image-v1` scheme). - * The returned handle is later passed to [AccountViewModel.updateMarmotGroupMetadata] as - * `MarmotGroupIconChange.Set` to commit it into the group's metadata. Uploading is separated from - * the metadata commit so the (slow) Blossom upload can show its own progress before the commit is - * signed. - */ -suspend fun AccountViewModel.uploadMarmotGroupIcon( - uri: Uri, - mimeType: String?, - uploader: MediaUploader, -): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader) - fun AccountViewModel.saveMediaToGallery( videoUri: String?, mimeType: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index 6b7216fa01..0d0353faff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.RandomInstance diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt index 40e271da12..5e9c16d579 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index 3323bad523..a66e90a0a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,10 +30,13 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupImage import com.vitorpamplona.amethyst.commons.resources.Res @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupIconUrl @@ -73,11 +74,13 @@ fun MarmotGroupIconEditor( onPick: (SelectedMedia) -> Unit, onRemove: () -> Unit, ) { - val resolver = LocalContext.current.contentResolver - val launcher = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) onPick(SelectedMedia(uri, resolver.getType(uri))) + var showPicker by remember { mutableStateOf(false) } + if (showPicker) { + GallerySelectSingle(imagesOnly = true) { media -> + showPicker = false + media?.let(onPick) } + } val model = when { @@ -100,7 +103,7 @@ fun MarmotGroupIconEditor( Modifier .size(96.dp) .clip(CircleShape) - .let { if (enabled) it.clickable { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } else it }, + .let { if (enabled) it.clickable { showPicker = true } else it }, loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, loadRobohash = LocalDisplaySettings.current.loadRobohash, ) @@ -111,7 +114,7 @@ fun MarmotGroupIconEditor( ) { TextButton( enabled = enabled, - onClick = { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, + onClick = { showPicker = true }, ) { Text(stringRes(if (hasIcon) Res.string.marmot_change_photo else Res.string.marmot_add_photo)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt index 39004e420c..b497add518 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt @@ -20,22 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send -import android.net.Uri import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageCipher import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageEncryption import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.utils.Log -import java.io.File /** * Encrypts a picked image with the MIP-01 v2 scheme (see [MarmotGroupImageEncryption]) @@ -50,7 +49,7 @@ class MarmotGroupIconUploader( val account: Account, ) { suspend fun upload( - uri: Uri, + uri: MediaUri, mimeType: String?, server: ServerName, uploader: MediaUploader, @@ -105,13 +104,7 @@ class MarmotGroupIconUploader( } finally { // Delete the intermediate compressed temp file (compress returns the original // URI unchanged when it skips compression, so only delete a distinct temp). - if (compressed.uri != uri) { - try { - compressed.uri.path?.let { path -> File(path).takeIf { it.exists() }?.delete() } - } catch (e: Exception) { - Log.w("MarmotGroupIconUploader", "Failed to delete temp icon file", e) - } - } + if (compressed.uri != uri) uploader.discardTempFile(compressed.uri) } } @@ -119,3 +112,15 @@ class MarmotGroupIconUploader( private const val DEFAULT_MIME = "image/jpeg" } } + +/** + * Encrypts and uploads a picked image as a group avatar (canonical `marmot-group-image-v1` + * scheme). The returned handle is later committed through + * [AccountViewModel.updateMarmotGroupMetadata] as `MarmotGroupIconChange.Set`; uploading is + * separate from that commit so the slow Blossom upload can show its own progress first. + */ +suspend fun AccountViewModel.uploadMarmotGroupIcon( + uri: MediaUri, + mimeType: String?, + uploader: MediaUploader, +): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader) 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 7a412dcaad..548500b82d 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 @@ -140,6 +140,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS +import com.vitorpamplona.amethyst.commons.ui.note.platform.RecordVoiceButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.VoiceMessagePreview import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel @@ -159,11 +162,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour @@ -536,10 +536,9 @@ private fun NewPostScreenBody( val alwaysOnEnabled by accountViewModel.account.settings.alwaysOnNotificationService .collectAsStateWithLifecycle() - val savedAccounts by com.vitorpamplona.amethyst.LocalPreferences - .accountsFlow() - .collectAsStateWithLifecycle() - val hasMultipleAccounts = (savedAccounts?.size ?: 0) > 1 + val savedAccounts by accountViewModel.host.savedAccounts + .collectAsStateWithLifecycle(emptySet()) + val hasMultipleAccounts = savedAccounts.size > 1 postViewModel.scheduledForSec?.let { current -> Row( verticalAlignment = CenterVertically, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 43185481ab..7a37f4b475 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose +import com.vitorpamplona.amethyst.commons.ui.note.platform.VoiceMessagePreview import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 69badc6e51..156773e3ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -32,9 +32,8 @@ import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel -import com.vitorpamplona.amethyst.commons.service.OnlineChecker +import com.vitorpamplona.amethyst.commons.service.isVideoOnline import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.checkVideoIsOnline import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers @@ -97,12 +96,12 @@ private suspend fun checkChannelIsOnline( // Check if streaming URL is online, fall back to relay check val streamingUrl = channel.info?.streaming() if (!streamingUrl.isNullOrBlank()) { - accountViewModel.checkVideoIsOnline(streamingUrl) + accountViewModel.httpClientBuilder.isVideoOnline(streamingUrl) } else { // Check relay connection val relayUrl = channel.relayHintUrl() if (relayUrl != null) { - OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + accountViewModel.httpClientBuilder.isVideoOnline(relayUrl.url) } else { false } @@ -113,7 +112,7 @@ private suspend fun checkChannelIsOnline( is EphemeralChatChannel -> { // Check relay connection for ephemeral chat val relayUrl = channel.roomId.relayUrl - OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo) + accountViewModel.httpClientBuilder.isVideoOnline(relayUrl.url) } else -> { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt index 1165156444..8313475070 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt @@ -89,6 +89,9 @@ interface MediaUploader { /** The file name the picker reported for [uri] (Android's `DISPLAY_NAME`), if any. */ fun displayName(uri: MediaUri): String? = uri.lastPathSegmentOrNull() + + /** Deletes a temporary copy [compressIfNeeded] wrote, once the caller is done with it. */ + fun discardTempFile(uri: MediaUri) = Unit } /** No media pipeline on this front end yet: every upload fails with a clear message. */ diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt index a6a0f8cb73..fc66fd59f1 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent @@ -44,8 +45,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.podcasts.PodcastAudio import kotlinx.collections.immutable.ImmutableList +import okio.Path /** * The platform halves of the shared note renderer: what a note card needs that only the front end @@ -439,6 +442,30 @@ interface NotePlatform { nav: INav, ) {} + /** + * The microphone button: records a voice message (up to [maxDurationSeconds]) and hands the + * finished recording to [onVoiceTaken]. + */ + @Composable + fun RecordVoiceButton( + onVoiceTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int?, + ) {} + + /** + * Plays back a voice message before it is sent: [localFile] while it is only on the device, + * else [voiceMetadata]'s URL. [onReRecord] replaces it with a new recording. + */ + @Composable + fun VoiceMessagePreview( + voiceMetadata: AudioMeta, + localFile: Path?, + onRemove: () -> Unit, + onReRecord: ((RecordingResult) -> Unit)?, + isUploading: Boolean, + modifier: Modifier, + ) {} + /** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */ @Composable fun EditPostView( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt index 32c38eb268..e3f439102c 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit +import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent @@ -47,9 +48,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.podcasts.PodcastAudio import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import okio.Path // Call-site shims for [NotePlatform]: the same names and defaults the app's own composables have, // so shared note code reads as it did and only its imports point here. @@ -568,6 +571,25 @@ fun QuickZapAmountRow( nav: INav, ) = LocalNotePlatform.current.QuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav) +/** The longest voice message the recorder allows, in seconds. */ +const val MAX_VOICE_RECORD_SECONDS = 600 + +@Composable +fun RecordVoiceButton( + onVoiceTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int? = null, +) = LocalNotePlatform.current.RecordVoiceButton(onVoiceTaken, maxDurationSeconds) + +@Composable +fun VoiceMessagePreview( + voiceMetadata: AudioMeta, + localFile: Path? = null, + onRemove: () -> Unit, + onReRecord: ((RecordingResult) -> Unit)? = null, + isUploading: Boolean = false, + modifier: Modifier = Modifier, +) = LocalNotePlatform.current.VoiceMessagePreview(voiceMetadata, localFile, onRemove, onReRecord, isUploading, modifier) + @Composable fun EditPostView( onClose: () -> Unit, From 117ed3424f53c352afecaa2f89c7d8bf88d27714 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 12:32:31 +0000 Subject: [PATCH 24/45] refactor: nest-room launch port, KMP ICS export and Marmot group notices - AppPlatform.openNestRoom starts the audio room; the live bubble and the nest lobby call it instead of NestActivity.launch. - IcsExport moves to commonMain: UTC stamps and basic dates are computed on SearchDate instead of java.time (IcsExportTest still passes). - Marmot create/edit screens toast through the shared notices; VoiceReplyViewModel names exceptions with KClass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../ui/platform/AndroidAppPlatform.kt | 5 ++++ .../chats/marmotGroup/CreateGroupScreen.kt | 12 ++------ .../chats/marmotGroup/EditGroupInfoScreen.kt | 18 ++++-------- .../loggedIn/home/VoiceReplyViewModel.kt | 2 +- .../home/live/RenderLiveActivityBubble.kt | 10 ++----- .../nests/room/lobby/NestLobbyScreen.kt | 10 ++----- .../commons/model/nip52Calendar/IcsExport.kt | 28 ++++++++++--------- .../commons/ui/platform/AppPlatform.kt | 6 ++++ 8 files changed, 42 insertions(+), 49 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt (89%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt index 468d992788..5be307c587 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.UriHandler +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -37,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel @@ -52,6 +54,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.legalSettingsCateg object AndroidAppPlatform : AppPlatform { override val isCastingAvailable: Boolean get() = BuildConfig.IS_CASTING_AVAILABLE + // NestActivity.launch adds FLAG_ACTIVITY_NEW_TASK, so the application context can start it. + override fun openNestRoom(addressValue: String) = NestActivity.launch(Amethyst.instance.appContext, addressValue) + // Per flavour: Play links the hosted policies, F-Droid surfaces none. override fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = flavorLegalSettingsCategory(uriHandler) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index 0d0353faff..ea2d79f4e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -42,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange import com.vitorpamplona.amethyst.commons.marmot.ui.MarmotRetentionChoice @@ -60,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not import com.vitorpamplona.amethyst.commons.resources.marmot_skip_for_now import com.vitorpamplona.amethyst.commons.resources.marmot_use_outbox_relays import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -93,7 +92,7 @@ fun CreateGroupScreen( var isCreating by remember { mutableStateOf(false) } var showKeyPackageRelayDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - val context = LocalContext.current + val longNotice = rememberLongNotice() /** * Create the group, optionally after [prepare]. @@ -154,12 +153,7 @@ fun CreateGroupScreen( } catch (e: Exception) { isCreating = false launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_create_group, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_create_group, e.message)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt index 5e9c16d579..03336f8f07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -40,7 +39,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange @@ -58,6 +56,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_name_placeholde import com.vitorpamplona.amethyst.commons.resources.marmot_legacy_group_no_avatar_url import com.vitorpamplona.amethyst.commons.resources.save import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -92,7 +92,8 @@ fun EditGroupInfoScreen( var removeIcon by remember { mutableStateOf(false) } var isSaving by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val iconChanged = pickedIcon != null || removeIcon val avatarUrlChanged = avatarUrl.trim() != currentAvatarUrl?.url.orEmpty() @@ -136,20 +137,13 @@ fun EditGroupInfoScreen( accountViewModel.setMarmotGroupAvatarUrl(nostrGroupId, avatarUrl.trim()) } launch(Dispatchers.Main) { - Toast - .makeText(context, loadStringRes(Res.string.marmot_group_info_updated), Toast.LENGTH_SHORT) - .show() + shortNotice.show(loadStringRes(Res.string.marmot_group_info_updated)) } nav.popBack() } catch (e: Exception) { isSaving = false launch(Dispatchers.Main) { - Toast - .makeText( - context, - loadStringRes(Res.string.marmot_failed_to_update, e.message), - Toast.LENGTH_LONG, - ).show() + longNotice.show(loadStringRes(Res.string.marmot_failed_to_update, e.message)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 287b79bcc5..fed2108c79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -220,7 +220,7 @@ class VoiceReplyViewModel : ViewModel() { loadStringRes(Res.string.upload_error_title), loadStringRes( Res.string.upload_error_voice_message_exception, - e.message ?: e.javaClass.simpleName, + e.message ?: e::class.simpleName.orEmpty(), ), ) } finally { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index 50ea9a4b59..4a7eb7ade0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -39,9 +38,9 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.observe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.Gallery +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent @Composable @@ -65,7 +64,7 @@ fun RenderLiveActivityBubble( null } } - val context = LocalContext.current + val appPlatform = LocalAppPlatform.current FilledTonalButton( contentPadding = PaddingValues(start = 8.dp, end = 10.dp, bottom = 0.dp, top = 0.dp), onClick = { @@ -75,10 +74,7 @@ fun RenderLiveActivityBubble( val dTag = meetingEvent.address().dTag if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) { NestBridge.set(accountViewModel) - NestActivity.launch( - context = context, - addressValue = meetingEvent.address().toValue(), - ) + appPlatform.openNestRoom(meetingEvent.address().toValue()) } else { // Fall back to the channel route so the user // still lands somewhere — same as a malformed diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt index c52dd74e07..80e025a7f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt @@ -55,7 +55,6 @@ 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.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -85,11 +84,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceClosedFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpaceOpenFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePlannedFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePrivateFlag +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel import com.vitorpamplona.quartz.nip01Core.core.Address @@ -292,7 +291,7 @@ private fun OpenNestRoomAction( val roomId = event.address().dTag if (serviceBase.isNullOrBlank() || endpoint.isNullOrBlank() || roomId.isBlank()) return - val context = LocalContext.current + val appPlatform = LocalAppPlatform.current // Filled button so the primary CTA is unmistakable; the actions // Row in the AppBar already centers its children vertically, but // we trim the default end padding so the button sits flush with @@ -300,10 +299,7 @@ private fun OpenNestRoomAction( Button( onClick = { NestBridge.set(accountViewModel) - NestActivity.launch( - context = context, - addressValue = event.address().toValue(), - ) + appPlatform.openNestRoom(event.address().toValue()) }, contentPadding = ButtonDefaults.ContentPadding, modifier = Modifier.padding(end = 8.dp), diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt similarity index 89% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt index 2c5e7b579b..ece4e7b3b4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt @@ -20,14 +20,11 @@ */ package com.vitorpamplona.amethyst.commons.model.nip52Calendar +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter /** * Serialises NIP-52 calendar events to RFC 5545 iCalendar (`.ics`) text. The output is the @@ -47,8 +44,6 @@ import java.time.format.DateTimeFormatter object IcsExport { private const val PRODID = "-//Amethyst//NIP-52//EN" private const val CRLF = "\r\n" - private val UtcStamp: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'") - private val IsoBasicDate: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") fun appointmentToIcs( event: Any, @@ -189,14 +184,21 @@ object IcsExport { sb.append("CATEGORIES:").append(hashtags.joinToString(",") { escapeText(it) }).append(CRLF) } - private fun formatUtcInstant(unixSeconds: Long): String = UtcStamp.format(Instant.ofEpochSecond(unixSeconds).atOffset(ZoneOffset.UTC)) + /** `yyyyMMdd'T'HHmmss'Z'` in UTC. */ + private fun formatUtcInstant(unixSeconds: Long): String { + val date = SearchDate.civilFromDays(unixSeconds.floorDiv(SECONDS_PER_DAY)) + val secondOfDay = unixSeconds.mod(SECONDS_PER_DAY) + return basicDate(date) + "T" + pad2(secondOfDay / 3600) + pad2(secondOfDay / 60 % 60) + pad2(secondOfDay % 60) + "Z" + } - private fun tryFormatBasicDate(iso: String): String? = - try { - IsoBasicDate.format(LocalDate.parse(iso)) - } catch (_: Throwable) { - null - } + /** `yyyy-MM-dd` as `yyyyMMdd`, or null when [iso] is not a valid date. */ + private fun tryFormatBasicDate(iso: String): String? = SearchDate.parse(iso)?.let(::basicDate) + + private fun basicDate(date: SearchDate): String = date.year.toString().padStart(4, '0') + pad2(date.month.toLong()) + pad2(date.day.toLong()) + + private fun pad2(value: Long): String = value.toString().padStart(2, '0') + + private const val SECONDS_PER_DAY = 86_400L /** * Escapes text per RFC 5545 §3.3.11: backslash, semicolon, comma, newline. Carriage diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt index 830d968259..3df0c16cbb 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt @@ -111,6 +111,12 @@ interface AppPlatform { @Composable fun rememberWebAppIconModel(url: String): String? = null + /** + * Opens the audio room at [addressValue] (a kind-30312 address) in its own window; callers + * hand the account over through `NestBridge` first. Does nothing where rooms are not wired. + */ + fun openNestRoom(addressValue: String) {} + /** Draws nothing: previews, and front ends still wiring their pieces. */ object None : AppPlatform } From 7a5000ce5042060b2645278a9121974c7aec35db Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 12:50:45 +0000 Subject: [PATCH 25/45] refactor: move the Home, Discover, nest lobby and Marmot group editor screens 22 files move to commonsUI: HomeScreen and DiscoverScreen with their top bars, live bubbles and datasources, the backup nudge, NestLobbyScreen with its chat composer, and the Marmot group create/edit screens with the icon editor and uploader. CordnScreenIndependenceTest now scans the chats packages in both the app and commonsUI: the last Marmot screens left the app, and scanning only there would leave the guard with nothing to check. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../amethyst/ui/navigation/AppNavigation.kt | 10 +++---- .../loggedIn/BottomBarFeedPreloaders.kt | 4 +-- .../loggedIn/nests/room/chat/NestChatPanel.kt | 2 ++ .../cordn/CordnScreenIndependenceTest.kt | 30 +++++++++++++------ .../loggedIn/backups/BackupConflictCards.kt | 2 +- .../chats/marmotGroup/CreateGroupScreen.kt | 5 ++-- .../chats/marmotGroup/EditGroupInfoScreen.kt | 5 ++-- .../marmotGroup/MarmotGroupIconEditor.kt | 2 +- .../send/MarmotGroupIconUploader.kt | 2 +- .../loggedIn/discover/DiscoverScreen.kt | 6 ++-- .../loggedIn/discover/DiscoveryTopBar.kt | 2 +- .../DiscoveryFilterAssemblerSubscription.kt | 2 +- .../loggedIn/home/AlgoFeedStatusBanner.kt | 2 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 14 ++++----- .../ui/screen/loggedIn/home/HomeTopBar.kt | 2 +- .../ui/screen/loggedIn/home/NewNoteButton.kt | 2 +- .../HomeFilterAssemblerSubscription.kt | 2 +- .../loggedIn/home/live/LiveStatusIndicator.kt | 3 +- .../home/live/RenderEphemeralBubble.kt | 2 +- .../loggedIn/home/live/RenderGeohashBubble.kt | 2 +- .../home/live/RenderLiveActivityBubble.kt | 2 +- .../loggedIn/keyBackup/BackupKeysNudge.kt | 2 +- .../nests/room/chat/NestEditFieldRow.kt | 2 +- .../nests/room/chat/NestFileUploadDialog.kt | 2 +- .../room/chat/NestNewMessageViewModel.kt | 3 +- .../nests/room/lobby/NestLobbyScreen.kt | 10 +++---- 26 files changed, 70 insertions(+), 52 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/backups/BackupConflictCards.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/DiscoverScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/DiscoveryTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/HomeScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/HomeTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/NewNoteButton.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt (97%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 420db239df..b4a8cce6c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -103,6 +103,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.G import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.NewGeohashChatScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen @@ -134,6 +136,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.Co import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.contactList.ContactListUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.drafts.DraftListScreen @@ -157,6 +160,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagPost import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen @@ -173,6 +177,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicPlaylist import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicTracksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.NewMusicPlaylistScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.ConnectedAppsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen @@ -286,15 +291,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEv import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.EditGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader @@ -310,7 +312,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeSc import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen @@ -319,7 +320,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppDetailScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index f26a420742..48922d31f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -30,11 +30,13 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.datasource.HighlightsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssemblerSubscription @@ -51,8 +53,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt index d1ede67cef..814f7a393d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt @@ -49,6 +49,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nest_chat_empty import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnScreenIndependenceTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnScreenIndependenceTest.kt index e5a28cf819..d45467d629 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnScreenIndependenceTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnScreenIndependenceTest.kt @@ -41,20 +41,32 @@ import java.io.File * asks for, added with the first screen rather than after the fifth. */ class CordnScreenIndependenceTest { - private val screensRoot: File by lazy { + /** + * The chats screen packages, in the app and in `commonsUI`: screens move from the first to the + * second one by one, and the guard has to follow them. + */ + private val screensRoots: List by lazy { // Resolved, not assumed: a wrong root makes an architecture test pass // for the wrong reason, which is worse than not having one. - generateSequence(File(".").absoluteFile) { it.parentFile } - .map { File(it, "amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats") } - .firstOrNull { it.isDirectory } - ?: error("cannot locate the chats screen package from ${File(".").absolutePath}") + val repo = + generateSequence(File(".").absoluteFile) { it.parentFile } + .firstOrNull { File(it, "commonsUI").isDirectory && File(it, "amethyst").isDirectory } + ?: error("cannot locate the repository root from ${File(".").absolutePath}") + listOf( + "amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats", + "commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats", + ).map { File(repo, it) } + .filter { it.isDirectory } + .ifEmpty { error("cannot locate the chats screen packages under $repo") } } private fun kotlinFilesIn(pkg: String): List = - File(screensRoot, pkg) - .walkTopDown() - .filter { it.isFile && it.extension == "kt" } - .toList() + screensRoots.flatMap { root -> + File(root, pkg) + .walkTopDown() + .filter { it.isFile && it.extension == "kt" } + .toList() + } private fun importsMatching( pkg: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictCards.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictCards.kt index de8a201fc7..ca6c952802 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictCards.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index ea2d79f4e8..fc6a54f588 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -63,13 +63,14 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt index 03336f8f07..f745746ee6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -62,11 +62,12 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index a66e90a0a2..ecfef27dd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.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.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt index b497add518..26e58a8b98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.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.chats.marmotGroup.send +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.marmotGroup.send import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoverScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoverScreen.kt index b8727ee1e4..625a50034f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoverScreen.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.discover +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi @@ -80,13 +80,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarP import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.TabItem import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoveryTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoveryTopBar.kt index 9035ab5fae..00e5be5b86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/DiscoveryTopBar.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.discover +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt index 37d199ccb7..7ae3e9c7b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.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.discover.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt index 5ba2457880..e30ced7868 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/AlgoFeedStatusBanner.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeScreen.kt index aed1eef237..b94874c2e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeScreen.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement @@ -92,8 +92,14 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.zonedDrawerSwipeIfM import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictCards import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.NewGeoPostButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.NewHashtagPostButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live.RenderEphemeralBubble +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live.RenderGeohashBubble +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live.RenderLiveActivityBubble +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.keyBackup.BackupKeysNudge import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -103,12 +109,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.backups.BackupConflictCards -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live.RenderEphemeralBubble -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live.RenderGeohashBubble -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live.RenderLiveActivityBubble -import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.BackupKeysNudge import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeTopBar.kt index 0f7d0f8077..a669a4889c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/HomeTopBar.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/NewNoteButton.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/NewNoteButton.kt index 0a3cf4bb8d..1bccc7efd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/NewNoteButton.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt index 06e836f313..17edd3ab3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.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.home.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 156773e3ca..be0799f27c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/LiveStatusIndicator.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.home.live +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.size @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag as MeetingSpaceStatusTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index 7c3cdf9c29..978b6fcc71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderEphemeralBubble.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.home.live +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt index 430fa01877..b3463968cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderGeohashBubble.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.home.live +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index 4a7eb7ade0..02326a8cf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.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.home.live +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.live import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt index 0054503041..fb275ef11f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/BackupKeysNudge.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/keyBackup/BackupKeysNudge.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.keyBackup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.keyBackup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index d61fcfa288..96f6fe6045 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.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.nests.room.chat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt index dbbcbf63c9..a23f9921c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.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.nests.room.chat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index a8d888ec2d..9b08f7b0ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.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.nests.room.chat +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -97,6 +97,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt index 80e025a7f2..e92a49ede6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.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.nests.room.lobby +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.lobby import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -57,7 +57,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -72,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.resources.nest_lobby_host_label import com.vitorpamplona.amethyst.commons.resources.nest_lobby_listeners_count import com.vitorpamplona.amethyst.commons.resources.nest_lobby_no_listeners import com.vitorpamplona.amethyst.commons.resources.nest_lobby_open_action +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor @@ -86,11 +86,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePlannedFlag import com.vitorpamplona.amethyst.commons.ui.note.types.MeetingSpacePrivateFlag import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -151,7 +151,7 @@ private fun NestLobbyContent( // so the user gets @-mention picker, file uploads, draft auto-save, // emoji suggestions, and reply preview without joining audio. val nestScreenModel: NestNewMessageViewModel = - viewModel(key = "NestLobby/$roomATag") + rememberViewModel(key = "NestLobby/$roomATag") { NestNewMessageViewModel() } nestScreenModel.init(accountViewModel) nestScreenModel.load(addressableNote) From 532ee55a59d9d8e1060fe284d908aea22258252f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 13:46:21 +0000 Subject: [PATCH 26/45] refactor: move the composers, calendars, chess, geocache and voice-reply screens Clear the next ring of platform blockers and move the screens they freed: - Date pickers (poll, zap-poll, schedule-at) read the local clock through LocalClock and rememberIs24HourClock instead of java.time / DateFormat. - FileSharer.shareTextFile replaces the app's .ics share; PhoneCalendar wraps the system event composer; relativeTimeSpan wraps DateUtils (and NSRelativeDateTimeFormatter on iOS). - rememberWindowViewModel + LocalWindowViewModelStoreOwner keep state that spans destinations (chess lobby and board, the Cordn group draft) without reaching for FragmentActivity. - isHlsMedia moves to commons.video; ChessEventBroadcaster to commonMain; the host gains supportsWritingAssistant. - geo: links and nostr: shares go through LocalUriHandler / TextSharer. 204 of 252 screens are now shared. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../playback/composable/RenderVideoPlayer.kt | 2 +- .../composable/controls/RenderTopButtons.kt | 2 +- .../composable/mediaitem/IsHlsMedia.kt | 51 ------------- .../composable/mediaitem/MediaItemCache.kt | 28 +------ .../vitorpamplona/amethyst/ui/MainActivity.kt | 8 +- .../ui/components/ZoomableContentDialog.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 28 +++---- .../ui/note/share/ShareNoteAsImageScreen.kt | 2 +- .../ui/note/types/CalendarRsvpRender.kt | 2 +- .../amethyst/ui/note/types/Chess.kt | 2 +- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../loggedIn/AndroidAccountViewModelHost.kt | 2 + .../loggedIn/BottomBarFeedPreloaders.kt | 2 +- .../calendars/CalendarEventListCard.kt | 1 + .../loggedIn/calendars/CalendarsScreen.kt | 2 +- .../ui/screen/loggedIn/calendars/IcsShare.kt | 60 --------------- .../loggedIn/home/NewChessGameButton.kt | 2 +- .../profile/gallery/GalleryCardCompose.kt | 1 + .../amethyst/ui/note/share/QrPayloadTest.kt | 2 + .../2026-09-27-one-ui-android-desktop.md | 13 +++- .../nip64Chess/ChessDismissedGamesStore.kt | 9 +++ .../nip64Chess/ChessEventBroadcaster.kt | 9 ++- .../amethyst/commons/video/HlsMedia.kt | 75 +++++++++++++++++++ .../viewmodels/AccountViewModelHost.kt | 3 + .../amethyst/commons/video}/IsHlsMediaTest.kt | 12 +-- .../ui/components/FileSharer.android.kt | 74 +++++++++++++----- .../calendars/PhoneCalendar.android.kt | 13 +++- .../calendars/RelativeTimeSpan.android.kt | 36 +++++++++ .../mediaServers/FileServerSelectionRow.kt | 2 +- .../uploads/UploadProgressIndicator.kt | 2 +- .../uploads/VoiceAnonymizationSection.kt | 2 +- .../ui/actions/uploads/VoicePresetSelector.kt | 4 +- .../commons/ui/components/FileSharer.kt | 13 +++- .../ui/components/RememberViewModel.kt | 1 + .../commons/ui/components/WindowViewModel.kt | 64 ++++++++++++++++ .../note/creators/polls/PollDeadlinePicker.kt | 2 +- .../note/creators/polls/PollOptionsField.kt | 2 +- .../creators/scheduling/ScheduleAtPicker.kt | 69 +++++++---------- .../zappolls/ZapPollDeadlinePicker.kt | 32 ++++---- .../ui/note/creators/zappolls/ZapPollField.kt | 2 +- .../note/creators/zappolls/ZapPollOption.kt | 4 +- .../zappolls/ZapPollVoteValueRange.kt | 2 +- .../commons}/ui/note/share/QrPayload.kt | 2 +- .../ui/note/share/ShareNoteAsQrScreen.kt | 2 +- .../commons}/ui/note/share/SharedNoteCard.kt | 6 +- .../calendars/CalendarCollectionsScreen.kt | 4 +- .../calendars/CalendarCollectionsTopBar.kt | 2 +- .../calendars/CalendarCollectionsView.kt | 8 +- .../calendars/CalendarRelativeTime.kt | 32 ++------ .../loggedIn/calendars/PhoneCalendar.kt | 41 ++++++++++ .../loggedIn/calendars/RelativeTimeSpan.kt | 22 +++--- .../CalendarsFilterAssemblerSubscription.kt | 2 +- .../calendars/detail/AddToCalendarSheet.kt | 2 +- .../detail/CalendarEventDetailScreen.kt | 67 ++++++++--------- .../cordnGroup/CordnCreateGroupScreen.kt | 2 +- .../cordnGroup/CordnCreateMembersScreen.kt | 15 ++-- .../nip53LiveActivities/ChannelView.kt | 12 +-- .../LiveActivityChannelScreen.kt | 8 +- .../header/LiveActivityTopBar.kt | 2 +- .../header/LiveStreamGoalHeader.kt | 2 +- .../header/LiveStreamTopZappers.kt | 17 +++-- .../loggedIn/chess/AndroidChessAdapter.kt | 2 +- .../screen/loggedIn/chess/ChessGameScreen.kt | 12 +-- .../screen/loggedIn/chess/ChessLobbyScreen.kt | 12 +-- .../loggedIn/chess/ChessRelaySettingsSheet.kt | 2 +- .../loggedIn/chess/ChessViewModelFactory.kt | 6 +- .../chess/datasource/ChessSubscription.kt | 2 +- .../geocaches/create/NewGeocacheScreen.kt | 22 +++--- .../geocaches/create/NewGeocacheViewModel.kt | 6 +- .../detail/GeocacheAddToHuntSheet.kt | 2 +- .../geocaches/detail/GeocacheDetailParts.kt | 15 ++-- .../geocaches/detail/GeocacheDetailScreen.kt | 11 +-- .../geocaches/detail/GeocacheLogSheet.kt | 2 +- .../geocaches/detail/GeocacheOwnerActions.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 27 +++---- .../screen/loggedIn/home/VoiceReplyScreen.kt | 12 +-- .../loggedIn/home/VoiceReplyViewModel.kt | 6 +- .../screen/loggedIn/polls/PollPostScreen.kt | 8 +- .../loggedIn/profile/gallery/GalleryThumb.kt | 7 +- .../settings/ComposeSettingsScreen.kt | 7 +- .../commons/ui/components/FileSharer.ios.kt | 15 +++- .../loggedIn/calendars/PhoneCalendar.ios.kt | 29 +++++++ .../calendars/RelativeTimeSpan.ios.kt | 46 ++++++++++++ .../commons/ui/components/FileSharer.jvm.kt | 15 +++- .../loggedIn/calendars/PhoneCalendar.jvm.kt | 29 +++++++ .../calendars/RelativeTimeSpan.jvm.kt | 48 ++++++++++++ .../ui/note/share/SharedNoteCardTest.kt | 2 +- .../cordnGroup}/CordnCoordinatorNameTest.kt | 3 +- 88 files changed, 768 insertions(+), 476 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt (94%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt rename {amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem => commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video}/IsHlsMediaTest.kt (89%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt => commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt (91%) create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/mediaServers/FileServerSelectionRow.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/uploads/UploadProgressIndicator.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/uploads/VoiceAnonymizationSection.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/actions/uploads/VoicePresetSelector.kt (95%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/polls/PollDeadlinePicker.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/polls/PollOptionsField.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/scheduling/ScheduleAtPicker.kt (86%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt (85%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/zappolls/ZapPollField.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/zappolls/ZapPollOption.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/creators/zappolls/ZapPollVoteValueRange.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/share/QrPayload.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/share/ShareNoteAsQrScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/share/SharedNoteCard.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt (77%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt (61%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/cordnGroup/CordnCreateMembersScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt (91%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt (91%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/AndroidChessAdapter.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/ChessGameScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/ChessLobbyScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/ChessViewModelFactory.kt (91%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/ShortNotePostScreen.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/VoiceReplyScreen.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/home/VoiceReplyViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/polls/PollPostScreen.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt (98%) create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/share/SharedNoteCardTest.kt (99%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup}/CordnCoordinatorNameTest.kt (95%) 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 6463ff915f..6785b44283 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 @@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.applyView import com.vitorpamplona.amethyst.service.playback.composable.controls.clampViewportShortSide import com.vitorpamplona.amethyst.service.playback.composable.controls.fullscreenSwipeControls import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt index 18348acf29..0761745b1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.cast.CastRequest @@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.service.cast.resolveCastLiveness import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt deleted file mode 100644 index 464553f440..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMedia.kt +++ /dev/null @@ -1,51 +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.service.playback.composable.mediaitem - -import androidx.media3.common.MimeTypes - -/** - * Whether a URL plus its imeta mime identifies HLS. - * - * This is the caller-side form for code that holds a URL and a mime but no `MediaItem` yet — UI that - * has a [MediaItemData] or a `MediaUrlContent`. Once a `MediaItem` exists, - * `isHlsMediaItem` is the equivalent, and both must answer the same way: the UI decides what to - * render, the factory decides how to load it, and a disagreement shows up as a player that streams - * something the surrounding chrome says is a still image. - * - * Defined in terms of [MediaItemCache.toExoPlayerMimeType] rather than re-deriving the rules, so it - * cannot drift from the mime that actually reaches ExoPlayer. That normalizer prefers an explicit - * mime (mapping the four HLS aliases onto [MimeTypes.APPLICATION_M3U8]) and otherwise falls back to a - * **path-anchored** `.m3u8` test. - * - * Both halves matter. A BUD-10 blossom playlist is `https://host/` with no extension at all, - * so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as - * HLS on the strength of its query string. - * - * Note this answers *is it HLS*, which callers use as a proxy for *is it live*. The proxy is - * imprecise in the same way for every HLS URL — an on-demand HLS playlist also answers true — and - * that imprecision is older than this function. Liveness is only truly knowable from - * `#EXT-X-ENDLIST` once the playlist is loaded, which is what `HlsLivenessCache` records. - */ -fun isHlsMedia( - url: String, - mimeType: String?, -): Boolean = MediaItemCache.toExoPlayerMimeType(mimeType, url) == MimeTypes.APPLICATION_M3U8 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt index 9a3096fe8c..4c6c5871fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt @@ -25,9 +25,9 @@ import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache +import com.vitorpamplona.amethyst.commons.video.normalizeStreamMimeType import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -45,23 +45,7 @@ class MediaItemCache : GenericBaseCache(20) { internal fun toExoPlayerMimeType( mimeType: String?, videoUri: String? = null, - ): String? { - if (!mimeType.isNullOrBlank()) { - return when (mimeType.lowercase()) { - "application/vnd.apple.mpegurl", - "application/x-mpegurl", - "audio/x-mpegurl", - "audio/mpegurl", - -> MimeTypes.APPLICATION_M3U8 - - else -> mimeType - } - } - if (videoUri != null && hasM3u8PathExtension(videoUri)) { - return MimeTypes.APPLICATION_M3U8 - } - return null - } + ): String? = normalizeStreamMimeType(mimeType, videoUri) // A side-loaded track needs its media type declared: a NIP-71 `text-track` says nothing // about the format, and a blossom-hosted one is `https://host/` with no extension @@ -81,14 +65,6 @@ class MediaItemCache : GenericBaseCache(20) { language != null && type != null -> "$language \u00B7 $type" else -> language ?: type } - - // Restrict `.m3u8` matching to the path component so a query param or - // fragment that happens to mention .m3u8 (e.g. `?ref=a.m3u8`) on an - // MP4 URI doesn't misroute to HlsMediaSource. - private fun hasM3u8PathExtension(uri: String): Boolean { - val path = uri.substringBefore('?').substringBefore('#') - return path.endsWith(".m3u8", ignoreCase = true) - } } override suspend fun compute(key: MediaItemData): LoadedMediaItem = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 1e0a0202c3..7a7d55aa09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -26,7 +26,9 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.CompositionLocalProvider import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.ui.components.LocalWindowViewModelStoreOwner import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService @@ -61,7 +63,11 @@ class MainActivity : AppCompatActivity() { setContent { AmethystTheme { NowProvider { - AccountScreen(Amethyst.instance.sessionManager) + // The Activity outlives every nav destination: screens that share state + // across destinations (chess lobby + board, the Cordn group draft) keep it here. + CompositionLocalProvider(LocalWindowViewModelStoreOwner provides this) { + AccountScreen(Amethyst.instance.sessionManager) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index fbb54dbea1..1bca3d9246 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -88,9 +88,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b4a8cce6c9..3131255e72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabResel import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.ReplyCommentPostScreen +import com.vitorpamplona.amethyst.commons.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen @@ -95,7 +96,11 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen @@ -123,6 +128,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen @@ -131,6 +137,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share.ShareToDMScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessGameScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen @@ -150,6 +158,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membersh import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen @@ -161,6 +171,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScre import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.HomeScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen @@ -190,6 +202,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsSc import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastAuthoringScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen @@ -212,6 +225,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisua import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CallSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWordsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen @@ -279,24 +293,16 @@ import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen -import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader @@ -305,15 +311,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FavoriteAppManifestPr import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen @@ -322,13 +324,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index a218c7d509..456783c0cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer @@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt index 60428dea67..46d47a6e65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt @@ -48,13 +48,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarAppointmentLines import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarDateBadge import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detailRouteFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index 4648940283..6f51e05d79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent 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 137fc0b083..745a0d2a72 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 @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size75Modifier import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController @@ -74,7 +75,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.PictureIn 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.mediaitem.MediaItemData -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.ui.components.ShareMediaAction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index e1c361343c..1e38a05e73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -99,6 +99,8 @@ class AndroidAccountViewModelHost( ): Result = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName)) // Built on the application context: the assistant outlives any one Activity inside the ViewModel. + override val supportsWritingAssistant: Boolean get() = WritingAssistantFactory.IS_SUPPORTED + override fun createWritingAssistant(): WritingAssistant = WritingAssistantFactory.create(modules.appContext) override val savedAccounts: Flow> = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 48922d31f1..52947ae777 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.dataso import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt index b08505e1bc..705333eca4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarEventListCard.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt index 0403996741..b43337ca94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt deleted file mode 100644 index fad8c29ef1..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/IcsShare.kt +++ /dev/null @@ -1,60 +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.calendars - -import android.content.Context -import android.content.Intent -import androidx.core.content.FileProvider -import java.io.File - -/** - * Writes [content] to a `.ics` file in the app's cache dir and opens the system share sheet so - * the user can hand the file to a calendar app, email client, file manager, etc. - * - * The file lives in `cacheDir/calendar/` — covered by the existing `` entry in - * `file_paths.xml`, so [FileProvider] can hand out a `content://` URI without further config. - * The receiver gets a read-permission grant via [Intent.FLAG_GRANT_READ_URI_PERMISSION] that - * lasts only for the duration of the share. - */ -fun shareIcs( - context: Context, - filename: String, - content: String, - chooserTitle: String, -) { - val dir = File(context.cacheDir, "calendar").apply { mkdirs() } - val file = File(dir, filename) - file.writeText(content) - - val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) - val intent = - Intent(Intent.ACTION_SEND).apply { - type = "text/calendar" - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - val chooser = - Intent - .createChooser(intent, chooserTitle) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(chooser) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt index a0e91364df..1d6270286f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_chess_game import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory /** * Floating action button for creating new chess game challenges 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 6807e8465a..0b56c3bddb 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 @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt index e00804aded..0e5a42badd 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.note.share import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.ui.note.share.QrPayloadMode +import com.vitorpamplona.amethyst.commons.ui.note.share.qrPayloadFor import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index bdbbf41bfc..891b398efc 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -466,7 +466,18 @@ assemblers, `EventSync`, …). Packages are renamed on the way: wraps BiometricPrompt with the keyguard fallback. `CalendarTimeFormat` is shared on `DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection editor, NWC setup and the zap-amount settings. 185 of 252 screens shared. - - **Next:** 67 screens remain, and what blocks them is the platform itself: + - **Wave 10 (2026-09-30):** Home, Discover, the nest lobby and the Marmot group editor + first; then the date pickers on `LocalClock` (poll and zap-poll deadlines, schedule-at + with its presets), `FileSharer.shareTextFile` (the .ics export), `PhoneCalendar` (the + system event composer), `relativeTimeSpan` (`DateUtils` / `NSRelativeDateTimeFormatter`), + `rememberWindowViewModel` + `LocalWindowViewModelStoreOwner` (state shared across + destinations: the chess lobby and board, the Cordn group draft), `isHlsMedia` in + `commons.video`, `ChessEventBroadcaster` in commonMain and `supportsWritingAssistant` on + the host. `geo:` links go through `LocalUriHandler`. Moved: the note and poll composers, + voice reply, the calendar collections and event detail, the geocache detail and editor, + the live-activity channel, chess, Cordn group creation, the QR share and the compose + settings. 204 of 252 screens shared. + - **Next:** 48 screens remain, and what blocks them is the platform itself: - `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents). - The Nests activity and PiP. - The call screens. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt index 1adbf9ab06..b895dfd75a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessDismissedGamesStore.kt @@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringSetPreferencesKey +import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import okio.IOException @@ -64,3 +65,11 @@ class ChessDismissedGamesStore( } } } + +/** + * Where the app keeps the dismissed-chess-games store. + * + * A new file rather than a migration of `chess_dismissed_games`: anyone who had dismissed a game + * sees it once more and dismisses it again. + */ +fun AppPreferenceStores.chessDismissedGamesStore(): ChessDismissedGamesStore = ChessDismissedGamesStore(getDataStore("chess_dismissed_games_v2")) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt similarity index 94% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt index 47a66c3f6c..c157b01c38 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.currentTimeMillis import kotlinx.coroutines.delay /** @@ -84,7 +85,7 @@ class ChessEventBroadcaster( val dummyFilter = Filter( kinds = listOf(JesterProtocol.KIND), - since = (System.currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results + since = (currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results limit = 1, ) @@ -136,10 +137,10 @@ class ChessEventBroadcaster( targetRelays: Set, timeoutMs: Long, ): Set { - val startTime = System.currentTimeMillis() + val startTime = currentTimeMillis() val pollInterval = 200L - while (System.currentTimeMillis() - startTime < timeoutMs) { + while (currentTimeMillis() - startTime < timeoutMs) { val connected = client.connectedRelaysFlow().value val targetConnected = targetRelays.intersect(connected) @@ -148,7 +149,7 @@ class ChessEventBroadcaster( } // At least one connected is good enough after half the timeout - if (targetConnected.isNotEmpty() && System.currentTimeMillis() - startTime > timeoutMs / 2) { + if (targetConnected.isNotEmpty() && currentTimeMillis() - startTime > timeoutMs / 2) { return targetConnected } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt new file mode 100644 index 0000000000..7918b678e3 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/video/HlsMedia.kt @@ -0,0 +1,75 @@ +/* + * 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.commons.video + +/** The mime HLS playlists are normalized to — the value of media3's `MimeTypes.APPLICATION_M3U8`. */ +const val HLS_MIME_TYPE = "application/x-mpegURL" + +/** + * The mime a player should be handed for [mimeType] on [url]: an explicit mime wins (the four HLS + * aliases map onto [HLS_MIME_TYPE]); without one, a path-anchored `.m3u8` means HLS; otherwise + * null, letting the player infer the format. + */ +fun normalizeStreamMimeType( + mimeType: String?, + url: String? = null, +): String? { + if (!mimeType.isNullOrBlank()) { + return when (mimeType.lowercase()) { + "application/vnd.apple.mpegurl", + "application/x-mpegurl", + "audio/x-mpegurl", + "audio/mpegurl", + -> HLS_MIME_TYPE + + else -> mimeType + } + } + if (url != null && hasM3u8PathExtension(url)) { + return HLS_MIME_TYPE + } + return null +} + +/** + * Whether a URL plus its imeta mime identifies HLS, for code that holds a URL and a mime but no + * player item yet. Defined through [normalizeStreamMimeType] so it cannot drift from the mime that + * actually reaches the player. + * + * Both halves matter. A BUD-10 blossom playlist is `https://host/` with no extension at all, + * so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as + * HLS on the strength of its query string. + * + * Note this answers *is it HLS*, which callers use as a proxy for *is it live*: an on-demand HLS + * playlist also answers true. Liveness is only truly knowable from `#EXT-X-ENDLIST` once the + * playlist is loaded. + */ +fun isHlsMedia( + url: String, + mimeType: String?, +): Boolean = normalizeStreamMimeType(mimeType, url) == HLS_MIME_TYPE + +// Restrict `.m3u8` matching to the path component so a query param or fragment that happens to +// mention .m3u8 (e.g. `?ref=a.m3u8`) on an MP4 URI doesn't misroute to the HLS source. +private fun hasM3u8PathExtension(uri: String): Boolean { + val path = uri.substringBefore('?').substringBefore('#') + return path.endsWith(".m3u8", ignoreCase = true) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index 4e6967c763..e5d4bca6d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -91,6 +91,9 @@ interface AccountViewModelHost { presetName: String, ): Result = Result.failure(UnsupportedOperationException("Voice anonymization is not available on this platform")) + /** Whether this build can offer an on-device writing assistant at all (it shows the setting). */ + val supportsWritingAssistant: Boolean get() = false + /** A new on-device writing assistant, or null where the platform has none. */ fun createWritingAssistant(): WritingAssistant? = null diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt similarity index 89% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt index 9b34587518..14e69d1f8c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/IsHlsMediaTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/video/IsHlsMediaTest.kt @@ -18,11 +18,11 @@ * 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.service.playback.composable.mediaitem +package com.vitorpamplona.amethyst.commons.video -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue /** * The two cases that motivated replacing the old `.m3u8`-substring predicate are @@ -39,7 +39,7 @@ class IsHlsMediaTest { assertTrue(isHlsMedia(blossom, "application/x-mpegurl")) assertTrue(isHlsMedia(blossom, "application/vnd.apple.mpegurl")) - assertFalse("no mime and no extension leaves nothing to go on", isHlsMedia(blossom, null)) + assertFalse(isHlsMedia(blossom, null), "no mime and no extension leaves nothing to go on") } @Test @@ -54,7 +54,7 @@ class IsHlsMediaTest { fun recognisesAPlainM3u8Path() { assertTrue(isHlsMedia("https://host/live.m3u8", null)) assertTrue(isHlsMedia("https://host/live.M3U8", null)) - assertTrue("query strings don't hide the path", isHlsMedia("https://host/live.m3u8?vt=abc", null)) + assertTrue(isHlsMedia("https://host/live.m3u8?vt=abc", null), "query strings don't hide the path") } @Test diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt index 6066b0f73c..50c6065060 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.android.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.ui.components +import android.content.Context import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -33,29 +34,62 @@ import java.util.zip.ZipOutputStream @Composable actual fun rememberFileSharer(): FileSharer { val context = LocalContext.current - return remember(context) { - FileSharer { zipName, files, title -> - val zipFile = File(context.cacheDir, zipName) - ZipOutputStream(FileOutputStream(zipFile)).use { zip -> - files.forEach { (name, content) -> - zip.putNextEntry(ZipEntry(name)) - zip.write(content.toByteArray()) - zip.closeEntry() - } + return remember(context) { AndroidFileSharer(context) } +} + +private class AndroidFileSharer( + val context: Context, +) : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) { + val zipFile = File(context.cacheDir, zipName) + ZipOutputStream(FileOutputStream(zipFile)).use { zip -> + files.forEach { (name, content) -> + zip.putNextEntry(ZipEntry(name)) + zip.write(content.toByteArray()) + zip.closeEntry() + } + } + + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "application/zip" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_TITLE, title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile) + context.startActivity(Intent.createChooser(sendIntent, title)) + } - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "application/zip" - putExtra(Intent.EXTRA_STREAM, uri) - putExtra(Intent.EXTRA_TITLE, title) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + /** + * The file lives in `cacheDir/shared/`, covered by the `` entry in + * `file_paths.xml`; the receiver's read grant lasts only for the share. + */ + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) { + val dir = File(context.cacheDir, "shared").apply { mkdirs() } + val file = File(dir, fileName) + file.writeText(content) - context.startActivity(Intent.createChooser(sendIntent, title)) - } + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) + val intent = + Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(Intent.createChooser(intent, title).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt index 11a93892f0..00a73473bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/AddToPhoneCalendar.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.android.kt @@ -18,17 +18,26 @@ * 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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.provider.CalendarContract +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar { + val context = LocalContext.current + return remember(context) { PhoneCalendar { addToPhoneCalendar(context, it) } } +} + /** * Opens the system "New Event" composer (Google Calendar / Samsung / iCloud / etc.) pre-populated * with this appointment's title, time, location, and description. The user sees their normal @@ -38,7 +47,7 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent * Returns true if a calendar app handled the intent, false if no handler was found — the caller * can decide whether to fall back to the .ics share path. */ -fun addToPhoneCalendar( +private fun addToPhoneCalendar( context: Context, event: Event, ): Boolean { diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt new file mode 100644 index 0000000000..36dad1e866 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.android.kt @@ -0,0 +1,36 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import android.text.format.DateUtils + +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String = + DateUtils + .getRelativeTimeSpanString( + targetSeconds * 1000L, + nowSeconds * 1000L, + if (dayResolution) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.kt index cc37db7d72..98ab16054c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/mediaServers/FileServerSelectionRow.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.actions.mediaServers +package com.vitorpamplona.amethyst.commons.ui.actions.mediaServers import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.kt index 0a8c4439ea..0da947cfed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/UploadProgressIndicator.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.kt index 28b2cfe15a..8f10a60d63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoiceAnonymizationSection.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.kt index 3e0ca3fbd4..4a9faa8802 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VoicePresetSelector.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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -32,7 +32,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -44,7 +43,6 @@ fun VoicePresetSelector( onPresetSelected: (VoicePreset) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current val isProcessing = processingPreset != null Row( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt index 544103e9cc..10d368ec52 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable /** Hands generated files to the platform's share sheet. */ -fun interface FileSharer { +interface FileSharer { /** * Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a * chooser titled [title] where the platform has one. @@ -33,6 +33,17 @@ fun interface FileSharer { files: List>, title: String, ) + + /** + * Writes [content] to a file named [fileName] and shares it as [mimeType], behind a chooser + * titled [title] where the platform has one. + */ + fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) } /** diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt index 5542f13703..0112f8656c 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt @@ -67,6 +67,7 @@ inline fun rememberViewModel( * when the store has no view model yet. */ @Composable +@PublishedApi internal fun rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory { val currentFactory by rememberUpdatedState(factory) return remember { diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt new file mode 100644 index 0000000000..f2084bd3c6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/WindowViewModel.kt @@ -0,0 +1,64 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import kotlin.reflect.KClass + +/** + * The store that outlives every navigation destination in this window: Android's Activity. The + * platform entry point provides it above the nav host, whose destinations each get a store of + * their own. + */ +val LocalWindowViewModelStoreOwner: ProvidableCompositionLocal = staticCompositionLocalOf { null } + +/** + * A view model shared by every destination of this window, for state that has to survive + * navigating between screens (a draft filled in over two screens, a game the lobby and the board + * both watch). Without a window store it falls back to [rememberViewModel], scoped to the caller. + */ +@Composable +fun rememberWindowViewModel( + modelClass: KClass, + key: String, + factory: ViewModelProvider.Factory, +): VM { + val owner = LocalWindowViewModelStoreOwner.current ?: return rememberViewModel(modelClass, key, factory) + return remember(owner, key) { ViewModelProvider.create(owner, factory).get(key, modelClass) } +} + +@Composable +inline fun rememberWindowViewModel( + key: String, + factory: ViewModelProvider.Factory, +): VM = rememberWindowViewModel(VM::class, key, factory) + +@Composable +inline fun rememberWindowViewModel( + key: String, + noinline factory: () -> VM, +): VM = rememberWindowViewModel(VM::class, key, rememberLambdaFactory(factory)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.kt index db77c424f5..9c08df9531 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollDeadlinePicker.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.note.creators.polls +package com.vitorpamplona.amethyst.commons.ui.note.creators.polls import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.kt index 3d071a9286..6346c97147 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/polls/PollOptionsField.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.note.creators.polls +package com.vitorpamplona.amethyst.commons.ui.note.creators.polls import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.kt index b7b183eedd..ee9973ce6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/scheduling/ScheduleAtPicker.kt @@ -18,9 +18,8 @@ * 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.note.creators.scheduling +package com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling -import android.text.format.DateFormat import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -54,7 +53,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -76,18 +74,15 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_time_label import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.DayOfWeek -import java.time.Instant -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.temporal.TemporalAdjusters +import com.vitorpamplona.quartz.utils.currentTimeMillis /** * Two-stage date + time picker for scheduling a post for future publication. @@ -107,29 +102,26 @@ fun ScheduleAtPicker( var showDatePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } - val currentTime = - Instant - .ofEpochMilli(scheduledForSec * 1000) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime() + // The scheduled time as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = scheduledForSec + LocalClock.utcOffsetSeconds(scheduledForSec) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = scheduledForSec * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86_400_000 + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= currentTimeMillis() - 86_400_000 }, ) - val context = LocalContext.current - val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) Column(Modifier.fillMaxWidth()) { @@ -220,8 +212,7 @@ fun ScheduleAtPicker( (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - val rawSec = datetimeLocalTimeZone - offset.totalSeconds + val rawSec = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) onChanged(roundUpToNextQuarterHour(rawSec)) showTimePicker = false @@ -300,24 +291,22 @@ private fun PresetChips(onPick: (Long) -> Unit) { } } -private fun presetInOneHour(): Long = (System.currentTimeMillis() / 1000) + 3600 +private fun presetInOneHour(): Long = (currentTimeMillis() / 1000) + 3600 -private fun presetTomorrowMorning(): Long { - val zone = ZoneId.systemDefault() - val tomorrow9am = LocalDate.now(zone).plusDays(1).atTime(LocalTime.of(9, 0)) - return tomorrow9am.atZone(zone).toEpochSecond() -} +private fun presetTomorrowMorning(): Long = nineAm(LocalClock.today().plusDays(1)) private fun presetNextMondayMorning(): Long { - val zone = ZoneId.systemDefault() // Always step at least one day forward — if today is Monday, return next Monday. - val target = - LocalDate - .now(zone) - .plusDays(1) - .with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)) - .atTime(LocalTime.of(9, 0)) - return target.atZone(zone).toEpochSecond() + val tomorrow = LocalClock.today().plusDays(1) + val monday = 1 + return nineAm(tomorrow.plusDays((monday - tomorrow.dayOfWeek()).mod(7))) +} + +/** 09:00 local time on [date], read with the offset in force at 09:00, not at midnight. */ +private fun nineAm(date: SearchDate): Long { + val midnight = LocalClock.startOfDay(date) + val guess = midnight + 9 * TimeUtils.ONE_HOUR + return guess + LocalClock.utcOffsetSeconds(midnight) - LocalClock.utcOffsetSeconds(guess) } /** @@ -326,9 +315,9 @@ private fun presetNextMondayMorning(): Long { * (rare — only if the user picks the exact current quarter-hour), bump forward * one slot. */ -internal fun roundUpToNextQuarterHour(epochSec: Long): Long { +fun roundUpToNextQuarterHour(epochSec: Long): Long { val quarter = 15 * 60L val rounded = ((epochSec + quarter - 1) / quarter) * quarter - val nowSec = System.currentTimeMillis() / 1000 + val nowSec = currentTimeMillis() / 1000 return if (rounded <= nowSec) rounded + quarter else rounded } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index 5b80cbd6ae..c12c3eadea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt @@ -18,9 +18,8 @@ * 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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls -import android.text.format.DateFormat import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -45,7 +44,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -57,13 +55,14 @@ import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in +import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate +import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.utils.TimeUtils -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset +import com.vitorpamplona.quartz.utils.currentTimeMillis @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -72,26 +71,28 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) { var showTimePicker by remember { mutableStateOf(false) } // Get current time details - val currentTime = Instant.ofEpochMilli(model.zapPollClosedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() + // The deadline as a local wall-clock time: seconds since the epoch shifted by this zone's offset. + val localSeconds = model.zapPollClosedAt + LocalClock.utcOffsetSeconds(model.zapPollClosedAt) + val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY) + val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year val datePickerState = rememberDatePickerState( initialSelectedDateMillis = model.zapPollClosedAt * 1000, - yearRange = currentTime.year..2050, + yearRange = currentYear..2050, selectableDates = object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean { // Only allow today and future dates - return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer + return utcTimeMillis >= currentTimeMillis() - 86400000 // minus 24h buffer } }, ) - val context = LocalContext.current val timePickerState = rememberTimePickerState( - initialHour = currentTime.hour, - initialMinute = currentTime.minute, - is24Hour = DateFormat.is24HourFormat(context), + initialHour = localSecondOfDay / TimeUtils.ONE_HOUR, + initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE, + is24Hour = rememberIs24HourClock(), ) OutlinedCard( @@ -148,10 +149,7 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) { (timePickerState.minute * TimeUtils.ONE_MINUTE) } ?: TimeUtils.oneDayAhead() - // Get the offset from UTC for the current instant in the local time zone - val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) - - model.zapPollClosedAt = datetimeLocalTimeZone - offset.totalSeconds + model.zapPollClosedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now()) showTimePicker = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.kt index 6227329a10..67c32e7e99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollField.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.kt index c634c455a9..a6fad228b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollOption.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.KeyboardOptions @@ -67,7 +67,7 @@ fun ZapPollOption( }, label = { Text( - text = stringRes(Res.string.poll_option_index).format(optionIndex + 1), + text = stringRes(Res.string.poll_option_index, (optionIndex + 1).toString()), color = MaterialTheme.colorScheme.placeholderText, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.kt index f9770d7e7a..9166ab3d9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/creators/zappolls/ZapPollVoteValueRange.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.note.creators.zappolls +package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.kt index 63ae327439..20a93d384d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/QrPayload.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/QrPayload.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.kt index 0932556399..b8a5a2216e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/ShareNoteAsQrScreen.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.kt index bf1e6a0050..aa7ac4bf7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCard.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.UrlImageView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.GalleryThumbnail -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt index 294dd1f135..783e53c1ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsScreen.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription /** * Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt index 380ea64ce6..7b7e4ff0f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 6c399baa03..c5058fd7fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarCollectionsView.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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -42,7 +42,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -62,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_s import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title import com.vitorpamplona.amethyst.commons.resources.calendar_export_event import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop @@ -157,7 +157,7 @@ fun CalendarCollectionCard( val title = remember(note.idHex) { event.title() } val description = remember(note.idHex) { event.content.take(180) } val count = remember(note.idHex) { event.calendarEventAddresses().size } - val context = LocalContext.current + val fileSharer = rememberFileSharer() val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) // Calendar collections are addressable (kind 31924); route to the dedicated @@ -223,7 +223,7 @@ fun CalendarCollectionCard( val members = collectMembers(event) val ics = IcsExport.calendarToIcs(event, members, TimeUtils.now()) val filename = IcsExport.calendarFilename(event) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) }) { Icon( symbol = MaterialSymbols.Share, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt similarity index 77% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt index 5549052e62..d3b0fb32df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/CalendarRelativeTime.kt @@ -18,9 +18,8 @@ * 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.calendars +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars -import android.text.format.DateUtils import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -29,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing_with_end import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.relativeTimeSpan import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -36,14 +36,14 @@ import com.vitorpamplona.quartz.utils.TimeUtils * label for an appointment. Returns null when the event has no parseable start (nothing to * anchor a relative phrase to). * - * Uses [DateUtils.getRelativeTimeSpanString] for the underlying minute/hour/day phrasing — that + * Uses the platform's relative-time formatter ([relativeTimeSpan]) for the underlying minute/hour/day phrasing — that * helper is locale-aware and ages from "just now" through "in N days" to absolute date for * far-out events. For all-day events we extend the resolution to DAY so we get "tomorrow", * "in 3 days" instead of an hour-precision phrase that would lie about the start moment. * * Ongoing events (start ≤ now ≤ end) get a composite "Happening now · ends in X" so a user * mid-event sees how much time is left rather than the misleading "started X minutes ago" that - * DateUtils would produce on its own. + * the formatter would produce on its own. */ suspend fun relativeTimeLabel( view: CalendarAppointmentView, @@ -54,31 +54,11 @@ suspend fun relativeTimeLabel( if (end != null && start <= nowSeconds && nowSeconds <= end) { val ongoing = loadStringRes(Res.string.calendar_relative_ongoing) - val endsIn = - DateUtils - .getRelativeTimeSpanString( - end * 1000L, - nowSeconds * 1000L, - if (view.isAllDay) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() + val endsIn = relativeTimeSpan(end, nowSeconds, view.isAllDay) return loadStringRes(Res.string.calendar_relative_ongoing_with_end, ongoing, endsIn) } - val minResolution = - if (view.isAllDay) { - DateUtils.DAY_IN_MILLIS - } else { - DateUtils.MINUTE_IN_MILLIS - } - - return DateUtils - .getRelativeTimeSpanString( - start * 1000L, - nowSeconds * 1000L, - minResolution, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() + return relativeTimeSpan(start, nowSeconds, view.isAllDay) } /** diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt new file mode 100644 index 0000000000..f2e91e24f6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.kt @@ -0,0 +1,41 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable +import com.vitorpamplona.quartz.nip01Core.core.Event + +/** Hands a NIP-52 appointment to the device's own calendar app. */ +fun interface PhoneCalendar { + /** + * Opens the calendar's "New Event" composer pre-filled with [event]. Returns false when there + * is no calendar app to take it (or [event] is not an appointment), so the caller can fall + * back to sharing an .ics file. + */ + fun add(event: Event): Boolean +} + +/** + * Android opens the system calendar composer. Desktop and iOS have no calendar composer wired + * yet and always report false. + */ +@Composable +expect fun rememberPhoneCalendar(): PhoneCalendar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt similarity index 61% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt index 8aa803bd54..4f290044c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.kt @@ -18,18 +18,16 @@ * 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.chess - -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars /** - * Where Android keeps the dismissed-chess-games store. - * - * A new file rather than a migration of `chess_dismissed_games`: the dismissed - * list is a convenience, and chess has few enough users that carrying the old - * data over is not worth the code. Anyone who had dismissed a game sees it once - * more and dismisses it again. + * The platform's own phrase for how far [targetSeconds] is from [nowSeconds] ("in 2 hr.", + * "5 min. ago", "tomorrow"), no finer than a day when [dayResolution] is set. Android asks + * `DateUtils`, iOS `NSRelativeDateTimeFormatter`; Desktop has no such formatter and writes a + * compact signed span. */ -internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore = stores.getDataStore("chess_dismissed_games_v2") +expect suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt index 4066159581..81c741b434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/datasource/CalendarsFilterAssemblerSubscription.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.calendars.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt index e686b5cbfb..2ab24d626a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.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.calendars.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index 08e1939341..8ee6961fcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -18,10 +18,8 @@ * 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.calendars.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -52,11 +50,10 @@ 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.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -91,6 +88,8 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_untitled import com.vitorpamplona.amethyst.commons.resources.edit_calendar_event import com.vitorpamplona.amethyst.commons.resources.route_calendar_event_detail import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture @@ -100,16 +99,15 @@ import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberPhoneCalendar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.shareIcs import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -177,7 +175,9 @@ fun CalendarEventDetailScreen( } }, actions = { - val context = LocalContext.current + val phoneCalendar = rememberPhoneCalendar() + val fileSharer = rememberFileSharer() + val textSharer = rememberTextSharer() val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) // Two share modes: // - .ics for non-nostr calendar apps (Google Calendar / iOS / Outlook) @@ -190,10 +190,10 @@ fun CalendarEventDetailScreen( // has no calendar app registered for ACTION_INSERT (rare; Wear OS, some // GrapheneOS profiles). IconButton(onClick = { - if (!addToPhoneCalendar(context, event)) { + if (!phoneCalendar.add(event)) { val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val filename = IcsExport.appointmentFilename(event, targetAddress) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) } }) { Icon( @@ -206,7 +206,7 @@ fun CalendarEventDetailScreen( IconButton(onClick = { val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val filename = IcsExport.appointmentFilename(event, targetAddress) - shareIcs(context, filename, ics, icsChooserTitle) + fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle) }) { Icon( symbol = MaterialSymbols.Share, @@ -224,15 +224,7 @@ fun CalendarEventDetailScreen( targetAddress.dTag, null, ) - val intent = - Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, "nostr:$naddr") - context.startActivity( - Intent - .createChooser(intent, shareTitle) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) + textSharer.share("nostr:$naddr", null, shareTitle) }) { Icon( symbol = MaterialSymbols.AutoMirrored.Send, @@ -347,7 +339,6 @@ private fun EventBody( fontWeight = FontWeight.Bold, ) } - val context = LocalContext.current val timeFormatter = rememberTimeOfDayFormatter() formatCalendarRange(note, timeFormatter)?.let { range -> Text( @@ -595,7 +586,7 @@ private fun HeroImage( @Composable private fun LocationRow(location: String) { - val context = LocalContext.current + val uriHandler = LocalUriHandler.current // NIP-52 `location` is free-text — a place name, an address, OR a meeting URL (Zoom, Jitsi, // a livestream link). Treat http(s) URLs as web links so they open in the browser; otherwise // hand to the maps app via the geo: intent. @@ -607,17 +598,10 @@ private fun LocationRow(location: String) { .clickable { runCatching { val trimmed = location.trim() - val intent = - if (isUrl) { - Intent(Intent.ACTION_VIEW, trimmed.toUri()) - } else { - // `geo:0,0?q=` is the Android geo intent; the user's - // installed maps app handles it. - Intent(Intent.ACTION_VIEW, "geo:0,0?q=${Uri.encode(trimmed)}".toUri()) - } - // runCatching swallows ActivityNotFoundException when no handler is + // `geo:0,0?q=` is the Android geo URI; the user's installed maps + // app handles it. runCatching swallows the failure when no handler is // installed — we don't have anywhere useful to fall back to. - context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + uriHandler.openUri(if (isUrl) trimmed else "geo:0,0?q=${encodeUriComponent(trimmed)}") } }.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -905,3 +889,18 @@ private fun rememberCalendarsContaining(targetAddress: Address): State create(modelClass: Class): T = LiveStreamTopZappersViewModel(channel) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = LiveStreamTopZappersViewModel(channel) as T }, ) @@ -164,7 +169,7 @@ private fun ZapperPill( } ZapIcon(Size16Modifier, BitcoinOrange) Text( - text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)), + text = showAmountInteger(BigDecimal(entry.totalSats)), style = MaterialTheme.typography.labelMedium, ) Spacer(Modifier.padding(start = 2.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.kt index 5e39b89c07..326f554e62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/AndroidChessAdapter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/AndroidChessAdapter.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.kt index 11af042705..c55fc092a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -18,9 +18,8 @@ * 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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -75,12 +72,13 @@ import com.vitorpamplona.amethyst.commons.resources.chess_game_waiting import com.vitorpamplona.amethyst.commons.resources.chess_loading_game import com.vitorpamplona.amethyst.commons.resources.go_back import com.vitorpamplona.amethyst.commons.resources.relay_settings +import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.quartz.nip64Chess.ChessGameNameGenerator /** @@ -100,10 +98,8 @@ fun ChessGameScreen( nav: INav, ) { // Scope ViewModel to Activity so it's shared between lobby and game screens - val activity = LocalActivity.current as FragmentActivity val chessViewModel: ChessViewModel = - viewModel( - viewModelStoreOwner = activity, + rememberWindowViewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index 89e93c15d2..d4c9b5526d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -18,9 +18,8 @@ * 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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -79,13 +76,14 @@ import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.dismiss import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.resources.route_chess +import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription import com.vitorpamplona.quartz.nip64Chess.Color as ChessColor /** @@ -98,10 +96,8 @@ fun ChessLobbyScreen( nav: INav, ) { // Scope ViewModel to Activity so it's shared between lobby and game screens - val activity = LocalActivity.current as FragmentActivity val chessViewModel: ChessViewModel = - viewModel( - viewModelStoreOwner = activity, + rememberWindowViewModel( key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt index 16af1d0b1f..1f5e16e911 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index ab27e660cb..8472ec36d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -18,16 +18,16 @@ * 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.chess +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel +import com.vitorpamplona.amethyst.commons.nip64Chess.chessDismissedGamesStore import kotlin.reflect.KClass /** @@ -49,7 +49,7 @@ class ChessViewModelFactory( fetcher = AndroidRelayFetcher(account), metadataProvider = AndroidMetadataProvider(), pollingConfig = ChessPollingDefaults.android, - dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)), + dismissedStorage = appStores.chessDismissedGamesStore(), ) as T } throw IllegalArgumentException("Unknown ViewModel class") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt index c5a072a0fc..d8bfb4390e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chess/datasource/ChessSubscription.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.chess.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index 4a9cca8365..057755356d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt @@ -18,11 +18,8 @@ * 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.geocaches.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -62,6 +59,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -70,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -99,7 +96,9 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_owner_qr_warning import com.vitorpamplona.amethyst.commons.resources.geocache_photo_remove import com.vitorpamplona.amethyst.commons.resources.route_edit_geocache import com.vitorpamplona.amethyst.commons.resources.route_new_geocache +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji @@ -139,7 +138,7 @@ fun NewGeocacheScreen( editPubKeyHex: String? = null, editDTag: String? = null, ) { - val model: NewGeocacheViewModel = viewModel() + val model: NewGeocacheViewModel = rememberViewModel { NewGeocacheViewModel() } val scope = rememberCoroutineScope() remember(editKind, editPubKeyHex, editDTag, prefillGeohash) { @@ -154,10 +153,13 @@ fun NewGeocacheScreen( var pickingLocation by remember { mutableStateOf(false) } - val photoPicker = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } + var pickingPhoto by rememberSaveable { mutableStateOf(false) } + if (pickingPhoto) { + GallerySelectSingle(imagesOnly = true) { media -> + pickingPhoto = false + if (media != null) scope.launch { model.uploadImage(media.uri, media.mimeType, accountViewModel.host.mediaUploader) } } + } Scaffold( topBar = { @@ -285,7 +287,7 @@ fun NewGeocacheScreen( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { OutlinedButton( enabled = !model.isUploading.value, - onClick = { photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, + onClick = { pickingPhoto = true }, ) { if (model.isUploading.value) { CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt index 1c60602098..d869e1fd38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt @@ -18,9 +18,8 @@ * 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.geocaches.create +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create -import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -28,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -175,7 +175,7 @@ class NewGeocacheViewModel : ViewModel() { fun isValid() = hasName() && hasLocation() suspend fun uploadImage( - uri: Uri, + uri: MediaUri, mimeType: String?, uploader: MediaUploader, ): Boolean { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt index 8ab7b184f9..7d98836520 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt index 52e05596e9..be3783dfaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt @@ -18,9 +18,8 @@ * 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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail -import android.content.Intent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -40,7 +39,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Size -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -57,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_dnf import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_maintenance import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_note import com.vitorpamplona.amethyst.commons.resources.geocache_verified_find +import com.vitorpamplona.amethyst.commons.resources.quick_action_share +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.GeocacheChip @@ -162,14 +162,11 @@ fun GeocacheWinnerStrip( /** Shares the cache as `nostr:naddr…`, which is how it opens in another NIP-CC client. */ @Composable fun GeocacheShareAction(address: Address) { - val context = LocalContext.current + val sharer = rememberTextSharer() + val shareStr = stringRes(Res.string.quick_action_share) IconButton(onClick = { val naddr = NAddress.create(address.kind, address.pubKeyHex, address.dTag, null) - val intent = - Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, "nostr:$naddr") - context.startActivity(Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + sharer.share("nostr:$naddr", null, shareStr) }) { Icon( symbol = MaterialSymbols.AutoMirrored.Send, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt index d997133943..43e9902e2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheDetailScreen.kt @@ -18,9 +18,8 @@ * 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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail -import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -64,10 +63,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -427,12 +425,11 @@ private fun GeocacheNavigateRow( longitude: Double, name: String, ) { - val context = LocalContext.current + val uriHandler = LocalUriHandler.current OutlinedButton(onClick = { val label = name.ifBlank { "Geocache" } - val uri = "geo:$latitude,$longitude?q=$latitude,$longitude($label)".toUri() runCatching { - context.startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + uriHandler.openUri("geo:$latitude,$longitude?q=$latitude,$longitude($label)") } }) { Icon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt index 87f4ab5b18..70f0b5c12d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheLogSheet.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt index f69a4d5c89..ddbc21f310 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.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.geocaches.detail +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 548500b82d..6f26fb38cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/ShortNotePostScreen.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween @@ -68,7 +68,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.BooleanType @@ -102,14 +101,18 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -129,13 +132,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageF import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists +import com.vitorpamplona.amethyst.commons.ui.note.creators.polls.PollOptionsField import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton +import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtPicker +import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.roundUpToNextQuarterHour import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls.ZapPollField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo @@ -161,14 +168,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection -import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField -import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker -import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour -import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @@ -189,7 +190,7 @@ fun ShortNotePostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ShortNotePostViewModel = viewModel() + val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() } postViewModel.init(accountViewModel) postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) @@ -739,7 +740,7 @@ private fun NewPostScreenBody( showAlwaysOnPrompt = true } else { postViewModel.scheduledForSec = - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) } }, ) @@ -761,7 +762,7 @@ private fun NewPostScreenBody( TextButton(onClick = { showAlwaysOnPrompt = false postViewModel.scheduledForSec = - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) }) { Text(stringRes(Res.string.schedule_post_always_on_prompt_continue)) } @@ -779,7 +780,7 @@ private fun BottomRowActions( if (postViewModel.scheduledForSec != null) { null } else { - roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) + roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60) } }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 7a37f4b475..edd2f1cecb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyScreen.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,8 +38,11 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose @@ -49,9 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -64,7 +64,7 @@ fun VoiceReplyScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: VoiceReplyViewModel = viewModel() + val viewModel: VoiceReplyViewModel = rememberViewModel { VoiceReplyViewModel() } LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) { viewModel.init(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index fed2108c79..f10278ecad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/home/VoiceReplyViewModel.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.home +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController import com.vitorpamplona.amethyst.commons.audio.VoicePreset @@ -58,6 +57,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -215,7 +215,6 @@ class VoiceReplyViewModel : ViewModel() { } catch (e: CancellationException) { Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e) } catch (e: Exception) { - val appContext = Amethyst.instance.appContext accountViewModel.toastManager.toast( loadStringRes(Res.string.upload_error_title), loadStringRes( @@ -239,7 +238,6 @@ class VoiceReplyViewModel : ViewModel() { result: UploadingState, onSuccess: () -> Unit, ) { - val appContext = Amethyst.instance.appContext val uploadErrorTitle = loadStringRes(Res.string.upload_error_title) val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt index 2aa522582a..5d89b902a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -18,18 +18,18 @@ * 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.polls +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.NewPostScreenInner import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.FlowPreview @@ -41,7 +41,7 @@ fun PollPostScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val postViewModel: ShortNotePostViewModel = viewModel() + val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() } postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 934fc14e28..fe3e644ab9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -18,9 +18,8 @@ * 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.profile.gallery +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery -import androidx.annotation.OptIn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -35,7 +34,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.media3.common.util.UnstableApi import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent @@ -61,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.commons.video.isHlsMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent @@ -216,7 +214,6 @@ fun DisplayGalleryAuthorBanner( } } -@OptIn(UnstableApi::class) @Composable fun GalleryContentView( contentList: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 34b9228931..a69dce7b8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ComposeSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -39,7 +39,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -109,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource @@ -153,7 +151,7 @@ fun ComposeSettingsContent( ) SettingsDivider() // The F-Droid build ships no on-device model, so the toggle would do nothing there. - if (WritingAssistantFactory.IS_SUPPORTED) { + if (accountViewModel.host.supportsWritingAssistant) { BooleanSwitchTile( flow = sharedPrefs.automaticallyProposeAiImprovements, icon = MaterialSymbols.AutoAwesome, @@ -246,7 +244,6 @@ private fun PowDifficultyTile(accountViewModel: AccountViewModel) { private fun PowTimeEstimate(difficulty: Int) { if (difficulty <= 0) return - val context = LocalContext.current val estimate by produceState(initialValue = null, difficulty) { val rate = deviceHashesPerSecond() diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt index 34b5b9ff77..433237d050 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.ios.kt @@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable -private val NoFileSharer = FileSharer { _, _, _ -> } +private object NoFileSharer : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) {} + + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) {} +} /** No share sheet wired yet. */ @Composable diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt new file mode 100644 index 0000000000..0cbe7794ae --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.ios.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable + +private val NoPhoneCalendar = PhoneCalendar { false } + +/** No calendar composer wired yet: callers fall back to the .ics share. */ +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt new file mode 100644 index 0000000000..232d530b46 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.ios.kt @@ -0,0 +1,46 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import platform.Foundation.NSDate +import platform.Foundation.NSRelativeDateTimeFormatter +import platform.Foundation.NSRelativeDateTimeFormatterUnitsStyleAbbreviated +import platform.Foundation.dateWithTimeIntervalSince1970 + +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String { + val formatter = NSRelativeDateTimeFormatter() + formatter.unitsStyle = NSRelativeDateTimeFormatterUnitsStyleAbbreviated + val day = 86_400L + val (target, now) = + if (dayResolution) { + Pair(targetSeconds.floorDiv(day) * day, nowSeconds.floorDiv(day) * day) + } else { + Pair(targetSeconds, nowSeconds) + } + return formatter.localizedStringForDate( + NSDate.dateWithTimeIntervalSince1970(target.toDouble()), + relativeToDate = NSDate.dateWithTimeIntervalSince1970(now.toDouble()), + ) +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt index 34b5b9ff77..433237d050 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileSharer.jvm.kt @@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable -private val NoFileSharer = FileSharer { _, _, _ -> } +private object NoFileSharer : FileSharer { + override fun shareTextFilesAsZip( + zipName: String, + files: List>, + title: String, + ) {} + + override fun shareTextFile( + fileName: String, + mimeType: String, + content: String, + title: String, + ) {} +} /** No share sheet wired yet. */ @Composable diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt new file mode 100644 index 0000000000..0cbe7794ae --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/PhoneCalendar.jvm.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import androidx.compose.runtime.Composable + +private val NoPhoneCalendar = PhoneCalendar { false } + +/** No calendar composer wired yet: callers fall back to the .ics share. */ +@Composable +actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt new file mode 100644 index 0000000000..285118e8f4 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/calendars/RelativeTimeSpan.jvm.kt @@ -0,0 +1,48 @@ +/* + * 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.commons.ui.screen.loggedIn.calendars + +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.d +import com.vitorpamplona.amethyst.commons.resources.h +import com.vitorpamplona.amethyst.commons.resources.m +import com.vitorpamplona.amethyst.commons.resources.now +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.math.abs + +/** The JDK has no relative-time formatter: a compact signed span ("+2h", "-5m"). */ +actual suspend fun relativeTimeSpan( + targetSeconds: Long, + nowSeconds: Long, + dayResolution: Boolean, +): String { + val diff = targetSeconds - nowSeconds + val span = abs(diff) + val sign = if (diff < 0) "-" else "+" + return when { + span >= TimeUtils.ONE_DAY -> sign + (span / TimeUtils.ONE_DAY) + loadStringRes(Res.string.d) + dayResolution -> loadStringRes(Res.string.now) + span >= TimeUtils.ONE_HOUR -> sign + (span / TimeUtils.ONE_HOUR) + loadStringRes(Res.string.h) + span >= TimeUtils.ONE_MINUTE -> sign + (span / TimeUtils.ONE_MINUTE) + loadStringRes(Res.string.m) + else -> loadStringRes(Res.string.now) + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.kt similarity index 99% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.kt index 7d76a5852f..063ed1de85 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/share/SharedNoteCardTest.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.note.share +package com.vitorpamplona.amethyst.commons.ui.note.share import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.kt similarity index 95% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.kt index 339fa80822..a540b1387a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/CordnCoordinatorNameTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnCoordinatorNameTest.kt @@ -18,9 +18,8 @@ * 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 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.disambiguate import org.junit.Assert.assertEquals import org.junit.Test From 39fb278ed797b2f5bf061ba1f7287127cc2d0716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:47:17 +0000 Subject: [PATCH 27/45] chore(actions): bump the actions group with 2 updates Bumps the actions group with 2 updates: [actions/setup-java](https://github.com/actions/setup-java) and [crowdin/github-action](https://github.com/crowdin/github-action). Updates `actions/setup-java` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v6.0.0...v6.0.1) Updates `crowdin/github-action` from 2 to 3 - [Release notes](https://github.com/crowdin/github-action/releases) - [Commits](https://github.com/crowdin/github-action/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: crowdin/github-action dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/create-release.yml | 8 ++++---- .github/workflows/crowdin.yml | 2 +- .github/workflows/smoke-test-desktop.yml | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32fad8469a..509202f13c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: run: .claude/hooks/compose_escaping_check.py - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -84,7 +84,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -176,7 +176,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -226,7 +226,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -281,7 +281,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -363,7 +363,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -409,7 +409,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 4683d17921..f9b2ce31d3 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -78,7 +78,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -405,7 +405,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -662,7 +662,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -953,7 +953,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 4235feb76b..f0ba59cb7f 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: crowdin action - uses: crowdin/github-action@v2 + uses: crowdin/github-action@v3 with: upload_sources: true upload_translations: true diff --git a/.github/workflows/smoke-test-desktop.yml b/.github/workflows/smoke-test-desktop.yml index 81052bac97..2b93dbd05f 100644 --- a/.github/workflows/smoke-test-desktop.yml +++ b/.github/workflows/smoke-test-desktop.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 @@ -66,7 +66,7 @@ jobs: uses: actions/checkout@v7 - name: Set up JDK 21 - uses: actions/setup-java@v6.0.0 + uses: actions/setup-java@v6.0.1 with: distribution: 'temurin' java-version: 21 From f9063c8191b996788252d565929b11325e718fcb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 01:24:59 +0000 Subject: [PATCH 28/45] feat: split kind 38000 into mint recommendations, prediction markets and ballots Kind 38000 is NIP-87's mint recommendation, but BAO Markets (prediction markets), an auditable-voting app (ballots) and a flood of d-only "sybil test vote" spam share the number. EventFactory mapped every 38000 to MintRecommendationEvent, so markets, ballots and spam rendered as "Ecash Mint Recommendation" cards in feeds, threads and search. EventFactory now dispatches 38000 by tags: - MintRecommendationEvent iff the first `k` is 38172/38173, or, with no `k`, a non-blank `u` (old recommendations); - else BallotEvent on a non-blank `election`; - else PredictionMarketEvent on `market`, >=2 `outcome`s, or `type`+`end`; - else a plain Event (never consumed, never indexed). New read-only, searchable classes under quartz/experimental: predictionMarkets/PredictionMarketEvent (all three BAO shapes: tags + `data` JSON, BAO Fund, and the first shape; title, description, outcomes, status, resolution, end, category, demo network, bet limits, fee, cancel reason) and ballots/BallotEvent (election, answers from `responses`, `ballot` or `vote_choice`, proof hash). JSON is parsed lazily, once, and never throws. EventFactory.probe(kind) answers kind-level questions (isKnownKind, the SQLite/FS FTS reindex pre-filters, SearchableKindsTest) so 38000 still probes as searchable even though a tagless 38000 is now a plain Event. EventCache consumes the new classes as addressables; wallet code keeps seeing only real recommendations. commonsUI renders them with new cards (PredictionMarketCard: kicker, question, status pill, demo pill, category, outcomes with the winner marked, closing time, terms, cancel reason; BallotCard: election, question -> answer rows, proof hash), wired into NoteCompose and ThreadFeedView. GitStatusPill's filled badge is extracted as StatusPill for reuse. Tests use real production events, one per shape. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012CsFEX4ZT8mL1r8h2M6qfH --- .../references/searchable-kinds.md | 16 +- .../loggedIn/threadview/ThreadFeedView.kt | 8 + .../commons/model/cache/EventCache.kt | 5 + .../composeResources/values/strings.xml | 26 + .../amethyst/commons/ui/note/GitStatusPill.kt | 17 + .../amethyst/commons/ui/note/NoteCompose.kt | 12 + .../amethyst/commons/ui/note/types/Ballot.kt | 241 ++++++++ .../commons/ui/note/types/PredictionMarket.kt | 531 ++++++++++++++++++ .../experimental/ballots/BallotAnswer.kt | 30 + .../experimental/ballots/BallotContent.kt | 100 ++++ .../experimental/ballots/BallotEvent.kt | 86 +++ .../experimental/ballots/TagArrayExt.kt | 32 ++ .../PredictionMarketDetails.kt | 118 ++++ .../PredictionMarketEvent.kt | 172 ++++++ .../PredictionMarketStatus.kt | 49 ++ .../predictionMarkets/TagArrayExt.kt | 88 +++ .../store/sqlite/FullTextSearchModule.kt | 13 +- .../recommendation/MintRecommendationEvent.kt | 27 + .../quartz/utils/EventFactory.kt | 42 +- .../experimental/ballots/BallotEventTest.kt | 137 +++++ .../PredictionMarketEventTest.kt | 284 ++++++++++ .../quartz/nip50Search/SearchableKindsTest.kt | 4 +- .../quartz/utils/EventFactoryKind38000Test.kt | 184 ++++++ .../quartz/utils/Kind38000Fixtures.kt | 88 +++ .../quartz/nip01Core/store/fs/FsEventStore.kt | 11 +- .../nip50Search/IndexableContentGoldenTest.kt | 4 + 26 files changed, 2304 insertions(+), 21 deletions(-) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotAnswer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotContent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketStatus.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEventTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/Kind38000Fixtures.kt diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index 532ecc3d68..1a95ebd664 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -4,8 +4,8 @@ Every concrete `SearchableEvent` implementor in Quartz, with the exact `indexabl expression. **Update this file in the same PR as any change to the searchable set or to an `indexableContent()` body** (see SKILL.md). Verified against the code 2026-09-17. -Counts: 137 concrete classes covering 140 kind values (`GitStatusEvent` spans 4 kinds; -kind 30063 is shared by two NIPs — see the footnote). File paths are under +Counts: 139 concrete classes covering 140 kind values (`GitStatusEvent` spans 4 kinds; +kind 30063 is shared by two NIPs and kind 38000 by three classes — see the footnotes). File paths are under `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/`. Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. @@ -134,7 +134,9 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 36787 | MusicTrackEvent | experimental/music/track | `listOfNotNull(title(), artist(), album(), content)` NL | | 37516 | GeocacheListingEvent | nipCCGeocaching/listing | `listOfNotNull(cacheName(), content)` NL (the `hint` is deliberately not indexed — matching a hint is spoiling it) | | 37517 | GeocacheCurationListEvent | nipCCGeocaching/curation | `listOfNotNull(title(), description(), content)` NL | -| 38000 | MintRecommendationEvent | nip87Ecash/recommendation | `content` | +| 38000 | MintRecommendationEvent ‡ | nip87Ecash/recommendation | `content` | +| 38000 | BallotEvent ‡ | experimental/ballots | `(listOfNotNull(election()) + answers().map { it.answer })` NL | +| 38000 | PredictionMarketEvent ‡ | experimental/predictionMarkets | `listOfNotNull(title(), description())` NL | | 38192 | Ps1SaveEvent | experimental/ps1saves | `listOfNotNull(summary(), saveTitle(), region(), filename())` NL | | 38383 | P2POrderEvent | nip69P2pOrderEvents | `(listOfNotNull(makerName(), currency()) + paymentMethods().orEmpty()).joinToString(" ")` (SP) | | 39000 | GroupMetadataEvent | nip29RelayGroups/metadata | `listOfNotNull(name(), about())` NL | @@ -155,6 +157,14 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. (the release notes) only when the event carries the NIP-82 `i` + `version` tags — a NIP-51 set may hold encrypted private items in `content`, which must never be indexed. +‡ **Kind 38000 is shared** by NIP-87 mint recommendations and two app formats, and `EventFactory` +picks the class by tags: `MintRecommendationEvent` when the first `k` is 38172/38173 (or, with no +`k`, a non-blank `u`); else `BallotEvent` on a non-blank `election`; else `PredictionMarketEvent` +on a `market`, ≥2 `outcome`s, or `type` + `end`; else a plain, unsearchable `Event` (the spam +votes that make up most of the kind). Kind-level probes (`EventFactory.probe`) answer as +`MintRecommendationEvent`. A market's `title()`/`description()` come from its `data` tag JSON, +the `title` tag, or JSON `content`, in that order. + ## Abstract bases (no kind of their own) | Base class | Body | Concrete kinds | 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 71492e7f99..86ff28599d 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 @@ -170,6 +170,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestation import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestationRequest import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestorProficiency import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestorRecommendation +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBallot import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBlossomPieceIndex import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBolt12Zap import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBookshelfDirectory @@ -202,6 +203,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPodcastEpisode import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPodcastMetadata import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPostApproval +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPredictionMarket import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPrivateMessage import com.vitorpamplona.amethyst.commons.ui.note.types.RenderProfileCard import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPs1Save @@ -285,6 +287,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward @@ -303,6 +306,7 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent @@ -1140,6 +1144,10 @@ private fun FullBleedNoteCompose( RenderFedimint(noteEvent) } else if (noteEvent is MintRecommendationEvent) { RenderMintRecommendation(noteEvent) + } else if (noteEvent is PredictionMarketEvent) { + RenderPredictionMarket(baseNote, makeItShort = false, accountViewModel) + } else if (noteEvent is BallotEvent) { + RenderBallot(baseNote, accountViewModel) } else if (noteEvent is ChatEvent) { RenderChat( note = baseNote, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index eab486d0d5..ca70a9b4ff 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -163,6 +163,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent @@ -191,6 +192,7 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent @@ -3962,6 +3964,9 @@ open class EventCache : is CashuMintEvent, is FedimintEvent, is MintRecommendationEvent, + // Other apps' kind-38000 formats, split from mint recommendations by EventFactory. + is BallotEvent, + is PredictionMarketEvent, -> consumeBaseReplaceable(event, relay, wasVerified) // ============================================================ diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index b7db85531f..e09af425e4 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -6361,4 +6361,30 @@ This file type can run code. Check that the name matches what you meant to get. Save From %1$s + Prediction market + Open + Closed + Resolved + Cancelled + Demo · play money + Closes in %1$s + Closed %1$s + Bets %1$s–%2$s sats + Min bet %1$s sats + Max bet %1$s sats + Fee %1$s + Cancelled: %1$s + Winning outcome + + +%1$d more + +%1$d more + + Ballot + Election %1$s + Vote + Proof %1$s + + +%1$d more answer + +%1$d more answers + diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/GitStatusPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/GitStatusPill.kt index 5ad02ef4bf..f61655fc25 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/GitStatusPill.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/GitStatusPill.kt @@ -104,6 +104,22 @@ fun GitStatusPill( } } + StatusPill(label, symbol, container, content, modifier) +} + +/** + * The filled, GitHub-style state badge behind [GitStatusPill]: an icon and a short label in + * [content] on a [container] fill. Shared by the other cards that show a lifecycle state + * (prediction markets) so every status badge in a feed looks alike. + */ +@Composable +fun StatusPill( + label: String, + symbol: MaterialSymbol, + container: Color, + content: Color, + modifier: Modifier = Modifier, +) { Row( modifier = modifier @@ -124,6 +140,7 @@ fun GitStatusPill( style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.SemiBold, color = content, + maxLines = 1, ) } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt index 682ee75b5a..55adb2159f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt @@ -170,6 +170,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestationRequest import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestorProficiency import com.vitorpamplona.amethyst.commons.ui.note.types.RenderAttestorRecommendation import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBadgeAward +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBallot import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBlossomPieceIndex import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBolt12Zap import com.vitorpamplona.amethyst.commons.ui.note.types.RenderBookshelfDirectory @@ -211,6 +212,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPodcastEpisode import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPodcastMetadata import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPostApproval +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPredictionMarket import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPrivateMessage import com.vitorpamplona.amethyst.commons.ui.note.types.RenderProfileCard import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPs1Save @@ -309,6 +311,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward @@ -329,6 +332,7 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent @@ -1492,6 +1496,14 @@ private fun RenderNoteRow( RenderMintRecommendation(noteEvent) } + is PredictionMarketEvent -> { + RenderPredictionMarket(baseNote, makeItShort, accountViewModel) + } + + is BallotEvent -> { + RenderBallot(baseNote, accountViewModel) + } + is ClassifiedsEvent -> { RenderClassifieds( noteEvent, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt new file mode 100644 index 0000000000..e13767902f --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt @@ -0,0 +1,241 @@ +/* + * 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.commons.ui.note.types + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.ballot +import com.vitorpamplona.amethyst.commons.resources.ballot_election +import com.vitorpamplona.amethyst.commons.resources.ballot_more_answers +import com.vitorpamplona.amethyst.commons.resources.ballot_proof_hash +import com.vitorpamplona.amethyst.commons.resources.ballot_vote +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.ui.theme.grayText +import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.experimental.ballots.BallotAnswer +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** How many answers a card lists before collapsing the rest into "+N more answers". */ +private const val MAX_ANSWERS = 4 + +/** + * Renders a kind-38000 ballot from an auditable-voting app: the election it was cast in, its + * answers as question → answer rows, and the proof hash that lets the vote be audited. + * + * Observed rather than read once: a ballot is addressable, so a re-cast vote replaces it in place. + */ +@Composable +fun RenderBallot( + note: Note, + accountViewModel: AccountViewModel, +) { + val observedEvent by observeNoteEvent(note, accountViewModel) + val noteEvent = observedEvent ?: return + + BallotCard(noteEvent) +} + +@Immutable +private class BallotCardState( + val election: String?, + val answers: ImmutableList, + val hiddenAnswers: Int, + val proofHash: String?, +) { + companion object { + fun from(event: BallotEvent): BallotCardState { + val answers = event.answers() + return BallotCardState( + election = event.election(), + answers = answers.take(MAX_ANSWERS).toImmutableList(), + hiddenAnswers = (answers.size - MAX_ANSWERS).coerceAtLeast(0), + proofHash = event.proofHash(), + ) + } + } +} + +@Composable +fun BallotCard(event: BallotEvent) { + val state = remember(event) { BallotCardState.from(event) } + + Column(MaterialTheme.colorScheme.replyModifier) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + symbol = MaterialSymbols.Checklist, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp), + ) + Text( + text = stringRes(Res.string.ballot), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + modifier = Modifier.padding(start = 6.dp), + ) + } + + state.election?.let { + Text( + text = stringRes(Res.string.ballot_election, it), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + if (state.answers.isNotEmpty()) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + state.answers.forEach { AnswerRow(it) } + + if (state.hiddenAnswers > 0) { + Text( + text = pluralStringRes(Res.plurals.ballot_more_answers, state.hiddenAnswers, state.hiddenAnswers), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.placeholderText, + ) + } + } + } + + state.proofHash?.let { ProofHash(it) } + } + } +} + +@Composable +private fun AnswerRow(answer: BallotAnswer) { + // A lone `vote_choice` has no question of its own; everything else is shown as published. + val question = + if (answer.question == BallotEvent.VOTE_QUESTION) { + stringRes(Res.string.ballot_vote) + } else { + answer.question + } + + Row(verticalAlignment = Alignment.Top) { + Text( + text = question, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.grayText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(0.4f), + ) + Icon( + symbol = MaterialSymbols.AutoMirrored.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(horizontal = 4.dp).size(16.dp), + ) + Text( + text = answer.answer, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(0.6f), + ) + } +} + +@Composable +private fun ProofHash(hash: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + symbol = MaterialSymbols.Shield, + contentDescription = null, + tint = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.size(12.dp), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringRes(Res.string.ballot_proof_hash, hash), + style = MaterialTheme.typography.labelSmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } +} + +@Preview +@Composable +fun BallotCardPreview() { + val ballot = + BallotEvent( + id = "78f5f012a9518a0b39134e659ef180eae6df9f6b38ce3b4b86963409837e2776", + pubKey = "caf0b6ef4eb461677ed7bf7b569a0212443d6429f6cc555ca8f00a599e453bb2", + createdAt = 1773998101, + tags = + arrayOf( + arrayOf("election", "spring-2026-council"), + arrayOf("proof_hash", "bb64227a0b57fa6af765881b0952fe9d01a3a18faf52a3e34eeb7a896ee36b95"), + ), + content = + "{\"ballot\":{\"funding_priority\":\"community-grants\",\"audit_policy\":\"every-election\"," + + "\"treasurer\":\"alice\",\"quorum\":\"60%\",\"term_length\":\"1 year\"}}", + sig = "", + ) + + ThemeComparisonColumn { + Column(Modifier.padding(10.dp)) { + BallotCard(ballot) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt new file mode 100644 index 0000000000..468069856b --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt @@ -0,0 +1,531 @@ +/* + * 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.commons.ui.note.types + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.prediction_market +import com.vitorpamplona.amethyst.commons.resources.prediction_market_bet_range +import com.vitorpamplona.amethyst.commons.resources.prediction_market_cancel_reason +import com.vitorpamplona.amethyst.commons.resources.prediction_market_closed_on +import com.vitorpamplona.amethyst.commons.resources.prediction_market_closes_in +import com.vitorpamplona.amethyst.commons.resources.prediction_market_demo +import com.vitorpamplona.amethyst.commons.resources.prediction_market_fee +import com.vitorpamplona.amethyst.commons.resources.prediction_market_max_bet +import com.vitorpamplona.amethyst.commons.resources.prediction_market_min_bet +import com.vitorpamplona.amethyst.commons.resources.prediction_market_more_outcomes +import com.vitorpamplona.amethyst.commons.resources.prediction_market_status_cancelled +import com.vitorpamplona.amethyst.commons.resources.prediction_market_status_closed +import com.vitorpamplona.amethyst.commons.resources.prediction_market_status_open +import com.vitorpamplona.amethyst.commons.resources.prediction_market_status_resolved +import com.vitorpamplona.amethyst.commons.resources.prediction_market_winning_outcome +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle +import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.commons.ui.note.StatusPill +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor +import com.vitorpamplona.amethyst.commons.ui.theme.grayText +import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.ui.theme.redColorOnSecondSurface +import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier +import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder +import com.vitorpamplona.amethyst.commons.ui.theme.warningColorOnSecondSurface +import com.vitorpamplona.amethyst.commons.util.formatGrouped +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketStatus +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** How many outcomes a card lists before collapsing the rest into "+N more". */ +private const val MAX_OUTCOMES = 8 + +private val OutcomeShape = RoundedCornerShape(8.dp) + +/** + * Renders a kind-38000 prediction market (BAO Markets): the question, its status, whether real + * money is at stake, the outcomes with the winner marked once resolved, and when betting closes. + * + * Observed rather than read once: a market is addressable, and its publisher replaces it as it + * moves from open to closed to resolved, so a newer version lands on the same Note. + */ +@Composable +fun RenderPredictionMarket( + note: Note, + makeItShort: Boolean, + accountViewModel: AccountViewModel, +) { + val observedEvent by observeNoteEvent(note, accountViewModel) + val noteEvent = observedEvent ?: return + + PredictionMarketCard(noteEvent, makeItShort) +} + +/** Everything the card shows, read off the event once per event rather than on every recomposition. */ +@Immutable +private class PredictionMarketCardState( + val title: String?, + val description: String?, + val outcomes: ImmutableList, + val status: PredictionMarketStatus?, + val resolution: String?, + val endsAt: Long?, + val category: String?, + val isDemo: Boolean, + /** Bet limits and fee, already formatted for display. */ + val minBetSats: String?, + val maxBetSats: String?, + val feePercent: String?, + val cancelReason: String?, +) { + fun isWinner(outcome: String) = resolution != null && outcome.equals(resolution, ignoreCase = true) + + companion object { + fun from( + event: PredictionMarketEvent, + now: Long, + ) = PredictionMarketCardState( + title = event.title() ?: socialPostHeadline(event.content), + description = event.description(), + outcomes = event.outcomes().toImmutableList(), + status = event.status(now), + resolution = event.resolution(), + endsAt = event.endsAt(), + category = event.category()?.let(::humanizeCategory), + isDemo = event.isDemo(), + minBetSats = event.minBetSats()?.let(::formatGrouped), + maxBetSats = event.maxBetSats()?.let(::formatGrouped), + feePercent = event.feePercent()?.let(::formatPercent), + cancelReason = event.cancelReason(), + ) + + /** The first line of a social-post `content`, for a market with no title anywhere else. */ + private fun socialPostHeadline(content: String): String? { + if (content.trimStart().startsWith("{")) return null + return content.lineSequence().firstOrNull { it.isNotBlank() }?.trim() + } + + /** 4.21 → "4.21%", 2.0 → "2%". */ + private fun formatPercent(value: Double): String { + val asLong = value.toLong() + return if (value == asLong.toDouble()) "$asLong%" else "$value%" + } + + private fun humanizeCategory(category: String): String = + category + .replace('-', ' ') + .replace('_', ' ') + .replaceFirstChar { if (it.isLowerCase()) it.titlecaseChar() else it } + } +} + +@Composable +fun PredictionMarketCard( + event: PredictionMarketEvent, + makeItShort: Boolean, +) { + val state = remember(event) { PredictionMarketCardState.from(event, TimeUtils.now()) } + + Column(MaterialTheme.colorScheme.replyModifier) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + MarketKicker(state.status) + + state.title?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + ) + } + + if (state.isDemo || state.category != null) { + MarketTags(state.isDemo, state.category) + } + + state.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.grayText, + maxLines = if (makeItShort) 3 else 8, + overflow = TextOverflow.Ellipsis, + ) + } + + if (state.outcomes.isNotEmpty()) { + MarketOutcomes(state) + } + + if (state.status == PredictionMarketStatus.CANCELLED) { + state.cancelReason?.let { CancelReason(it) } + } else { + state.endsAt?.let { ClosingTime(it, state.status) } + } + + MarketTerms(state.minBetSats, state.maxBetSats, state.feePercent) + } + } +} + +@Composable +private fun MarketKicker(status: PredictionMarketStatus?) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + symbol = MaterialSymbols.AutoMirrored.ShowChart, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp), + ) + Text( + text = stringRes(Res.string.prediction_market), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + modifier = Modifier.padding(start = 6.dp).weight(1f), + ) + status?.let { MarketStatusPill(it) } + } +} + +@Composable +private fun MarketStatusPill(status: PredictionMarketStatus) { + when (status) { + PredictionMarketStatus.OPEN -> { + StatusPill( + label = stringRes(Res.string.prediction_market_status_open), + symbol = MaterialSymbols.RadioButtonChecked, + container = Color(0xFF1F883D), + content = Color.White, + ) + } + + PredictionMarketStatus.CLOSED -> { + StatusPill( + label = stringRes(Res.string.prediction_market_status_closed), + symbol = MaterialSymbols.Lock, + container = MaterialTheme.colorScheme.surfaceVariant, + content = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + PredictionMarketStatus.RESOLVED -> { + StatusPill( + label = stringRes(Res.string.prediction_market_status_resolved), + symbol = MaterialSymbols.CheckCircle, + container = MaterialTheme.colorScheme.primary, + content = MaterialTheme.colorScheme.onPrimary, + ) + } + + PredictionMarketStatus.CANCELLED -> { + StatusPill( + label = stringRes(Res.string.prediction_market_status_cancelled), + symbol = MaterialSymbols.Block, + container = Color(0xFFCF222E), + content = Color.White, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun MarketTags( + isDemo: Boolean, + category: String?, +) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (isDemo) { + val warning = MaterialTheme.colorScheme.warningColorOnSecondSurface + StatusPill( + label = stringRes(Res.string.prediction_market_demo), + symbol = MaterialSymbols.Science, + container = warning.copy(alpha = 0.16f), + content = warning, + ) + } + category?.let { + HeaderPill(symbol = MaterialSymbols.Tag, text = it) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun MarketOutcomes(state: PredictionMarketCardState) { + val shown = if (state.outcomes.size > MAX_OUTCOMES) state.outcomes.subList(0, MAX_OUTCOMES) else state.outcomes + val hidden = state.outcomes.size - shown.size + // A cancelled market has no winner and takes no bets: its outcomes are shown, but muted. + val muted = state.status == PredictionMarketStatus.CANCELLED + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + shown.forEach { outcome -> + OutcomeChip(outcome, isWinner = state.isWinner(outcome), muted = muted) + } + if (hidden > 0) { + Text( + text = pluralStringRes(Res.plurals.prediction_market_more_outcomes, hidden, hidden), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.align(Alignment.CenterVertically), + ) + } + } +} + +@Composable +private fun OutcomeChip( + outcome: String, + isWinner: Boolean, + muted: Boolean, +) { + val success = MaterialTheme.colorScheme.allGoodColor + val borderColor = if (isWinner) success else MaterialTheme.colorScheme.subtleBorder + val textColor = + when { + isWinner -> success + muted -> MaterialTheme.colorScheme.placeholderText + else -> MaterialTheme.colorScheme.onSurface + } + + Row( + modifier = + Modifier + .clip(OutcomeShape) + .background(if (isWinner) success.copy(alpha = 0.14f) else Color.Transparent) + .border(1.dp, borderColor, OutcomeShape) + .padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (isWinner) { + Icon( + symbol = MaterialSymbols.CheckCircle, + contentDescription = stringRes(Res.string.prediction_market_winning_outcome), + tint = success, + filled = true, + modifier = Modifier.size(14.dp), + ) + } + Text( + text = outcome, + style = MaterialTheme.typography.labelLarge, + fontWeight = if (isWinner) FontWeight.Bold else FontWeight.Medium, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun ClosingTime( + endsAt: Long, + status: PredictionMarketStatus?, +) { + val stillOpen = status == PredictionMarketStatus.OPEN && endsAt > TimeUtils.now() + val text = + if (stillOpen) { + stringRes(Res.string.prediction_market_closes_in, timeAheadNoDot(endsAt)) + } else { + val date = remember(endsAt) { formatDateTime(endsAt * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.NONE) } + stringRes(Res.string.prediction_market_closed_on, date) + } + + MarketFootnote( + symbol = if (stillOpen) MaterialSymbols.Schedule else MaterialSymbols.EventAvailable, + text = text, + color = MaterialTheme.colorScheme.placeholderText, + ) +} + +@Composable +private fun CancelReason(reason: String) { + MarketFootnote( + symbol = MaterialSymbols.Block, + text = stringRes(Res.string.prediction_market_cancel_reason, reason), + color = MaterialTheme.colorScheme.redColorOnSecondSurface, + ) +} + +@Composable +private fun MarketTerms( + minBetSats: String?, + maxBetSats: String?, + feePercent: String?, +) { + val limits = + when { + minBetSats != null && maxBetSats != null -> { + stringRes(Res.string.prediction_market_bet_range, minBetSats, maxBetSats) + } + + minBetSats != null -> { + stringRes(Res.string.prediction_market_min_bet, minBetSats) + } + + maxBetSats != null -> { + stringRes(Res.string.prediction_market_max_bet, maxBetSats) + } + + else -> { + null + } + } + val fee = feePercent?.let { stringRes(Res.string.prediction_market_fee, it) } + + if (limits == null && fee == null) return + + MarketFootnote( + symbol = MaterialSymbols.Paid, + text = listOfNotNull(limits, fee).joinToString(" · "), + color = MaterialTheme.colorScheme.placeholderText, + ) +} + +@Composable +private fun MarketFootnote( + symbol: MaterialSymbol, + text: String, + color: Color, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + symbol = symbol, + contentDescription = null, + tint = color, + modifier = Modifier.size(14.dp), + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + color = color, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 6.dp), + ) + } +} + +@Preview +@Composable +fun PredictionMarketCardPreview() { + val resolved = + PredictionMarketEvent( + id = "3de5a164159cd7b3925c3d7aa16a5c32f8f01e50d296e2a3821bfbf5c22db82e", + pubKey = "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", + createdAt = 1778605676, + tags = + arrayOf( + arrayOf("d", "caec08eca65442725a5bd8f77e6d2666"), + arrayOf("market", "caec08eca65442725a5bd8f77e6d2666"), + arrayOf("category", "ai-llm"), + arrayOf("status", "resolved"), + arrayOf("end", "1777680000"), + arrayOf("network", "demo"), + arrayOf("fee_percent", "4.21"), + arrayOf("min_bet", "100"), + arrayOf("max_bet", "100000"), + arrayOf("outcome", "YES"), + arrayOf("outcome", "NO"), + arrayOf("title", "Will an AI coding tool appear in GitHub top trending repos today?"), + arrayOf("resolution", "NO"), + ), + content = "", + sig = "", + ) + + val open = + PredictionMarketEvent( + id = "31da397fdc3a5c61aaa3fc12e8413279f99175ecdba65443d0a2e2495d73f5df", + pubKey = "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", + createdAt = 1786702119, + tags = + arrayOf( + arrayOf("d", "181f3a7acd28280a49e81bf70ec22998"), + arrayOf("market", "181f3a7acd28280a49e81bf70ec22998"), + arrayOf("status", "active"), + arrayOf("category", "mining"), + arrayOf("end", (TimeUtils.now() + TimeUtils.ONE_DAY * 3).toString()), + arrayOf("network", "mainnet"), + arrayOf("outcome", "Foundry USA"), + arrayOf("outcome", "AntPool"), + arrayOf("outcome", "F2Pool"), + arrayOf("outcome", "ViaBTC"), + arrayOf("outcome", "Other"), + arrayOf("title", "Which mining pool will mine the most Bitcoin blocks today?"), + ), + content = "{\"description\":\"Resolves based on the total block count at midnight UTC.\"}", + sig = "", + ) + + ThemeComparisonColumn { + Column(Modifier.padding(10.dp)) { + PredictionMarketCard(resolved, makeItShort = true) + PredictionMarketCard(open, makeItShort = true) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotAnswer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotAnswer.kt new file mode 100644 index 0000000000..d1d45d920a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotAnswer.kt @@ -0,0 +1,30 @@ +/* + * 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.quartz.experimental.ballots + +import androidx.compose.runtime.Immutable + +/** One answer on a ballot: the [question] (as the ballot names it, often an id) and the [answer] chosen. */ +@Immutable +data class BallotAnswer( + val question: String, + val answer: String, +) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotContent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotContent.kt new file mode 100644 index 0000000000..522977068f --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotContent.kt @@ -0,0 +1,100 @@ +/* + * 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.quartz.experimental.ballots + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * The JSON `content` of a [BallotEvent]. Three shapes are seen in the wild, tried in order: + * + * - `responses`: `[{question_id, value}]`, one entry per question; a value may be a string, a + * number or a boolean. + * - `ballot`: an object mapping each question to its choice. + * - `vote_choice`: a single choice with no question, read as [BallotEvent.VOTE_QUESTION]. + * + * Values that are objects, arrays or null are skipped rather than stringified. + */ +@Immutable +class BallotContent( + val answers: List, + val proofHash: String?, +) { + companion object { + /** Parses [json], or returns null when it is not a JSON object. Never throws. */ + fun parse(json: String): BallotContent? { + if (!looksLikeJsonObject(json)) return null + + val obj = + try { + Json.parseToJsonElement(json) as? JsonObject + } catch (_: Exception) { + null + } ?: return null + + return BallotContent( + answers = answers(obj), + proofHash = (obj["proof_hash"] as? JsonPrimitive)?.takeIf { it.isString }?.content?.ifBlank { null }, + ) + } + + private fun looksLikeJsonObject(text: String): Boolean { + for (c in text) { + if (!c.isWhitespace()) return c == '{' + } + return false + } + + private fun answers(obj: JsonObject): List { + val responses = obj["responses"] as? JsonArray + if (responses != null) { + return responses.mapNotNull { response -> + val entry = response as? JsonObject ?: return@mapNotNull null + val question = scalar(entry["question_id"]) ?: scalar(entry["question"]) ?: return@mapNotNull null + val answer = scalar(entry["value"]) ?: return@mapNotNull null + BallotAnswer(question, answer) + } + } + + val ballot = obj["ballot"] as? JsonObject + if (ballot != null) { + return ballot.mapNotNull { (question, choice) -> + scalar(choice)?.let { BallotAnswer(question, it) } + } + } + + return scalar(obj["vote_choice"])?.let { listOf(BallotAnswer(BallotEvent.VOTE_QUESTION, it)) } ?: emptyList() + } + + /** A string, number or boolean as text; null for anything else, and for blank strings. */ + private fun scalar(element: JsonElement?): String? = + if (element is JsonPrimitive && element !is JsonNull) { + element.content.ifBlank { null } + } else { + null + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEvent.kt new file mode 100644 index 0000000000..9b6a5a884e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEvent.kt @@ -0,0 +1,86 @@ +/* + * 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.quartz.experimental.ballots + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent + +/** + * A ballot cast in an auditable-voting app (kind 38000). + * + * App-specific and **not** defined by any NIP: kind 38000 is NIP-87's mint recommendation, and + * the voting app reuses the number. [com.vitorpamplona.quartz.utils.EventFactory] tells them + * apart by the `election` tag ([isBallot]). Amethyst only reads these — it never publishes them. + * + * The schema is derived from events seen in the wild: an `election` tag naming the election, an + * optional proof hash (`proof-hash` or `proof_hash` tag, or `proof_hash` in the content), and the + * choices as JSON `content` in one of three shapes (see [BallotContent]). The content is parsed at + * most once per event, lazily, and a malformed blob just reads as a ballot with no answers. + */ +@Immutable +class BallotEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + SearchableEvent { + private val parsedContent by lazy { BallotContent.parse(content) } + + override fun indexableContent() = + buildList { + election()?.let { add(it) } + answers().forEach { add(it.answer) } + }.joinToString("\n") + + // The read path: the same fields indexableContent() joins, handed over without + // building the joined string a scan would throw away. + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + if (!visitor.visit(election())) return + for (answer in answers()) { + if (!visitor.visit(answer.answer)) return + } + } + + fun election() = tags.election() + + /** The ballot's answers in the order they were cast; empty when the content says nothing readable. */ + fun answers(): List = parsedContent?.answers ?: emptyList() + + fun proofHash() = tags.proofHash() ?: parsedContent?.proofHash + + companion object { + const val KIND = 38000 + + /** The question a lone `vote_choice` answers; the UI shows its own label for it. */ + const val VOTE_QUESTION = "Vote" + + /** True when a kind-38000 event is a ballot: it names the `election` it is cast in. */ + fun isBallot(tags: TagArray): Boolean = tags.fastAny { it.size > 1 && it[0] == "election" && it[1].isNotBlank() } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/TagArrayExt.kt new file mode 100644 index 0000000000..5664964db1 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ballots/TagArrayExt.kt @@ -0,0 +1,32 @@ +/* + * 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.quartz.experimental.ballots + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull + +private fun TagArray.firstNonBlankValue(name: String): String? = fastFirstNotNullOfOrNull { if (it.size > 1 && it[0] == name && it[1].isNotBlank()) it[1] else null } + +/** The election this ballot is cast in. */ +fun TagArray.election() = firstNonBlankValue("election") + +/** The ballot's proof hash, from either spelling of the tag (`proof-hash` or `proof_hash`). */ +fun TagArray.proofHash() = firstNonBlankValue("proof-hash") ?: firstNonBlankValue("proof_hash") diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt new file mode 100644 index 0000000000..b1a72b6463 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt @@ -0,0 +1,118 @@ +/* + * 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.quartz.experimental.predictionMarkets + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.longOrNull + +/** + * The market fields BAO Markets writes as JSON — in the `data` tag of its current shape, or as the + * whole `content` of its BAO Fund and first shapes. Every field is optional: the shapes disagree + * on which ones they carry, and nothing here is trusted to be well-formed. + */ +@Immutable +class PredictionMarketDetails( + val title: String?, + val description: String?, + val outcomes: List, + val category: String?, + val status: String?, + val reason: String?, + val resolution: String?, + val endDate: Long?, + val resolutionSource: String?, + val minBetSats: Long?, + val maxBetSats: Long?, + val feePercent: Double?, +) { + companion object { + /** Parses [json] into details, or null when it is not a JSON object. Never throws. */ + fun parse(json: String?): PredictionMarketDetails? { + if (json == null || !looksLikeJsonObject(json)) return null + + val obj = + try { + Json.parseToJsonElement(json) as? JsonObject + } catch (_: Exception) { + null + } ?: return null + + return PredictionMarketDetails( + title = obj.text("title") ?: obj.text("question"), + description = obj.text("description"), + outcomes = obj.outcomes(), + category = obj.text("category"), + status = obj.text("status"), + reason = obj.text("reason"), + resolution = obj.text("resolution"), + endDate = obj.long("endDate") ?: obj.long("end"), + resolutionSource = obj.text("resolutionSource"), + minBetSats = obj.long("minBetSats"), + maxBetSats = obj.long("maxBetSats"), + feePercent = obj.double("feePercent"), + ) + } + + /** Skips the parser for the social-post `content` of the current shape: JSON objects start with `{`. */ + private fun looksLikeJsonObject(text: String): Boolean { + for (c in text) { + if (!c.isWhitespace()) return c == '{' + } + return false + } + + private fun JsonObject.primitive(key: String): JsonPrimitive? = this[key] as? JsonPrimitive + + /** A string field, or null when absent, blank, null or not a string. */ + private fun JsonObject.text(key: String): String? = primitive(key)?.takeIf { it.isString }?.content?.ifBlank { null } + + private fun JsonObject.long(key: String): Long? = primitive(key)?.longOrNull + + private fun JsonObject.double(key: String): Double? = primitive(key)?.doubleOrNull + + /** + * `outcomes` as display strings: a plain string per outcome (current shape), or an object + * per outcome whose `label` (else `id`, `name`) names it (BAO Fund and first shapes). + */ + private fun JsonObject.outcomes(): List { + val array = this["outcomes"] as? JsonArray ?: return emptyList() + val result = ArrayList(array.size) + for (element in array) { + val name = outcomeName(element) + if (name != null && name !in result) result.add(name) + } + return result + } + + private fun outcomeName(element: JsonElement): String? = + when (element) { + is JsonPrimitive -> element.takeIf { it.isString }?.content?.ifBlank { null } + is JsonObject -> element.text("label") ?: element.text("id") ?: element.text("name") + else -> null + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt new file mode 100644 index 0000000000..5b23c4f6ee --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt @@ -0,0 +1,172 @@ +/* + * 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.quartz.experimental.predictionMarkets + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastForEach +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * A prediction market (kind 38000), as published by BAO Markets. + * + * App-specific and **not** defined by any NIP: kind 38000 is NIP-87's mint recommendation, and + * BAO Markets reuses the number. [com.vitorpamplona.quartz.utils.EventFactory] tells the two + * apart by tags ([isPredictionMarket]). Amethyst only reads these — it never publishes them. + * + * The schema is derived from events seen in the wild, in three shapes: + * + * - **Current**: everything in tags — `market`, `status` (also `state`, `s`), `end`, `network`/`n` + * ("demo" is play money), `category`/`c`, repeated `outcome`, `resolution`, `resolution_source`, + * `min_bet`/`max_bet` (sats), `fee_percent`, `oracle`, `cancel_reason`, `client`, and a `data` + * tag holding JSON `{title, description, outcomes, …}`. `content` is a human-readable social + * post of the same words. + * - **BAO Fund**: a `title` tag, `c` = bao-fund, `n`, `pool_model`, `end`, `["outcome", id, + * label]` tags, and JSON `content` `{title, description, outcomes: [{id, label}], …}`. + * - **First**: `type`, `category`, `oracle`, `end`, `state`, `platform_fee` tags and JSON + * `content` `{title, description, outcomes: [{id, label, probability}], …}`. + * + * Tags win over JSON wherever both carry a field. The JSON is parsed at most once per event, + * lazily, and a malformed blob just reads as absent. + */ +@Immutable +class PredictionMarketEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + SearchableEvent { + private val dataDetails by lazy { PredictionMarketDetails.parse(tags.marketData()) } + + private val contentDetails by lazy { PredictionMarketDetails.parse(content) } + + override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n") + + // The read path: the same fields indexableContent() joins, handed over without + // building the joined string a scan would throw away. + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + if (!visitor.visit(title())) return + visitor.visit(description()) + } + + fun marketId() = tags.marketId() + + /** The question being predicted. */ + fun title() = dataDetails?.title ?: tags.marketTitle() ?: contentDetails?.title + + fun description() = dataDetails?.description ?: contentDetails?.description + + /** The outcomes one can bet on: the `outcome` tags, else the JSON `outcomes`. */ + fun outcomes(): List = + tags.marketOutcomes().ifEmpty { + dataDetails?.outcomes?.ifEmpty { null } ?: contentDetails?.outcomes ?: emptyList() + } + + /** The winning outcome, once resolved. */ + fun resolution() = tags.marketResolution() ?: dataDetails?.resolution ?: contentDetails?.resolution + + /** When betting closes, unix seconds. */ + fun endsAt() = tags.marketEnd() ?: dataDetails?.endDate ?: contentDetails?.endDate + + fun category() = tags.marketCategory() ?: dataDetails?.category ?: contentDetails?.category + + /** `demo` (play money, most markets), `mainnet`, or whatever else the publisher names. */ + fun network() = tags.marketNetwork() + + /** True for play-money markets: nothing real is at stake. */ + fun isDemo() = network().equals(DEMO_NETWORK, ignoreCase = true) + + fun resolutionSource() = tags.marketResolutionSource() ?: dataDetails?.resolutionSource ?: contentDetails?.resolutionSource + + fun minBetSats() = tags.marketMinBet() ?: dataDetails?.minBetSats ?: contentDetails?.minBetSats + + fun maxBetSats() = tags.marketMaxBet() ?: dataDetails?.maxBetSats ?: contentDetails?.maxBetSats + + fun feePercent() = tags.marketFeePercent() ?: dataDetails?.feePercent ?: contentDetails?.feePercent + + fun cancelReason() = tags.marketCancelReason() ?: contentDetails?.reason + + fun oracle() = tags.marketOracle() + + fun client() = tags.marketClient() + + /** + * The market's status at [now] (unix seconds), or null when nothing says. + * + * The declared `status`/`state`/`s` word decides when it is one we know; a declared RESOLVED + * or CANCELLED is final. Otherwise the event's own evidence refines it: a `resolution` means + * RESOLVED, a `cancel_reason` on an undeclared market means CANCELLED, and an open market + * whose `end` has passed is CLOSED — publishers leave "active" in place long after betting + * stops. + */ + fun status(now: Long = TimeUtils.now()): PredictionMarketStatus? { + val declared = declaredStatus() + if (declared == PredictionMarketStatus.RESOLVED || declared == PredictionMarketStatus.CANCELLED) return declared + if (resolution() != null) return PredictionMarketStatus.RESOLVED + if (declared == PredictionMarketStatus.CLOSED) return declared + if (declared == null && cancelReason() != null) return PredictionMarketStatus.CANCELLED + + val end = endsAt() + return when { + end != null -> if (end > now) PredictionMarketStatus.OPEN else PredictionMarketStatus.CLOSED + else -> declared + } + } + + private fun declaredStatus(): PredictionMarketStatus? = + tags.marketStatusValues().firstNotNullOfOrNull(PredictionMarketStatus::parse) + ?: contentDetails?.status?.let(PredictionMarketStatus::parse) + + companion object { + const val KIND = 38000 + const val DEMO_NETWORK = "demo" + + /** + * True when a kind-38000 event is shaped like a prediction market: it names a `market`, + * offers at least two `outcome`s, or declares a market `type` with an `end`. Checked + * after [com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent.isMintRecommendation] + * and [com.vitorpamplona.quartz.experimental.ballots.BallotEvent.isBallot]. + */ + fun isPredictionMarket(tags: TagArray): Boolean { + var outcomes = 0 + var hasType = false + var hasEnd = false + tags.fastForEach { + if (it.size > 1) { + when (it[0]) { + "market" -> if (it[1].isNotBlank()) return true + "outcome" -> outcomes++ + "type" -> hasType = true + "end" -> hasEnd = true + } + } + } + return outcomes >= 2 || (hasType && hasEnd) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketStatus.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketStatus.kt new file mode 100644 index 0000000000..44c595f070 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketStatus.kt @@ -0,0 +1,49 @@ +/* + * 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.quartz.experimental.predictionMarkets + +/** Where a prediction market is in its life, collapsed from the many words publishers use for it. */ +enum class PredictionMarketStatus { + /** Taking bets (or, before that, being funded). */ + OPEN, + + /** No longer taking bets, not resolved yet. */ + CLOSED, + + /** The oracle has named a winning outcome. */ + RESOLVED, + + /** Voided: bets are refunded and no outcome wins. */ + CANCELLED, + ; + + companion object { + /** Maps a published `status`/`state` word to a [PredictionMarketStatus], or null when it is not one we know. */ + fun parse(value: String): PredictionMarketStatus? = + when (value.trim().lowercase()) { + "active", "open", "funding" -> OPEN + "resolving", "ended", "closed" -> CLOSED + "resolved", "settled" -> RESOLVED + "voided", "void", "cancelled", "canceled" -> CANCELLED + else -> null + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt new file mode 100644 index 0000000000..c4cdd43c4a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt @@ -0,0 +1,88 @@ +/* + * 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.quartz.experimental.predictionMarkets + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.core.fastForEach + +/** The first non-blank value of a tag named [name], or null. */ +private fun TagArray.firstNonBlankValue(name: String): String? = fastFirstNotNullOfOrNull { if (it.size > 1 && it[0] == name && it[1].isNotBlank()) it[1] else null } + +/** The first non-blank value among [first] and then [second], in that priority order. */ +private fun TagArray.firstNonBlankValue( + first: String, + second: String, +): String? = firstNonBlankValue(first) ?: firstNonBlankValue(second) + +fun TagArray.marketId() = firstNonBlankValue("market") + +fun TagArray.marketTitle() = firstNonBlankValue("title") + +/** The JSON blob of the `data` tag BAO Markets attaches to its current event shape. */ +fun TagArray.marketData() = firstNonBlankValue("data") + +fun TagArray.marketType() = firstNonBlankValue("type") + +/** + * The declared lifecycle state, tried by tag name in priority order: `status` first, then the + * older `state` and the one-letter `s`. BAO Markets writes all three but lets them drift apart + * (an "active" status next to an "ended" state), and `status` is the one it keeps current. + */ +fun TagArray.marketStatusValues(): List = listOfNotNull(firstNonBlankValue("status"), firstNonBlankValue("state"), firstNonBlankValue("s")) + +/** When betting closes, unix seconds. */ +fun TagArray.marketEnd() = firstNonBlankValue("end")?.trim()?.toLongOrNull() + +fun TagArray.marketNetwork() = firstNonBlankValue("network", "n") + +fun TagArray.marketCategory() = firstNonBlankValue("category", "c") + +/** + * Every `outcome` tag's value, in order. The BAO Fund shape writes `["outcome", id, label]`; + * the id is what a `resolution` names, so it is the one returned. + */ +fun TagArray.marketOutcomes(): List { + val result = ArrayList(2) + fastForEach { + if (it.size > 1 && it[0] == "outcome" && it[1].isNotBlank() && it[1] !in result) result.add(it[1]) + } + return result +} + +/** The winning outcome, once the market is resolved. */ +fun TagArray.marketResolution() = firstNonBlankValue("resolution") + +fun TagArray.marketResolutionSource() = firstNonBlankValue("resolution_source") + +fun TagArray.marketMinBet() = firstNonBlankValue("min_bet")?.trim()?.toLongOrNull() + +fun TagArray.marketMaxBet() = firstNonBlankValue("max_bet")?.trim()?.toLongOrNull() + +fun TagArray.marketFeePercent() = firstNonBlankValue("fee_percent")?.trim()?.toDoubleOrNull() + +fun TagArray.marketOracle() = firstNonBlankValue("oracle") + +fun TagArray.marketCancelReason() = firstNonBlankValue("cancel_reason") + +fun TagArray.marketPoolModel() = firstNonBlankValue("pool_model") + +fun TagArray.marketClient() = firstNonBlankValue("client") diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt index b098e30d02..ed1735b0a1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt @@ -521,8 +521,9 @@ class FullTextSearchModule( /** * The distinct kinds present in `event_headers` that currently parse - * to a [SearchableEvent]. Kind alone selects the event class in - * [EventFactory], so one probe per distinct kind is authoritative; + * to a [SearchableEvent]. One [EventFactory.probe] per distinct kind + * decides (a kind whose class is picked by tags answers as its + * primary class; the insert path still checks each event itself); * the result drives a `kind IN (...)` filter on the rebuild scan so * non-searchable rows are skipped at the SQL layer. */ @@ -537,15 +538,9 @@ class FullTextSearchModule( return out } - private fun isSearchableKind(kind: Int): Boolean = EventFactory.create(PROBE_ID, PROBE_ID, 0L, kind, EMPTY_TAGS, "", "") is SearchableEvent + private fun isSearchableKind(kind: Int): Boolean = EventFactory.probe(kind) is SearchableEvent companion object { - // A non-blank id keeps kinds that lazily hash a missing id (e.g. - // NIP-17 chat messages) from doing that work — the probe only - // inspects the resulting runtime type. - private const val PROBE_ID = "0" - private val EMPTY_TAGS = emptyArray>() - /** Column order matches the positional `read.getText(1)`…`getText(7)` event rebuilds. */ private const val SELECT_EVENT_COLUMNS = "SELECT row_id, id, pubkey, created_at, kind, tags, content, sig FROM event_headers" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt index 467e9a2fd6..962b0142ed 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt @@ -23,10 +23,14 @@ package com.vitorpamplona.quartz.nip87Ecash.recommendation import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.core.fastFirstOrNull import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -62,6 +66,29 @@ class MintRecommendationEvent( companion object { const val KIND = 38000 + /** + * True when a kind-38000 event is actually a NIP-87 mint recommendation. + * + * Kind 38000 is shared with unrelated apps (prediction markets, ballots, spam votes), so + * the kind number alone does not say what an event is. A recommendation names the kind of + * the mint it recommends in its `k` tag — 38172 (Cashu) or 38173 (Fedimint). Some early + * recommendations carry no `k` at all; those are accepted when they point at a mint URL + * (`u`). A `k` naming anything else is not a mint recommendation, whatever else it carries. + */ + fun isMintRecommendation(tags: TagArray): Boolean { + val kTag = tags.fastFirstOrNull { it.size > 1 && it[0] == "k" } + if (kTag != null) { + val mintKind = kTag[1].toIntOrNull() + return mintKind == CashuMintEvent.KIND || mintKind == FedimintEvent.KIND + } + return tags.fastAny { it.size > 1 && it[0] == MintUrlTag.TAG_NAME && it[1].isNotBlank() } + } + + /** + * [mintKind] must be [CashuMintEvent.KIND] or [FedimintEvent.KIND]: the `k` tag it writes is + * what [isMintRecommendation] reads back, so any other value signs into a plain kind-38000 + * event rather than a [MintRecommendationEvent]. + */ fun build( mintIdentifier: String, mintKind: Int, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index 8e42496847..c3d7d2969b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -124,6 +124,7 @@ import com.vitorpamplona.quartz.experimental.attestations.recommendation.Attesto import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent @@ -160,6 +161,7 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent @@ -755,7 +757,20 @@ class EventFactory { MeetingRoomPresenceEvent.KIND -> MeetingRoomPresenceEvent(id, pubKey, createdAt, tags, content, sig) MeetingSpaceEvent.KIND -> MeetingSpaceEvent(id, pubKey, createdAt, tags, content, sig) AdminCommandEvent.KIND -> AdminCommandEvent(id, pubKey, createdAt, tags, content, sig) - MintRecommendationEvent.KIND -> MintRecommendationEvent(id, pubKey, createdAt, tags, content, sig) + // kind:38000 is NIP-87's mint recommendation, but unrelated apps reuse the number: + // BAO Markets publishes prediction markets on it, an auditable-voting app publishes + // ballots, and most of what a big relay holds for it is spam (`d`-only "sybil test + // votes"). Parsing all of it as a recommendation rendered markets, ballots and spam + // as ecash mint cards. Disambiguate by tags, recommendation first — its `k` (the + // recommended mint's kind) or, on old events without one, its `u` (mint URL) — and + // fall back to a plain Event so junk gets no card and no search index entry. + MintRecommendationEvent.KIND -> + when { + MintRecommendationEvent.isMintRecommendation(tags) -> MintRecommendationEvent(id, pubKey, createdAt, tags, content, sig) + BallotEvent.isBallot(tags) -> BallotEvent(id, pubKey, createdAt, tags, content, sig) + PredictionMarketEvent.isPredictionMarket(tags) -> PredictionMarketEvent(id, pubKey, createdAt, tags, content, sig) + else -> Event(id, pubKey, createdAt, kind, tags, content, sig) + } MediaFollowListEvent.KIND -> MediaFollowListEvent(id, pubKey, createdAt, tags, content, sig) MediaStarterPackEvent.KIND -> MediaStarterPackEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) @@ -940,6 +955,29 @@ class EventFactory { * Used to decide whether a repost's inner (boosted) kind is something * Amethyst can parse and render at all. */ - fun isKnownKind(kind: Int): Boolean = create("", "", 0L, kind, emptyArray(), "", "")::class != Event::class + fun isKnownKind(kind: Int): Boolean = probe(kind)::class != Event::class + + /** + * A tagless, contentless instance of [kind], for questions asked of a kind rather than of + * an event: does it have a typed class ([isKnownKind]), is it searchable (the stores' + * reindex pre-filters, `SearchableKinds`)? + * + * For most kinds that is just [create] with no tags. It is not for kinds whose class is + * chosen by tags, where a tagless event can land on a different class than the kind's + * real events. Kind 38000 is the case that matters: with no tags it is a plain [Event] + * (junk), yet every typed shape of it — mint recommendation, ballot, prediction market — + * is a searchable, renderable class, so it answers as its primary class. (The other + * tag-split kinds, 39005 and 20001, already land on a typed class with no tags.) + * + * The id is non-blank so kinds that lazily hash a missing id (NIP-17 chat) skip that work + * — only the runtime type matters here. + */ + fun probe(kind: Int): Event = + when (kind) { + MintRecommendationEvent.KIND -> MintRecommendationEvent(PROBE_ID, PROBE_ID, 0L, emptyArray(), "", "") + else -> create(PROBE_ID, PROBE_ID, 0L, kind, emptyArray(), "", "") + } + + private const val PROBE_ID = "0" } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEventTest.kt new file mode 100644 index 0000000000..87529c7bea --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ballots/BallotEventTest.kt @@ -0,0 +1,137 @@ +/* + * 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.quartz.experimental.ballots + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.utils.Kind38000Fixtures +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull + +class BallotEventTest { + private fun parse(json: String) = assertIs(Event.fromJson(json)) + + private fun ballotWith( + content: String, + vararg extraTags: Array, + ) = BallotEvent("0".repeat(64), "1".repeat(64), 1L, arrayOf(arrayOf("election", "e1"), *extraTags), content, "") + + @Test + fun responsesShape() { + val event = parse(Kind38000Fixtures.BALLOT_RESPONSES) + + assertEquals("sec06-feedback-1774311582", event.election()) + val answers = event.answers() + assertEquals(10, answers.size) + assertEquals(BallotAnswer("q1", "Yes"), answers[0]) + // Numbers read as text. + assertEquals(BallotAnswer("q3", "5"), answers[2]) + assertEquals(BallotAnswer("q9", "NA"), answers[8]) + assertNull(event.proofHash()) + } + + @Test + fun responsesWithProofHashTag() { + val event = parse(Kind38000Fixtures.BALLOT_RESPONSES_PROOF_HASH) + + assertEquals("spring-2026-council", event.election()) + assertEquals(listOf(BallotAnswer("proposal_approval", "Yes")), event.answers()) + assertEquals("361689b5ccd44f35eebba995129d8f2b00dd369aca4c51b55f00065b2b477ddd", event.proofHash()) + assertEquals("spring-2026-council\nYes", event.indexableContent()) + } + + @Test + fun ballotObjectShape() { + val event = parse(Kind38000Fixtures.BALLOT_OBJECT) + + assertEquals("spring-2026-council", event.election()) + assertEquals( + listOf( + BallotAnswer("funding_priority", "community-grants"), + BallotAnswer("audit_policy", "every-election"), + ), + event.answers(), + ) + assertEquals("bb64227a0b57fa6af765881b0952fe9d01a3a18faf52a3e34eeb7a896ee36b95", event.proofHash()) + } + + @Test + fun voteChoiceShape() { + val event = parse(Kind38000Fixtures.BALLOT_VOTE_CHOICE) + + assertEquals("election1", event.election()) + assertEquals(listOf(BallotAnswer(BallotEvent.VOTE_QUESTION, "yes")), event.answers()) + assertNull(event.proofHash()) + } + + @Test + fun proofHashFallsBackToContent() { + assertEquals("abc", ballotWith("""{"proof_hash":"abc","vote_choice":"no"}""").proofHash()) + assertEquals("tag", ballotWith("""{"proof_hash":"abc"}""", arrayOf("proof_hash", "tag")).proofHash()) + assertEquals("dash", ballotWith("""{"proof_hash":"abc"}""", arrayOf("proof_hash", "underscore"), arrayOf("proof-hash", "dash")).proofHash()) + } + + @Test + fun nonScalarValuesAreSkipped() { + val responses = + ballotWith( + """{"responses":[1,{"question_id":"q","value":{"x":1}},{"question_id":"q2","value":null},{"value":"orphan"},""" + + """{"question_id":"q3","value":true},{"question_id":4,"value":2.5},{"question_id":"q5","value":" "}]}""", + ) + assertEquals(listOf(BallotAnswer("q3", "true"), BallotAnswer("4", "2.5")), responses.answers()) + + val ballotObject = ballotWith("""{"ballot":{"a":[1],"b":2,"c":null,"d":{"x":"y"},"e":"E"}}""") + assertEquals(listOf(BallotAnswer("b", "2"), BallotAnswer("e", "E")), ballotObject.answers()) + + assertEquals(emptyList(), ballotWith("""{"vote_choice":{"x":1}}""").answers()) + } + + @Test + fun junkJsonNeverThrows() { + listOf("", "not json", "{", "[]", "null", "{\"responses\":", "{\"responses\": \"x\", \"ballot\": 3}", "{\"a\":".repeat(5000)).forEach { + val event = ballotWith(it) + assertEquals(emptyList(), event.answers()) + assertNull(event.proofHash()) + assertEquals("e1", event.indexableContent()) + } + } + + @Test + fun visitorAgreesWithIndexedContentAndStops() { + listOf(Kind38000Fixtures.BALLOT_RESPONSES, Kind38000Fixtures.BALLOT_OBJECT, Kind38000Fixtures.BALLOT_VOTE_CHOICE).forEach { json -> + val event = parse(json) + val visited = mutableListOf() + event.forEachIndexableField { + if (it != null) visited.add(it) + true + } + assertEquals(event.indexableContent(), visited.joinToString(event.indexableSeparator())) + + var seen = 0 + event.forEachIndexableField { + seen++ + false + } + assertEquals(1, seen) + } + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt new file mode 100644 index 0000000000..48eeb74508 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt @@ -0,0 +1,284 @@ +/* + * 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.quartz.experimental.predictionMarkets + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.utils.Kind38000Fixtures +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class PredictionMarketEventTest { + private fun market(json: String) = assertIs(Event.fromJson(json)) + + private fun market( + tags: Array>, + content: String = "", + ) = PredictionMarketEvent("0".repeat(64), "1".repeat(64), 1L, tags, content, "") + + @Test + fun currentShapeResolvedBinary() { + val event = market(Kind38000Fixtures.MARKET_RESOLVED_BINARY) + + assertEquals("caec08eca65442725a5bd8f77e6d2666", event.marketId()) + assertEquals("Will an AI coding tool appear in GitHub top trending repos today? (UTC 2,May,2026)", event.title()) + assertTrue(event.description()!!.startsWith("At least one repository in GitHub's daily trending page")) + assertEquals(listOf("YES", "NO"), event.outcomes()) + assertEquals("NO", event.resolution()) + assertEquals(PredictionMarketStatus.RESOLVED, event.status()) + assertEquals(1777680000L, event.endsAt()) + assertEquals("ai-llm", event.category()) + assertEquals("demo", event.network()) + assertTrue(event.isDemo()) + assertEquals("github.com/trending daily repos", event.resolutionSource()) + assertEquals(100L, event.minBetSats()) + assertEquals(100000L, event.maxBetSats()) + assertEquals(4.21, event.feePercent()) + assertNull(event.cancelReason()) + assertEquals("BAO Markets", event.client()) + assertEquals("2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", event.oracle()) + assertEquals("${event.title()}\n${event.description()}", event.indexableContent()) + } + + @Test + fun currentShapeActiveMarketClosesAtItsEnd() { + val event = market(Kind38000Fixtures.MARKET_ACTIVE_BINARY) + val end = 1777939200L + + assertEquals(end, event.endsAt()) + assertEquals("bitcoin", event.category()) + assertNull(event.resolution()) + // `status` says "active" (it wins over `state` = "ended")… + assertEquals(PredictionMarketStatus.OPEN, event.status(now = end - 1)) + // …but once `end` passes, betting is over. + assertEquals(PredictionMarketStatus.CLOSED, event.status(now = end)) + assertEquals(PredictionMarketStatus.CLOSED, event.status()) + assertEquals("https://mempool.space/api/v1/blocks", event.resolutionSource()) + } + + @Test + fun currentShapeVoidedMultiOutcome() { + val event = market(Kind38000Fixtures.MARKET_VOIDED_MULTI_OUTCOME) + + assertEquals(listOf("Foundry USA", "AntPool", "F2Pool", "ViaBTC", "Other"), event.outcomes()) + assertEquals(PredictionMarketStatus.CANCELLED, event.status(now = 0)) + assertEquals("mining", event.category()) + assertTrue(event.title()!!.startsWith("Which mining pool will mine the most Bitcoin blocks today?")) + } + + @Test + fun currentShapeCancelled() { + val event = market(Kind38000Fixtures.MARKET_CANCELLED) + + assertEquals("Will BTC price be above \$60,000 at 2026-08-13 13:00 UTC?", event.title()) + assertNull(event.description()) + assertEquals(emptyList(), event.outcomes()) + assertEquals(PredictionMarketStatus.CANCELLED, event.status()) + assertEquals("bridge-test junk", event.cancelReason()) + assertEquals(1786626000L, event.endsAt()) + assertEquals("bitcoin", event.category()) + assertTrue(event.isDemo()) + assertEquals(event.title(), event.indexableContent()) + } + + @Test + fun baoFundShape() { + val event = market(Kind38000Fixtures.MARKET_BAO_FUND) + val end = 1790727795L + + assertEquals("Will E2E demo-rail liquid refund munbn1yg deliver: E2E milestone munbn1yg by 2026-09-30?", event.title()) + assertTrue(event.description()!!.contains("CRITERIA: E2E milestone munbn1yg")) + assertEquals(listOf("YES", "NO"), event.outcomes()) + assertEquals("bao-fund", event.category()) + assertTrue(event.isDemo()) + assertEquals(end, event.endsAt()) + // No status tag at all: the end decides. + assertEquals(PredictionMarketStatus.OPEN, event.status(now = end - 100)) + assertEquals(PredictionMarketStatus.CLOSED, event.status(now = end + 1)) + assertNull(event.minBetSats()) + assertNull(event.feePercent()) + } + + @Test + fun firstShape() { + val event = market(Kind38000Fixtures.MARKET_FIRST_SHAPE) + val end = 1769256240L + + assertEquals("will trump turn purple before May?", event.title()) + assertEquals("Trump is orange, but when purple? I think he hes been orange for far too long", event.description()) + // Outcome objects read by their label. + assertEquals(listOf("Yes", "No"), event.outcomes()) + assertEquals("nostr", event.category()) + assertEquals("community", event.oracle()) + assertNull(event.network()) + assertFalse(event.isDemo()) + assertEquals(end, event.endsAt()) + // `state` = "funding" is open, until the end passes. + assertEquals(PredictionMarketStatus.OPEN, event.status(now = end - 1)) + assertEquals(PredictionMarketStatus.CLOSED, event.status(now = end + 1)) + } + + @Test + fun statusWords() { + val expected = + mapOf( + "active" to PredictionMarketStatus.OPEN, + "Open" to PredictionMarketStatus.OPEN, + "funding" to PredictionMarketStatus.OPEN, + "resolving" to PredictionMarketStatus.CLOSED, + "ended" to PredictionMarketStatus.CLOSED, + "closed" to PredictionMarketStatus.CLOSED, + "resolved" to PredictionMarketStatus.RESOLVED, + "settled" to PredictionMarketStatus.RESOLVED, + "voided" to PredictionMarketStatus.CANCELLED, + "cancelled" to PredictionMarketStatus.CANCELLED, + " CANCELED " to PredictionMarketStatus.CANCELLED, + ) + expected.forEach { (word, status) -> assertEquals(status, PredictionMarketStatus.parse(word), word) } + assertNull(PredictionMarketStatus.parse("pending")) + assertNull(PredictionMarketStatus.parse("")) + } + + @Test + fun statusFallbacks() { + // No status at all, no end: unknown. + assertNull(market(arrayOf(arrayOf("market", "m"))).status(now = 10)) + // An unknown word falls through to the next tag name. + assertEquals( + PredictionMarketStatus.RESOLVED, + market(arrayOf(arrayOf("market", "m"), arrayOf("status", "weird"), arrayOf("state", "resolved"))).status(now = 10), + ) + // A resolution resolves an undeclared or still-open market. + assertEquals(PredictionMarketStatus.RESOLVED, market(arrayOf(arrayOf("market", "m"), arrayOf("resolution", "YES"))).status(now = 10)) + assertEquals( + PredictionMarketStatus.RESOLVED, + market(arrayOf(arrayOf("market", "m"), arrayOf("status", "active"), arrayOf("resolution", "YES"))).status(now = 10), + ) + // …but never un-cancels one. + assertEquals( + PredictionMarketStatus.CANCELLED, + market(arrayOf(arrayOf("market", "m"), arrayOf("status", "voided"), arrayOf("resolution", "YES"))).status(now = 10), + ) + // A cancel reason on an undeclared market cancels it. + assertEquals(PredictionMarketStatus.CANCELLED, market(arrayOf(arrayOf("market", "m"), arrayOf("cancel_reason", "oops"))).status(now = 10)) + // Undeclared: the end decides. + assertEquals(PredictionMarketStatus.OPEN, market(arrayOf(arrayOf("market", "m"), arrayOf("end", "20"))).status(now = 10)) + assertEquals(PredictionMarketStatus.CLOSED, market(arrayOf(arrayOf("market", "m"), arrayOf("end", "20"))).status(now = 20)) + // A status in JSON content counts when no tag declares one. + assertEquals(PredictionMarketStatus.CANCELLED, market(arrayOf(arrayOf("market", "m")), """{"status":"cancelled"}""").status(now = 10)) + } + + @Test + fun tagsWinOverJson() { + val event = + market( + arrayOf( + arrayOf("market", "m"), + arrayOf("title", "From tag"), + arrayOf("min_bet", "5"), + arrayOf("outcome", "A"), + arrayOf("outcome", "B"), + arrayOf("data", """{"title":"From data","minBetSats":7,"maxBetSats":9,"outcomes":["X","Y"]}"""), + ), + """{"title":"From content","description":"Content description"}""", + ) + + // `data` is BAO's canonical blob, so its title beats the tag; the tag beats JSON content. + assertEquals("From data", event.title()) + assertEquals("Content description", event.description()) + assertEquals(5L, event.minBetSats()) + assertEquals(9L, event.maxBetSats()) + assertEquals(listOf("A", "B"), event.outcomes()) + } + + @Test + fun junkJsonNeverThrows() { + val junk = + listOf( + "", + " ", + "not json at all", + "{", + "{\"title\":", + "[1,2,3]", + "null", + "{\"a\":".repeat(5000), + "{\"title\": 5, \"description\": null, \"outcomes\": \"nope\", \"minBetSats\": \"abc\", \"feePercent\": {}}", + "{\"outcomes\": [1, null, [], {\"id\": 7}, {\"label\": \"A\"}, \"B\", \"B\", \" \"]}", + ) + + junk.forEach { json -> + listOf( + market(arrayOf(arrayOf("market", "m"), arrayOf("data", json)), json), + market(arrayOf(arrayOf("type", "binary"), arrayOf("end", "not a number"), arrayOf("min_bet", "x"), arrayOf("fee_percent", "y")), json), + ).forEach { event -> + event.title() + event.description() + event.outcomes() + event.status(now = 10) + event.endsAt() + event.minBetSats() + event.maxBetSats() + event.feePercent() + event.cancelReason() + event.indexableContent() + } + } + + val odd = market(arrayOf(arrayOf("market", "m")), junk.last()) + assertEquals(listOf("A", "B"), odd.outcomes()) + + val typed = market(arrayOf(arrayOf("market", "m"), arrayOf("min_bet", "x")), junk[junk.size - 2]) + assertNull(typed.title()) + assertNull(typed.description()) + assertEquals(emptyList(), typed.outcomes()) + assertNull(typed.minBetSats()) + assertNull(typed.feePercent()) + assertEquals("", typed.indexableContent()) + } + + @Test + fun visitorAgreesWithIndexedContentAndStops() { + listOf( + Kind38000Fixtures.MARKET_RESOLVED_BINARY, + Kind38000Fixtures.MARKET_CANCELLED, + Kind38000Fixtures.MARKET_FIRST_SHAPE, + ).forEach { json -> + val event = market(json) + val visited = mutableListOf() + event.forEachIndexableField { + if (it != null) visited.add(it) + true + } + assertEquals(event.indexableContent(), visited.joinToString(event.indexableSeparator())) + + var seen = 0 + event.forEachIndexableField { + seen++ + false + } + assertEquals(1, seen) + } + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKindsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKindsTest.kt index c23e969128..c2852f1a74 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKindsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKindsTest.kt @@ -37,7 +37,9 @@ import kotlin.test.assertEquals * being left out of everything downstream that reads this list. */ class SearchableKindsTest { - private fun build(kind: Int): Event = EventFactory.create("9".repeat(64), "a".repeat(64), 1L, kind, emptyArray(), "", "") + // Through the factory's kind probe, like the stores' reindex pre-filters: a kind whose class + // is picked by tags (38000) answers as its primary class rather than as a tagless plain Event. + private fun build(kind: Int): Event = EventFactory.probe(kind) @Test fun theRecordedSetIsExactlyWhatTheFactoryBuilds() { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt new file mode 100644 index 0000000000..f622681611 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt @@ -0,0 +1,184 @@ +/* + * 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.quartz.utils + +import com.vitorpamplona.quartz.experimental.ballots.BallotEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.verify +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent +import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent +import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue + +/** + * Kind 38000 is NIP-87's mint recommendation, but prediction markets, ballots and spam share the + * number. The factory must hand each shape its own class, and junk none. + */ +class EventFactoryKind38000Test { + private fun parse(json: String): Event = Event.fromJson(json) + + private fun build(vararg tags: Array): Event = EventFactory.create("0".repeat(64), "1".repeat(64), 1L, 38000, arrayOf(*tags), "", "") + + @Test + fun fixturesAreRealSignedEvents() { + listOf( + Kind38000Fixtures.MARKET_RESOLVED_BINARY, + Kind38000Fixtures.MARKET_ACTIVE_BINARY, + Kind38000Fixtures.MARKET_VOIDED_MULTI_OUTCOME, + Kind38000Fixtures.MARKET_CANCELLED, + Kind38000Fixtures.MARKET_BAO_FUND, + Kind38000Fixtures.MARKET_FIRST_SHAPE, + Kind38000Fixtures.BALLOT_RESPONSES, + Kind38000Fixtures.BALLOT_RESPONSES_PROOF_HASH, + Kind38000Fixtures.BALLOT_OBJECT, + Kind38000Fixtures.BALLOT_VOTE_CHOICE, + Kind38000Fixtures.MINT_REVIEW_CASHU, + Kind38000Fixtures.MINT_REVIEW_FEDIMINT, + Kind38000Fixtures.MINT_REVIEW_NO_K, + Kind38000Fixtures.SPAM_SYBIL_VOTE, + Kind38000Fixtures.SPAM_APP_MANIFEST, + ).forEach { json -> + val event = parse(json) + assertEquals(38000, event.kind) + assertTrue(event.verify(), "fixture ${event.id} does not verify") + } + } + + @Test + fun mintReviewsAreMintRecommendations() { + val cashu = assertIs(parse(Kind38000Fixtures.MINT_REVIEW_CASHU)) + assertTrue(cashu.isCashuRecommendation()) + assertEquals(listOf("https://21mint.me"), cashu.mintUrls()) + + val fedimint = assertIs(parse(Kind38000Fixtures.MINT_REVIEW_FEDIMINT)) + assertTrue(fedimint.isFedimintRecommendation()) + + val noK = assertIs(parse(Kind38000Fixtures.MINT_REVIEW_NO_K)) + assertEquals(listOf("https://mint.lnpay.cz"), noK.mintUrls()) + } + + @Test + fun marketsArePredictionMarkets() { + listOf( + Kind38000Fixtures.MARKET_RESOLVED_BINARY, + Kind38000Fixtures.MARKET_ACTIVE_BINARY, + Kind38000Fixtures.MARKET_VOIDED_MULTI_OUTCOME, + Kind38000Fixtures.MARKET_CANCELLED, + Kind38000Fixtures.MARKET_BAO_FUND, + Kind38000Fixtures.MARKET_FIRST_SHAPE, + ).forEach { assertIs(parse(it)) } + } + + @Test + fun ballotsAreBallots() { + listOf( + Kind38000Fixtures.BALLOT_RESPONSES, + Kind38000Fixtures.BALLOT_RESPONSES_PROOF_HASH, + Kind38000Fixtures.BALLOT_OBJECT, + Kind38000Fixtures.BALLOT_VOTE_CHOICE, + ).forEach { assertIs(parse(it)) } + } + + @Test + fun spamAndOneOffsArePlainEvents() { + listOf( + Kind38000Fixtures.SPAM_SYBIL_VOTE, + Kind38000Fixtures.SPAM_APP_MANIFEST, + ).forEach { + val event = parse(it) + assertEquals(Event::class, event::class) + assertFalse(event is SearchableEvent) + } + } + + @Test + fun aKNamingAnotherKindIsNotARecommendationEvenWithAMintUrl() { + assertEquals(Event::class, build(arrayOf("k", "1"), arrayOf("u", "https://mint.example"))::class) + // The first k decides. + assertEquals(Event::class, build(arrayOf("k", "1"), arrayOf("k", "38172"))::class) + assertIs(build(arrayOf("k", "38173"), arrayOf("k", "1"))) + } + + @Test + fun aBlankMintUrlWithoutKIsNotARecommendation() { + assertEquals(Event::class, build(arrayOf("d", "x"), arrayOf("u", " "))::class) + assertEquals(Event::class, build(arrayOf("u"))::class) + } + + @Test + fun recommendationWinsOverTheOtherShapes() { + assertIs(build(arrayOf("k", "38172"), arrayOf("election", "e1"), arrayOf("market", "m1"))) + } + + @Test + fun ballotWinsOverMarket() { + assertIs(build(arrayOf("election", "e1"), arrayOf("market", "m1"))) + } + + @Test + fun aBlankElectionIsNotABallot() { + assertEquals(Event::class, build(arrayOf("election", ""))::class) + } + + @Test + fun marketNeedsAMarketTwoOutcomesOrATypeAndAnEnd() { + assertIs(build(arrayOf("market", "m1"))) + assertIs(build(arrayOf("outcome", "YES"), arrayOf("outcome", "NO"))) + assertIs(build(arrayOf("type", "binary"), arrayOf("end", "1"))) + + assertEquals(Event::class, build(arrayOf("market", " "))::class) + assertEquals(Event::class, build(arrayOf("outcome", "YES"))::class) + assertEquals(Event::class, build(arrayOf("type", "binary"))::class) + assertEquals(Event::class, build(arrayOf("end", "1"))::class) + } + + @Test + fun signingARecommendationRoundTripsToARecommendation() { + val signer = NostrSignerSync() + listOf(CashuMintEvent.KIND, FedimintEvent.KIND).forEach { mintKind -> + val signed = + signer.sign( + MintRecommendationEvent.build( + mintIdentifier = "https://mint.example", + mintKind = mintKind, + review = "great", + ), + ) + assertIs(signed) + assertEquals(mintKind, signed.mintEventKind()) + assertIs(Event.fromJson(signed.toJson())) + } + } + + @Test + fun theKindStillProbesAsAKnownSearchableKind() { + assertTrue(EventFactory.isKnownKind(MintRecommendationEvent.KIND)) + assertIs(EventFactory.probe(MintRecommendationEvent.KIND)) + assertTrue(EventFactory.probe(MintRecommendationEvent.KIND) is SearchableEvent) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/Kind38000Fixtures.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/Kind38000Fixtures.kt new file mode 100644 index 0000000000..32e406b69a --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/Kind38000Fixtures.kt @@ -0,0 +1,88 @@ +/* + * 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.quartz.utils + +/** + * Real kind-38000 events from a production search relay, one per shape seen in a 12-month census + * (15,347 events): NIP-87 mint recommendations, BAO Markets prediction markets, auditable-voting + * ballots, and the spam and one-offs that are none of these. + */ +object Kind38000Fixtures { + /** BAO Markets, current shape: a resolved YES/NO market (resolution = NO). */ + const val MARKET_RESOLVED_BINARY = + """{"id": "3de5a164159cd7b3925c3d7aa16a5c32f8f01e50d296e2a3821bfbf5c22db82e", "pubkey": "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", "created_at": 1778605676, "kind": 38000, "tags": [["d", "caec08eca65442725a5bd8f77e6d2666"], ["market", "caec08eca65442725a5bd8f77e6d2666"], ["m", "caec08eca65442725a5bd8f77e6d2666"], ["type", "binary"], ["w", "binary"], ["category", "ai-llm"], ["c", "ai-llm"], ["state", "resolved"], ["status", "resolved"], ["s", "resolved"], ["end", "1777680000"], ["yes_hash", "e7713ddb231bdd5fdb32eeb902786019894cfbb3d076992b5c3b9514be1870b5"], ["no_hash", "b57e7121377201283659de7144e78f87332e568142edaf24accbd97b445eff00"], ["yes_adaptor_point", "04188a0f14f632aa77b1aa8065ddf541556e5ddfe654a22304fb8661710cb1f6e48d5c606f280ec4a265e8ab45ff9c529a11d53da0ecb6aeaae285a977a4a81b3f"], ["no_adaptor_point", "04b43fe7b12d84d5c6ac9368447c85fda62b57c5bbe7e6acaf4de0e31728703b68cd977cc8e3a20a1c2a2919f3175317debfac1f85169c14f0f30802f182f151f8"], ["oracle", "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349"], ["expiry_blocks", "205"], ["network", "demo"], ["n", "demo"], ["flash", "true"], ["duration_blocks", "61"], ["oracle_tier", "2"], ["pool_model", "smj"], ["duration_tier", "now"], ["fee_percent", "4.21"], ["min_bet", "100"], ["max_bet", "100000"], ["t", "smj-market"], ["t", "prediction-market"], ["t", "ai-llm"], ["client", "BAO Markets"], ["resolution_source", "github.com/trending daily repos"], ["escrow_method", "htlc"], ["outcome", "YES"], ["outcome", "NO"], ["data", "{\"title\":\"Will an AI coding tool appear in GitHub top trending repos today? (UTC 2,May,2026)\",\"description\":\"At least one repository in GitHub's daily trending page is directly related to AI coding tools (e.g., Copilot alternatives, AI code generation, AI-powered IDEs). Resolution via github.com/trending.\",\"outcomes\":[\"YES\",\"NO\"],\"isFlash\":true,\"poolModel\":\"smj\",\"durationTier\":\"now\",\"feePercent\":4.21,\"minBetSats\":100,\"maxBetSats\":100000,\"resolutionSource\":\"github.com/trending daily repos\",\"resolutionLogic\":\"\"}"], ["poll", "9f1a858603ca85c2d52d2a63710c0c5162b8af6489b9309e547c93dcf09303bb"], ["resolution", "NO"]], "content": "🤖 Will an AI coding tool appear in GitHub top trending repos today? (UTC 2,May,2026)\n\nAt least one repository in GitHub's daily trending page is directly related to AI coding tools (e.g., Copilot alternatives, AI code generation, AI-powered IDEs). Resolution via github.com/trending.\n\n📈 🟢 YES | 🔴 NO\n\n⚡ Flash market (SMJ)\n\n⏱️ Closes: May 2 at 00:00 UTC\n\n#baomarkets #predictionmarkets #truthmachine", "sig": "7e21243b38779711181ccde0a722dbb8e10264568605bd74a5024f0d650be2c925e18cc984aa57030213b6882c61cae2f9f18647c01ae0ed05e3cbc6831e0fdb"}""" + + /** BAO Markets, current shape: status "active" but state "ended", end in the past. */ + const val MARKET_ACTIVE_BINARY = + """{"id": "4f7146bcdd339f7c1f4a56f460c79cf56587a17c61ccdb5f43ea24eb7af494c0", "pubkey": "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", "created_at": 1786702043, "kind": 38000, "tags": [["d", "812916327ded76bb2b4bba766596bac3"], ["market", "812916327ded76bb2b4bba766596bac3"], ["m", "812916327ded76bb2b4bba766596bac3"], ["type", "binary"], ["w", "binary"], ["category", "bitcoin"], ["c", "bitcoin"], ["state", "ended"], ["status", "active"], ["s", "ended"], ["end", "1777939200"], ["yes_hash", "494a22a6612b840ca5c7079fefa00b48e40fd33bc96718692ebf94af1b93779b"], ["no_hash", "e5ee4d1c1d10dc0f148cf2e54e8564b468694cd0ef94a7bd6085d20ba4b41be8"], ["yes_adaptor_point", "04763467fb0e756f204f7a53571103ec49aad8354118807edfeffc1ca1e5cc5921eb4b61cdee42810aff726a90ab162f6d605796e2687076190b5bac8aeb19c381"], ["no_adaptor_point", "04dd8f56d6d9b86abb6dade0a83f689c2286b4c8cf36168bc7465a6df5512124051ec402443c973e427730db4c462e598d4f0fadb947009d6ce4b131052391e719"], ["oracle", "4a468afe57e1c93291929d2ffe5f287976b1639f3545845655d4b447a289186d"], ["expiry_blocks", "223"], ["network", "demo"], ["n", "demo"], ["smj", "true"], ["duration_blocks", "79"], ["oracle_tier", "1"], ["pool_model", "smj"], ["duration_tier", "now"], ["fee_percent", "4.21"], ["min_bet", "100"], ["max_bet", "100000"], ["t", "smj-market"], ["t", "prediction-market"], ["t", "bitcoin"], ["client", "BAO Markets"], ["resolution_source", "https://mempool.space/api/v1/blocks"], ["escrow_method", "htlc"], ["outcome", "YES"], ["outcome", "NO"], ["data", "{\"title\":\"Will the Bitcoin network mine fewer than 140 blocks today? (May 4, 2026) (UTC 5,May,2026)\",\"description\":\"Target is 144 blocks per day. Resolves YES if total blocks mined between 00:00:00 and 23:59:59 UTC is 139 or less. \\n\\nSource of Truth: mempool.space/api/v1/blocks\",\"outcomes\":[\"YES\",\"NO\"],\"isSmj\":true,\"poolModel\":\"smj\",\"durationTier\":\"now\",\"feePercent\":4.21,\"minBetSats\":100,\"maxBetSats\":100000,\"resolutionSource\":\"https://mempool.space/api/v1/blocks\",\"resolutionLogic\":\"\"}"], ["poll", "a8e310da83e3a92c17a3ab72f32cac5bcac0dc53fa52cae0d24ba68d98504b98"]], "content": "₿ Will the Bitcoin network mine fewer than 140 blocks today? (May 4, 2026) (UTC 5,May,2026)\n\nTarget is 144 blocks per day. Resolves YES if total blocks mined between 00:00:00 and 23:59:59 UTC is 139 or less. \n\nSource of Truth: mempool.space/api/v1/blocks\n\n📈 🟢 YES | 🔴 NO\n\n⚡ Smj market (SMJ)\n\n⏱️ Closes: May 5 at 00:00 UTC\n\n#baomarkets #predictionmarkets #truthmachine", "sig": "dd4bbbdf0b62d149d0f7bc512d9017d818ab758e2cf2c292d2307fc29519ac07782f683b7b2654d7b04f5747b73dc40bdb0b794ae07d6f45492fd24db1da374d"}""" + + /** BAO Markets, current shape: five named outcomes, status "voided". */ + const val MARKET_VOIDED_MULTI_OUTCOME = + """{"id": "31da397fdc3a5c61aaa3fc12e8413279f99175ecdba65443d0a2e2495d73f5df", "pubkey": "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", "created_at": 1786702119, "kind": 38000, "tags": [["d", "181f3a7acd28280a49e81bf70ec22998"], ["market", "181f3a7acd28280a49e81bf70ec22998"], ["m", "181f3a7acd28280a49e81bf70ec22998"], ["type", "binary"], ["w", "binary"], ["state", "ended"], ["status", "voided"], ["s", "ended"], ["end", "1778889600"], ["yes_hash", "8379c8fb7f7c5fcf48b92e114bbade3e44cb9e918f9b529e4618f29d92c46f4b"], ["no_hash", "f978de5abf4264a4638e5d90da140f46a510c578ed916852729156f06bd5a45c"], ["yes_adaptor_point", "04ccddc9b30d3689a9478aa020155f5cacd833d304b3296cf4a9f65e8f0b76dea1993581ea15331502e4fc96641ad3c7d3c782d6e5aadfc83b97d3647989195f33"], ["no_adaptor_point", "0458bd7dcb9b57064489e79724f4ece3f0df725c91ffe6427318ec20d022f6e47ec8ef7f5f445d95a92a3228957334b47efabd6f17fe183c34e39a1839997af197"], ["oracle", "73d9e19ef07e0d098fc0fc5fb75db0f854824e8b4e43905acce638ddf6469960"], ["expiry_blocks", "287"], ["network", "demo"], ["n", "demo"], ["smj", "true"], ["duration_blocks", "143"], ["oracle_tier", "1"], ["pool_model", "smj"], ["duration_tier", "now"], ["fee_percent", "4.21"], ["min_bet", "100"], ["max_bet", "100000"], ["t", "smj-market"], ["t", "prediction-market"], ["t", "mining-pools"], ["client", "BAO Markets"], ["resolution_source", "https://mempool.space/api/v1/blocks"], ["escrow_method", "htlc"], ["outcome", "Foundry USA"], ["outcome", "AntPool"], ["outcome", "F2Pool"], ["outcome", "ViaBTC"], ["outcome", "Other"], ["data", "{\"title\":\"Which mining pool will mine the most Bitcoin blocks today? (May 15, 2026) (UTC 16,May,2026)\",\"description\":\"Resolves based on the total block count at midnight UTC. \\n\\nSource of Truth: mempool.space/api/v1/blocks\",\"outcomes\":[\"Foundry USA\",\"AntPool\",\"F2Pool\",\"ViaBTC\",\"Other\"],\"isSmj\":true,\"poolModel\":\"smj\",\"durationTier\":\"now\",\"feePercent\":4.21,\"minBetSats\":100,\"maxBetSats\":100000,\"resolutionSource\":\"https://mempool.space/api/v1/blocks\",\"resolutionLogic\":\"\"}"], ["poll", "5c7dc9874ddbfd350d4b4d5f984cf566d8895c4e24f958278ec4401fd038ba76"], ["t", "mining"], ["c", "mining"], ["category", "mining"]], "content": "🔮 Which mining pool will mine the most Bitcoin blocks today? (May 15, 2026) (UTC 16,May,2026)\n\nResolves based on the total block count at midnight UTC. \n\nSource of Truth: mempool.space/api/v1/blocks\n\n📈 🟢 Foundry USA | 🔴 AntPool | 🔵 F2Pool | 🟡 ViaBTC | ⚪ Other\n\n⚡ Smj market (SMJ)\n\n⏱️ Closes: May 16 at 00:00 UTC\n\n#baomarkets #predictionmarkets #truthmachine", "sig": "2046a2128d384ffd3b590bcbbd466f957c76bd9ab88fcdb7e6e80ca6baedd9a77f2183092f466102f392207adac5ef13c12779b7fe2fbe7837058c56637a728e"}""" + + /** BAO Markets, current shape: cancelled, with a cancel_reason and a JSON content. */ + const val MARKET_CANCELLED = + """{"id": "22c74e00e0d123f8cee351895a0a840a4473cab0935b86b79c02051979277363", "pubkey": "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", "created_at": 1786629062, "kind": 38000, "tags": [["d", "lt-0225a12b-msrhc3x6"], ["market", "lt-0225a12b-msrhc3x6"], ["m", "lt-0225a12b-msrhc3x6"], ["state", "cancelled"], ["status", "cancelled"], ["s", "cancelled"], ["n", "demo"], ["network", "demo"], ["c", "bitcoin"], ["t", "smj-market"], ["t", "prediction-market"], ["title", "Will BTC price be above $60,000 at 2026-08-13 13:00 UTC?"], ["end", "1786626000"], ["cancel_reason", "bridge-test junk"], ["e", "fdd7ccd4fb6dc6255d1b42d3b26b0b496aed5e7c275d6c26c5497376b84c01d0", "", "replaces"]], "content": "{\"title\":\"Will BTC price be above $60,000 at 2026-08-13 13:00 UTC?\",\"status\":\"cancelled\",\"reason\":\"bridge-test junk\",\"cancelledAt\":1786629062}", "sig": "df0998ce37ee7e90e925bb621faf5b40e8dc80ae579d0b7ec54e24e100f26e79c0c0bf7bd4ff304b1d927c2bf53aff0ad06ca07735d861796b05c8625368de86"}""" + + /** BAO Fund shape: title tag, ["outcome", id, label] tags, JSON content. */ + const val MARKET_BAO_FUND = + """{"id": "90ca7c1399f8309715f243e53f14c6ce7e4e71687b501290b5ea9553d9520e52", "pubkey": "2eaf781b3cabef3cbf17313bc43580761748651bc09273d50646651dd06c6349", "created_at": 1790725353, "kind": 38000, "tags": [["d", "baofund-fr_2450b9bc52e06b51-0"], ["title", "Will E2E demo-rail liquid refund munbn1yg deliver: E2E milestone munbn1yg by 2026-09-30?"], ["c", "bao-fund"], ["n", "demo"], ["network", "demo"], ["pool_model", "amm"], ["end", "1790727795"], ["outcome", "YES", "YES"], ["outcome", "NO", "NO"]], "content": "{\"title\":\"Will E2E demo-rail liquid refund munbn1yg deliver: E2E milestone munbn1yg by 2026-09-30?\",\"description\":\"Will E2E demo-rail liquid refund munbn1yg deliver: E2E milestone munbn1yg by 2026-09-30?\\n\\nCRITERIA: E2E milestone munbn1yg\\nTREASURY: 12000 sats locked (liquid)\\nRUNNER FEE: 2.14%\\nCHALLENGE WINDOW: 7 days after proof submission\\nDEMO: signet market, admin-resolved oracle.\",\"category\":\"bao-fund\",\"type\":\"binary\",\"outcomes\":[{\"id\":\"YES\",\"label\":\"YES\",\"price\":0.5},{\"id\":\"NO\",\"label\":\"NO\",\"price\":0.5}],\"endDate\":1790727795}", "sig": "280f2f6ea98789df8f77f4d1f2b194b2b76a6efede7513efffdd795de4dff4c372fc392cbb4322b53daffab171f35bf6d48fd08b72459ee488cc6c5ebbaba1df"}""" + + /** BAO Markets' first shape: type/end/state tags, outcomes as JSON objects in content. */ + const val MARKET_FIRST_SHAPE = + """{"id": "3c69cbbecbb2578283d3978e4dfc39774adb2bd5fa6f9642c0680b9fc0cae887", "pubkey": "fba1bbd8ab57f258673157defd5afc9ceda004c6845f99db3169fe4b61ba7416", "created_at": 1769205950, "kind": 38000, "tags": [["d", "baoMarkets-mkt-1769205950092-0b985e090"], ["type", "binary"], ["category", "nostr"], ["oracle", "community"], ["end", "1769256240"], ["state", "funding"], ["platform_fee", "200"], ["t", "trump"]], "content": "{\"title\":\"will trump turn purple before May?\",\"description\":\"Trump is orange, but when purple? I think he hes been orange for far too long\",\"outcomes\":[{\"id\":\"YES\",\"label\":\"Yes\",\"probability\":0.5,\"totalVolume\":0,\"price\":50},{\"id\":\"NO\",\"label\":\"No\",\"probability\":0.5,\"totalVolume\":0,\"price\":50}],\"minLiquidity\":100000,\"currentLiquidity\":0,\"resolutionTime\":1770984240}", "sig": "cb3230e903b7b1bd12b54cef3d7de79f41a5978ff8397b3b826c87a68d23065952d0b7b25b95a30004b62d21736d39a0ff2149b84ec6db829bf2023fad52d181"}""" + + /** Ballot with a responses array; values are strings and numbers. */ + const val BALLOT_RESPONSES = + """{"id": "abebb24cb12d96514726739fba359e1327eaa193649e8faa2909f4abb7ec907b", "pubkey": "7f8e147582c364ce91fbcdceabee0059a7d44b5caa0d51eb79fbfb7bd0c57346", "created_at": 1774357688, "kind": 38000, "tags": [["election", "sec06-feedback-1774311582"]], "content": "{\"election_id\":\"sec06-feedback-1774311582\",\"responses\":[{\"question_id\":\"q1\",\"value\":\"Yes\"},{\"question_id\":\"q2\",\"value\":\"Yes\"},{\"question_id\":\"q3\",\"value\":5},{\"question_id\":\"q4\",\"value\":\"9am. 10am would’ve been better. \"},{\"question_id\":\"q5\",\"value\":\"Staple food recommendations. It’s really hard to find food around Funchal. All of them are tourist traps. \"},{\"question_id\":\"q6\",\"value\":\"Sauna talks. It was promised. \"},{\"question_id\":\"q7\",\"value\":\"Filter water near the coffee area. \"},{\"question_id\":\"q8\",\"value\":\"4 weeks. \"},{\"question_id\":\"q9\",\"value\":\"NA\"},{\"question_id\":\"q10\",\"value\":\"The t shirts could be of better quality. Happy to pay for it. \"}],\"timestamp\":1774357688}", "sig": "501788604b03e1185d4f62232f2201c876dff570a3d795de993e2e90a92426361be7096b10edbf404753fd83a69e79d054202a64d4c80c9e053db6a43bd63c3f"}""" + + /** Ballot with a responses array and a proof-hash tag. */ + const val BALLOT_RESPONSES_PROOF_HASH = + """{"id": "3c3702d747bd27b59f5890b54f4e7f68402d4ead3893656fe003abf094fab780", "pubkey": "c908bcaaf6f2edf4edaab9c4b73c2b9ab0ffe6f6d76a960cbd1bbf6f94ee8248", "created_at": 1774910824, "kind": 38000, "tags": [["election", "spring-2026-council"], ["proof-hash", "361689b5ccd44f35eebba995129d8f2b00dd369aca4c51b55f00065b2b477ddd"]], "content": "{\"election_id\":\"spring-2026-council\",\"responses\":[{\"question_id\":\"proposal_approval\",\"value\":\"Yes\"}],\"timestamp\":1774910824}", "sig": "842f2f776960985bf4414d5c3770b3763c5cfa93ff31342e941117041e6684766e1100381ccc16aafa802439b1d2fbf0ac65c4d58de0c8793757699a8a032464"}""" + + /** Ballot with a ballot object, a proof_hash tag and a mint tag. */ + const val BALLOT_OBJECT = + """{"id": "78f5f012a9518a0b39134e659ef180eae6df9f6b38ce3b4b86963409837e2776", "pubkey": "caf0b6ef4eb461677ed7bf7b569a0212443d6429f6cc555ca8f00a599e453bb2", "created_at": 1773998101, "kind": 38000, "tags": [["t", "auditable-vote"], ["election", "spring-2026-council"], ["proof_hash", "bb64227a0b57fa6af765881b0952fe9d01a3a18faf52a3e34eeb7a896ee36b95"], ["mint", "http://localhost:8787/mock-mint"]], "content": "{\"election_id\":\"spring-2026-council\",\"proof_hash\":\"bb64227a0b57fa6af765881b0952fe9d01a3a18faf52a3e34eeb7a896ee36b95\",\"ballot\":{\"funding_priority\":\"community-grants\",\"audit_policy\":\"every-election\"}}", "sig": "79b1722a28ab18634994d939963734551748eaa5f1ce1459b43ef6349242849b721dfb57beda91409984cb3e364d5be65173c0e9011bcfa2f68fb26fba456c50"}""" + + /** Ballot with a lone vote_choice. */ + const val BALLOT_VOTE_CHOICE = + """{"id": "0e6b7a37d9db693dfdc7b2d8b4747efb8cac7fa493cc4915d9f07e6da61732ba", "pubkey": "5e7debebfde8730583455268731e4f8d2079dbe01cba06542e3f4825e598e233", "created_at": 1773715079, "kind": 38000, "tags": [["election", "election1"]], "content": "{\"vote_choice\":\"yes\"}", "sig": "d3259dfa20846d70f7045bdd5b80a3dc65610a4ebd12657a21e835f9ffa6bfbd3dde520d19b19133bc17cf32cfc5702473b9afe605ed7f6c0207dee923b4ad03"}""" + + /** NIP-87 recommendation of a Cashu mint (k = 38172, u). */ + const val MINT_REVIEW_CASHU = + """{"id": "9086081aea72122e58fd9c058ce6dd8068454750ee69b9e92d51896deb6201cb", "pubkey": "9d9789c0212d5157c0ac1351e1588b055dff0b91394261eecfa276e390382c7e", "created_at": 1790670504, "kind": 38000, "tags": [["k", "38172"], ["u", "https://21mint.me"], ["d", "https://21mint.me"]], "content": "[5/5] Been using it since a year now, most reliable mint out there.", "sig": "fd461995093e15c95dd807ecab361e5ef4f78d7198229b5e9e69708edc0930924487369757265d04938834aa73198f9731b30677f63319dca68b8106536187e7"}""" + + /** NIP-87 recommendation of a Fedimint (k = 38173, no u). */ + const val MINT_REVIEW_FEDIMINT = + """{"id": "092ce058a30d09d9dbe73808a2c15fd0146a27ebfb58a3c0c5a408e3b3e09c5f", "pubkey": "d362b888c17926dd5188a5228779251d17744b8ebe9ec5d313eff52c4776fdf3", "created_at": 1790796532, "kind": 38000, "tags": [["d", "6b2bcd96ab83d6ba31e36b821c89f6cc7a57d2133cdf884018833d3770456fe6"], ["k", "38173"], ["rating", "5"]], "content": "[5/5]", "sig": "7424fdfda8c7af66b5108c90ded4b893faf5f538ec24ca09f2ce85ec0dd9484c93e7c7024493577384ad850bac230211516051845bae2e90e151ed92fd5fd06c"}""" + + /** Old NIP-87 recommendation without a k tag, pointing at a mint URL. */ + const val MINT_REVIEW_NO_K = + """{"id": "557f112e147877de3f956c86d04cb2feeded99287ad0d2e56e9990ac1eef2d82", "pubkey": "89c16b5e5ba539db0d8921e75100cc8161737c381600ce47d3788dce18083604", "created_at": 1790252206, "kind": 38000, "tags": [["d", "https://mint.lnpay.cz"], ["u", "https://mint.lnpay.cz"], ["rating", "5"], ["comment", "Stable and reliable"]], "content": "Stable and reliable", "sig": "9a126f18b435234f1c2e9bda3b4b61a155cf8060de24be42cc28250ef447c1e42bbfac2191160589515edadeaaf74857ae4b5644c92d6a60e66366ad5bc4a5da"}""" + + /** Spam: a d-only "sybil test vote" (most kind-38000 events on big relays). */ + const val SPAM_SYBIL_VOTE = + """{"id": "6c0fccc4bead821665d6f4ce6f57d8dc5d67e60277b24950040796aa6e3266ee", "pubkey": "9bd0c7b7420e312c83201fc462fbd2150ec36ef9eed3db93c9fd0f35a79d9f52", "created_at": 1789336638, "kind": 38000, "tags": [["d", "713d9b889b5e207a85d8632e2e5f6926cf74c532299bc313a5fc105534007fce"]], "content": "[1/5] fedimint-observer security PoC (PR #124) - sybil test vote, safe to delete mass2#210099", "sig": "c42fc32ea66ee311b1d68d298e4160ff389f9e08c7615ac7f769a3a276e615377147122ab21b19f244043432a8d8aaa0e5b1eac0e608e0128587d0b6b35a7006"}""" + + /** A one-off app manifest: not a format Amethyst knows. */ + const val SPAM_APP_MANIFEST = + """{"id": "3abc7024d399b8e2e1aa4cd519c60a0bbfb439f1267a2f956f3d7d975c5c3e34", "pubkey": "c99281bdc983fcba340a23a3ff6380e3b93fde8621795535f1220eafd4fa26d3", "created_at": 1779037402, "kind": 38000, "tags": [["d", "pulsar-tunnel"], ["t", "pulsar"], ["t", "pulsar-tunnel"]], "content": "{\"name\":\"Pulsar Tunnel\",\"version\":\"0.1.0\",\"pubkey\":\"c99281bdc983fcba340a23a3ff6380e3b93fde8621795535f1220eafd4fa26d3\",\"transport\":\"nostr\"}", "sig": "5498710238eb4f2bd4f7bb848db3faf2e896a0e42bf2f6a3698037e5a4ff9e3f883038df11260375c301990b111a7a1b76e0f731e2de495af31a7cace8f92c21"}""" +} diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt index 62616dd7ef..6dd19fd388 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt @@ -599,13 +599,12 @@ open class FsEventStore( } /** - * True when [kind] currently parses to a [SearchableEvent]. Kind - * alone selects the event class in [EventFactory], so a single probe - * per kind is authoritative. The id is non-blank so kinds that lazily - * hash a missing id (NIP-17 chat) skip that work — only the runtime - * type matters here. + * True when [kind] currently parses to a [SearchableEvent], decided by + * one [EventFactory.probe] per kind (a kind whose class is picked by + * tags answers as its primary class; [FsIndexer.linkFts] still checks + * each event itself). */ - private fun isSearchableKind(kind: Int): Boolean = EventFactory.create("0", "0", 0L, kind, emptyArray(), "", "") is SearchableEvent + private fun isSearchableKind(kind: Int): Boolean = EventFactory.probe(kind) is SearchableEvent private fun deleteRecursively(p: java.nio.file.Path) { if (!Files.exists(p)) return diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip50Search/IndexableContentGoldenTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip50Search/IndexableContentGoldenTest.kt index eb73ec3eba..e7efd459b7 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip50Search/IndexableContentGoldenTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip50Search/IndexableContentGoldenTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip50Search +import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.utils.EventFactory import org.junit.Assert.assertEquals import org.junit.Test @@ -69,6 +70,9 @@ class IndexableContentGoldenTest { arrayOf("t", "hashtag2"), arrayOf("image", "https://example.com/i.png"), arrayOf("published_at", "1700000000"), + // Kind 38000 picks its class by tags; without a mint `k` it would sample as the + // plain Event its junk parses to rather than as the mint recommendation it indexes. + *if (kind == MintRecommendationEvent.KIND) arrayOf(arrayOf("k", "38172")) else emptyArray(), ), "The content body.", "", From bb7b528eea98e37ecf748c35260aea66b0e76622 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:24:21 +0000 Subject: [PATCH 29/45] feat: share what you listen to as a NIP-38 music status Publishes the song or podcast episode the user is playing as the account's NIP-38 `music` status (podcasts included, so every client renders them), with a NIP-40 expiration at the track's end, and clears it when playback stops. Everything is opt-in per account under Settings > Now Playing. Sources: - Android, in app: a Player.Listener on every ExoPlayer resolves the item's nostr: URI to a music track, audio track or podcast episode (feed videos, voice notes and muted autoplay never count) and tags the status with its a/e reference. - Android, other apps: a NotificationListenerService reads other apps' media sessions (Spotify, YouTube Music, podcast players, ...) once the user grants notification access; apps can be blocked one by one. - Desktop: Linux MPRIS via dbus-send, macOS Spotify/Music via osascript, Windows media controls via PowerShell/WinRT. No new dependencies. Shared core in commons (NowPlayingPublisher, NowPlayingStatusService, NowPlayingResolver, settings + DataStore store) and a shared settings screen in commonsUI. The publisher waits 5s before posting a new track, re-posts on seeks, refreshes unknown-length streams, and waits 30s before clearing so a pause or stall doesn't churn events. Also fixes UserStatusCache keeping a status listed after a newer blank (cleared) or expired version arrived, and keeps the music status out of the drawer's status editor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N2naGACCZy1GvqmZNCwpBu --- amethyst/src/main/AndroidManifest.xml | 19 ++ .../com/vitorpamplona/amethyst/AppModules.kt | 13 + .../nowPlaying/AndroidNowPlayingAccess.kt | 33 +++ .../service/nowPlaying/InAppPlayback.kt | 124 +++++++++ .../nowPlaying/NowPlayingStatusCoordinator.kt | 118 +++++++++ .../nowPlaying/OtherAppsNowPlayingService.kt | 219 ++++++++++++++++ .../playback/playerPool/ExoPlayerBuilder.kt | 2 + .../playback/playerPool/ExoPlayerPool.kt | 6 + .../amethyst/ui/navigation/AppNavigation.kt | 7 + .../ui/navigation/drawer/DrawerContent.kt | 7 +- .../amethyst/commons/model/Account.kt | 14 ++ .../commons/model/navigation/Routes.kt | 2 + .../nip38UserStatuses/UserStatusAction.kt | 20 ++ .../nip38UserStatuses/UserStatusCache.kt | 13 +- .../nowPlaying/NowPlaying.kt | 93 +++++++ .../nowPlaying/NowPlayingAccess.kt | 33 +++ .../nowPlaying/NowPlayingPublisher.kt | 156 ++++++++++++ .../nowPlaying/NowPlayingResolver.kt | 109 ++++++++ .../nowPlaying/NowPlayingSettings.kt | 89 +++++++ .../nowPlaying/NowPlayingStatusService.kt | 59 +++++ .../preferences/NowPlayingSettingsStore.kt | 108 ++++++++ .../nip38UserStatuses/UserStatusCacheTest.kt | 61 +++++ .../nowPlaying/NowPlayingPublisherTest.kt | 215 ++++++++++++++++ .../nowPlaying/NowPlayingResolverTest.kt | 73 ++++++ .../NowPlayingSettingsStoreTest.kt | 106 ++++++++ .../composeResources/values/strings.xml | 13 + .../settings/NowPlayingSettingsScreen.kt | 238 ++++++++++++++++++ .../settings/SettingsCatalogBuilder.kt | 3 + .../vitorpamplona/amethyst/desktop/Main.kt | 45 ++++ .../DesktopNowPlayingCoordinator.kt | 90 +++++++ .../nowPlaying/DesktopNowPlayingSettings.kt | 78 ++++++ .../desktop/nowPlaying/MacNowPlayingReader.kt | 98 ++++++++ .../nowPlaying/MprisNowPlayingReader.kt | 190 ++++++++++++++ .../desktop/nowPlaying/OsNowPlayingReader.kt | 75 ++++++ .../nowPlaying/WindowsNowPlayingReader.kt | 108 ++++++++ .../nowPlaying/OsNowPlayingReaderTest.kt | 201 +++++++++++++++ 36 files changed, 2833 insertions(+), 5 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/AndroidNowPlayingAccess.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/InAppPlayback.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingAccess.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisher.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingSettings.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingStatusService.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStore.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCacheTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolverTest.kt create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStoreTest.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingSettings.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MacNowPlayingReader.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/WindowsNowPlayingReader.kt create mode 100644 desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index 91acb98039..d5d91e424e 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -90,6 +90,11 @@ + + + + @@ -556,6 +561,20 @@ + + + + + + + >(emptyMap()) + + val flow: StateFlow> = playing + + /** The most recently started playback, if any. */ + fun current(all: Map): InAppPlayback? = all.values.lastOrNull() + + fun update( + player: Player, + playback: InAppPlayback?, + ) { + val key = System.identityHashCode(player) + playing.update { current -> + when { + playback == null -> if (key in current) current - key else current + current[key] == playback -> current + // Remove first so a new track moves to the end and becomes the current one. + else -> (current - key) + (key to playback) + } + } + } + + /** A released player reports nothing on its way out, so the pool drops it here. */ + fun forget(player: Player) = update(player, null) +} + +/** + * Reports a player's audible playback to [InAppPlaybackRegistry]. A muted player (feed autoplay) + * or one without a `nostr:` callback URI reports nothing. + * + * Attached once per player in `ExoPlayerBuilder`; runs on the player's looper. + */ +class InAppPlaybackListener( + private val registry: InAppPlaybackRegistry = InAppPlaybackRegistry, + private val nowSeconds: () -> Long = TimeUtils::now, +) : Player.Listener { + override fun onEvents( + player: Player, + events: Player.Events, + ) { + if (events.containsAny(*RELEVANT_EVENTS)) { + registry.update(player, snapshot(player)) + } + } + + private fun snapshot(player: Player): InAppPlayback? { + if (!player.isPlaying || player.volume < 0.001f) return null + + val item = player.currentMediaItem ?: return null + val metadata = item.mediaMetadata + val callbackUri = metadata.extras?.getString(MediaItemCache.EXTRA_CALLBACK_URI) ?: return null + if (!callbackUri.startsWith("nostr:")) return null + + val duration = player.duration.takeIf { it != C.TIME_UNSET && !player.isCurrentMediaItemLive } + + return InAppPlayback( + callbackUri = callbackUri, + // MediaItemCache falls back to the media URL (the media id) when there is no title. + title = metadata.title?.toString()?.takeUnless { it == item.mediaId }, + artist = metadata.artist?.toString(), + endsAt = NowPlaying.endsAt(nowSeconds(), duration, player.currentPosition, player.playbackParameters.speed), + ) + } + + companion object { + private val RELEVANT_EVENTS = + intArrayOf( + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_MEDIA_ITEM_TRANSITION, + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_TIMELINE_CHANGED, + Player.EVENT_VOLUME_CHANGED, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + Player.EVENT_MEDIA_METADATA_CHANGED, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt new file mode 100644 index 0000000000..4998f945a2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt @@ -0,0 +1,118 @@ +/* + * 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.service.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingPublisher +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingResolver +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingStatusService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull + +/** + * Publishes what the user is listening to, in Amethyst or in another app, as the logged-in + * account's NIP-38 music status, following that account's now-playing settings. + * + * App-scoped, like the other background watchers in AppModules: the music keeps playing (and + * other apps keep reporting) after the activity is gone. While the app is in the background the + * relay pool is paused, so every publish holds [relayServices] open for [RELAY_HOLD_MS], long + * enough for the event to go out. + */ +class NowPlayingStatusCoordinator( + private val scope: CoroutineScope, + private val accountFlow: Flow, + private val relayServices: Flow<*>, + private val inApp: StateFlow> = InAppPlaybackRegistry.flow, + private val otherApps: StateFlow = OtherAppsPlaybackRegistry.flow, +) { + private var relayHold: Job? = null + + fun start() { + scope.launch(Dispatchers.IO) { + accountFlow + .distinctUntilChanged { a, b -> a?.signer?.pubKey == b?.signer?.pubKey } + .collectLatest { account -> + if (account == null || !account.isWriteable()) return@collectLatest + run(account) + } + } + } + + private suspend fun run(account: Account) { + val inAppTracks = + inApp + .map { InAppPlaybackRegistry.current(it) } + .distinctUntilChanged() + .map { playback -> playback?.let(::resolve) } + .flowOn(Dispatchers.IO) + + val publisher = + NowPlayingPublisher( + publish = { track, expiration -> + account.publishNowPlaying(track, expiration) + holdRelays() + }, + clear = { + account.clearNowPlaying() + holdRelays() + }, + ) + + NowPlayingStatusService( + settings = account.nowPlayingSettings.flow, + sources = listOf(inAppTracks, otherApps), + onOtherAppSeen = { account.nowPlayingSettings.rememberApp(it.id, it.label) }, + publisher = publisher, + ).run() + } + + private fun resolve(playback: InAppPlayback): NowPlaying? = + NowPlayingResolver.fromNostrUri( + uri = playback.callbackUri, + findEvent = { key -> LocalCache.getNoteIfExists(key)?.event }, + fallbackTitle = playback.title, + fallbackArtist = playback.artist, + endsAt = playback.endsAt, + ) + + private fun holdRelays() { + relayHold?.cancel() + relayHold = + scope.launch(Dispatchers.IO) { + withTimeoutOrNull(RELAY_HOLD_MS) { relayServices.collect {} } + } + } + + companion object { + private const val RELAY_HOLD_MS = 30_000L + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt new file mode 100644 index 0000000000..6a8a69718a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt @@ -0,0 +1,219 @@ +/* + * 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.service.nowPlaying + +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.provider.Settings +import android.service.notification.NotificationListenerService +import androidx.core.app.NotificationManagerCompat +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** What another app on this device is playing, as reported by its media session. */ +object OtherAppsPlaybackRegistry { + private val playing = MutableStateFlow(null) + + val flow: StateFlow = playing + + fun update(track: NowPlaying?) { + playing.value = track + } +} + +/** + * Reads the media sessions of other apps (Spotify, YouTube Music, podcast players, ...) to share + * what the user is listening to as a NIP-38 music status. + * + * Android only hands out other apps' sessions to an enabled notification listener, which is why + * this is one: it never reads or posts notifications. The user grants it in the system's + * "Notification access" screen ([openAccessSettings]); until then the system never binds it. + * Whether anything is published is still up to the account's now-playing settings. + * + * Runs in the main process (it is not declared in `:napplet`), where the account lives. + */ +class OtherAppsNowPlayingService : NotificationListenerService() { + private val handler = Handler(Looper.getMainLooper()) + private var sessionManager: MediaSessionManager? = null + private val watched = mutableMapOf>() + + private val sessionsListener = + MediaSessionManager.OnActiveSessionsChangedListener { controllers -> + watch(controllers.orEmpty()) + } + + override fun onListenerConnected() { + super.onListenerConnected() + val manager = getSystemService(MediaSessionManager::class.java) ?: return + sessionManager = manager + + try { + val component = ComponentName(this, OtherAppsNowPlayingService::class.java) + manager.addOnActiveSessionsChangedListener(sessionsListener, component, handler) + watch(manager.getActiveSessions(component)) + } catch (e: SecurityException) { + // Access was revoked between the bind and this call. + Log.w("OtherAppsNowPlaying", "Notification access is not granted", e) + } + + // The system can start the process just to bind this listener; the publisher needs the account. + Amethyst.instance.sessionManager.loginWithDefaultAccountIfLoggedOff() + } + + override fun onListenerDisconnected() { + sessionManager?.removeOnActiveSessionsChangedListener(sessionsListener) + sessionManager = null + unwatchAll() + OtherAppsPlaybackRegistry.update(null) + super.onListenerDisconnected() + } + + override fun onDestroy() { + sessionManager?.removeOnActiveSessionsChangedListener(sessionsListener) + unwatchAll() + OtherAppsPlaybackRegistry.update(null) + super.onDestroy() + } + + private fun watch(controllers: List) { + val others = controllers.filter { it.packageName != packageName } + val keys = others.map { it.sessionToken.toString() }.toSet() + + watched.keys.filter { it !in keys }.forEach { key -> + watched.remove(key)?.let { (controller, callback) -> controller.unregisterCallback(callback) } + } + + others.forEach { controller -> + val key = controller.sessionToken.toString() + if (key !in watched) { + val callback = + object : MediaController.Callback() { + override fun onPlaybackStateChanged(state: PlaybackState?) = refresh() + + override fun onMetadataChanged(metadata: MediaMetadata?) = refresh() + + override fun onSessionDestroyed() { + watched.remove(key)?.let { (it, cb) -> it.unregisterCallback(cb) } + refresh() + } + } + controller.registerCallback(callback, handler) + watched[key] = controller to callback + } + } + + refresh() + } + + private fun unwatchAll() { + watched.values.forEach { (controller, callback) -> controller.unregisterCallback(callback) } + watched.clear() + } + + /** The first session that is playing, in the system's priority order. */ + private fun refresh() { + val track = watched.values.firstNotNullOfOrNull { (controller, _) -> toNowPlaying(controller) } + OtherAppsPlaybackRegistry.update(track) + } + + private fun toNowPlaying(controller: MediaController): NowPlaying? { + val state = controller.playbackState ?: return null + if (state.state != PlaybackState.STATE_PLAYING) return null + + val metadata = controller.metadata ?: return null + val title = + metadata.getString(MediaMetadata.METADATA_KEY_TITLE)?.ifBlank { null } + ?: metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)?.ifBlank { null } + ?: return null + val artist = + metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)?.ifBlank { null } + ?: metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)?.ifBlank { null } + ?: metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)?.ifBlank { null } + + val duration = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION).takeIf { it > 0 } + val speed = state.playbackSpeed.takeIf { it > 0f } ?: 1f + val position = + if (state.lastPositionUpdateTime > 0) { + state.position + ((SystemClock.elapsedRealtime() - state.lastPositionUpdateTime) * speed).toLong() + } else { + state.position + } + + return NowPlaying( + title = title, + artist = artist, + source = NowPlayingSource.OtherApp(controller.packageName, appLabel(controller.packageName)), + endsAt = NowPlaying.endsAt(TimeUtils.now(), duration, position, speed), + ) + } + + private fun appLabel(packageName: String): String = + try { + packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() + } catch (_: PackageManager.NameNotFoundException) { + packageName + } + + companion object { + fun hasAccess(context: Context): Boolean = context.packageName in NotificationManagerCompat.getEnabledListenerPackages(context) + + /** Opens the system screen where the user grants or revokes access to this listener. */ + fun openAccessSettings(context: Context) { + val component = ComponentName(context, OtherAppsNowPlayingService::class.java).flattenToString() + // Straight to this app's toggle where the system supports it, else the list of listeners. + val intents = + listOfNotNull( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS) + .putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME, component) + } else { + null + }, + Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS), + ) + + for (intent in intents) { + try { + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + return + } catch (e: ActivityNotFoundException) { + Log.w("OtherAppsNowPlaying", "No activity for ${intent.action}", e) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt index aa969245d4..888df68e76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt @@ -33,6 +33,7 @@ import androidx.media3.exoplayer.audio.AudioSink import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.audio.TeeAudioProcessor import com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache +import com.vitorpamplona.amethyst.service.nowPlaying.InAppPlaybackListener import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.playerPool.aspectRatio.AspectRatioCacher import com.vitorpamplona.amethyst.service.playback.playerPool.positions.CurrentPlayPositionCacher @@ -89,6 +90,7 @@ class ExoPlayerBuilder( addListener(KeepVideosPlaying(this)) addListener(CurrentPlayPositionCacher(this, VideoViewedPositionCache)) addListener(MediaPlayTimeTracker()) + addListener(InAppPlaybackListener()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt index 6e9c18c89c..c6de2f111e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt @@ -25,6 +25,7 @@ import androidx.annotation.OptIn import androidx.media3.common.C import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer +import com.vitorpamplona.amethyst.service.nowPlaying.InAppPlaybackRegistry import com.vitorpamplona.amethyst.service.playback.PLAYBACK_DIAG_TAG import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineExceptionHandler @@ -132,6 +133,7 @@ class ExoPlayerPool( if (error != null) { Log.d("PlaybackService") { "ExoPlayerPool discarding errored warm player: $preferredMediaId (${error.errorCodeName})" } PcmTapRegistry.unregisterPlayer(warm) + InAppPlaybackRegistry.forget(warm) warm.release() releaseDecoder() } else { @@ -239,6 +241,7 @@ class ExoPlayerPool( if (error != null) { Log.d("PlaybackService") { "ExoPlayerPool dropping errored player: ${player.currentMediaItem?.mediaId} (${error.errorCodeName})" } PcmTapRegistry.unregisterPlayer(player) + InAppPlaybackRegistry.forget(player) player.release() releaseDecoder() return@withLock @@ -309,6 +312,7 @@ class ExoPlayerPool( } } else { PcmTapRegistry.unregisterPlayer(player) + InAppPlaybackRegistry.forget(player) player.release() // Release if pool is full. } } @@ -345,11 +349,13 @@ class ExoPlayerPool( } warmSnapshot.forEach { PcmTapRegistry.unregisterPlayer(it.player) + InAppPlaybackRegistry.forget(it.player) it.player.release() releaseDecoder() } coldPool.forEach { PcmTapRegistry.unregisterPlayer(it) + InAppPlaybackRegistry.forget(it) it.release() } coldPool.clear() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b7271fb601..4bab06f572 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -170,6 +170,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWord import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MessagesSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MutedThreadsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NowPlayingSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ProfileUiSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen @@ -202,6 +203,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.WorkoutsSc import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages +import com.vitorpamplona.amethyst.service.nowPlaying.AndroidNowPlayingAccess import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthPromptHost import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages import com.vitorpamplona.amethyst.service.resourceusage.DisplayResourceUsageAlert @@ -653,6 +655,11 @@ fun BuildNavigation( composableFromEnd { ProfileUiSettingsScreen(accountViewModel, nav) } composableFromEnd { VideoPlayerSettingsScreen(accountViewModel, nav) } composableFromEnd { CallSettingsScreen(accountViewModel, nav) } + composableFromEnd { + val context = LocalContext.current + val access = remember(context) { AndroidNowPlayingAccess(context.applicationContext) } + NowPlayingSettingsScreen(accountViewModel, nav, access) + } composableFromEnd { NotificationSettingsScreen(accountViewModel, nav) } composableFromEnd { ResourceUsageScreen(accountViewModel, nav) } composableFromEnd { ImportFollowListSelectUserScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index e947d9f041..5190d02c82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -165,6 +165,7 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import org.jetbrains.compose.resources.StringResource @@ -355,7 +356,11 @@ private fun EditStatusBoxes( accountViewModel: AccountViewModel, nav: INav, ) { - val statuses by observeUserStatuses(baseAccountUser, accountViewModel) + val allStatuses by observeUserStatuses(baseAccountUser, accountViewModel) + + // The music status is written by the now-playing publisher, not typed here: editing it would + // overwrite the track and keep its expiration, so only the other statuses are editable. + val statuses = remember(allStatuses) { allStatuses.filter { it.address.dTag != UserStatusEvent.MUSIC } } if (statuses.isEmpty()) { PreviewStatusEditBar(accountViewModel = accountViewModel, nav = nav) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt index dca2865779..9b77f366a1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt @@ -93,6 +93,8 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupListS import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPacksState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46SignerState import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcInfoCache @@ -532,6 +534,11 @@ class Account( // channel list. Restored/persisted per account by BuzzChannelStarStore. val buzzChannelStars = BuzzChannelStars() + // Whether THIS account shares what the user listens to as its NIP-38 music status, and from + // which apps. Per account because the status is signed and published by this key. Restored/ + // persisted per account by NowPlayingSettingsStore on Android. + val nowPlayingSettings = NowPlayingSettingsState() + // The NIP-OA attestation an owner issued to THIS account's key, attached to its Buzz-relay // AUTH so the relay grants virtual membership. Restored/persisted per account by // BuzzAttestationStore. @@ -3345,6 +3352,13 @@ class Account( suspend fun deleteStatus(oldStatus: AddressableNote) = sendMyPublicAndPrivateOutbox(UserStatusAction.delete(oldStatus, signer)) + suspend fun publishNowPlaying( + track: NowPlaying, + expiration: Long, + ) = sendMyPublicAndPrivateOutbox(UserStatusAction.createMusic(track, expiration, signer)) + + suspend fun clearNowPlaying() = sendMyPublicAndPrivateOutbox(UserStatusAction.clearMusic(signer)) + suspend fun removeEmojiPack(emojiPack: Note) = sendMyPublicAndPrivateOutbox(emoji.removeEmojiPack(emojiPack)) suspend fun addEmojiPack(emojiPack: Note) = sendMyPublicAndPrivateOutbox(emoji.addEmojiPack(emojiPack)) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt index 70b8daf092..f2a0d2e832 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt @@ -585,6 +585,8 @@ sealed class Route { @Serializable object CallSettings : Route() + @Serializable object NowPlayingSettings : Route() + @Serializable object NotificationSettings : Route() @Serializable object CalendarReminderSettings : Route() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 4fac63e096..57499eb3cb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent @@ -34,6 +35,25 @@ class UserStatusAction { type: String = UserStatusEvent.GENERAL, ): UserStatusEvent = UserStatusEvent.create(newStatus, type, signer = signer) + /** A `music` status for [track] that expires at [expiration], when the track ends. */ + suspend fun createMusic( + track: NowPlaying, + expiration: Long, + signer: NostrSigner, + ): UserStatusEvent = + UserStatusEvent.create( + msg = track.statusText(), + type = UserStatusEvent.MUSIC, + expiration = expiration, + url = track.url, + eventId = track.eventId, + addressableId = track.addressId, + signer = signer, + ) + + /** Clears the `music` status: NIP-38 clears a status by replacing it with blank content. */ + suspend fun clearMusic(signer: NostrSigner): UserStatusEvent = UserStatusEvent.create("", UserStatusEvent.MUSIC, signer = signer) + suspend fun update( oldStatus: AddressableNote, newStatus: String, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt index fc04d0352a..c5a548cc31 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt @@ -42,11 +42,16 @@ class UserStatusCache : UserDependencies { ) fun addStatus(note: AddressableNote) { - // if it's already there, quick exit - if (statuses.value.contains(note) || note.isEmptyStatus()) return + // A newer version of a listed status can be blank (NIP-38 clears a status that way, and + // a music status is cleared every time playback stops) or already expired: the note is + // the same object, so it has to leave the list rather than be skipped as already there. + if (note.isEmptyStatus() || note.event?.isExpired() == true) { + removeStatus(note) + return + } - // don't add expired statuses - if (note.event?.isExpired() == true) return + // if it's already there, quick exit + if (statuses.value.contains(note)) return statuses.update { (it + note).sortedWith(sortModel).toImmutableList() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt new file mode 100644 index 0000000000..ca57ed9045 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt @@ -0,0 +1,93 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +/** Where a track is being played. */ +@Immutable +sealed interface NowPlayingSource { + /** Amethyst's own player (a music track or podcast episode opened in the app). */ + data object InApp : NowPlayingSource + + /** + * Another app on this device. [id] is stable across sessions (an Android package name, an + * MPRIS bus name, a Windows app id) and is what the per-app block list keys on; [label] is + * what the settings screen shows. + */ + data class OtherApp( + val id: String, + val label: String, + ) : NowPlayingSource +} + +/** + * A track (song or podcast episode) the user is playing right now, ready to become a NIP-38 + * `music` status. + * + * [endsAt] is the Unix time (seconds) the track will finish at its current position and speed, + * which becomes the status' NIP-40 expiration, as NIP-38 asks for music. It is null when the + * length is unknown (a live stream, an app that does not report a duration). + * + * [url], [addressId] and [eventId] become the status' `r`, `a` and `e` tags, so readers can open + * what is playing: in-app tracks point at their Nostr event. + */ +@Immutable +data class NowPlaying( + val title: String, + val artist: String? = null, + val source: NowPlayingSource, + val endsAt: Long? = null, + val url: String? = null, + val addressId: String? = null, + val eventId: HexKey? = null, +) { + /** The status line, in the `Title - Artist` shape of the NIP-38 example. */ + fun statusText(): String = if (artist.isNullOrBlank()) title else "$title - $artist" + + /** Same track, regardless of the playback position. */ + fun isSameTrack(other: NowPlaying): Boolean = + title == other.title && + artist == other.artist && + url == other.url && + addressId == other.addressId && + eventId == other.eventId + + companion object { + /** + * When a track [durationMs] long, now at [positionMs] and playing at [speed], ends, + * measured from [nowSeconds]. Null when the duration is unknown or the track already ended. + */ + fun endsAt( + nowSeconds: Long, + durationMs: Long?, + positionMs: Long, + speed: Float = 1f, + ): Long? { + if (durationMs == null || durationMs <= 0) return null + val remainingMs = durationMs - positionMs.coerceAtLeast(0) + if (remainingMs <= 0) return null + val effectiveSpeed = if (speed > 0f) speed else 1f + return nowSeconds + (remainingMs / effectiveSpeed / 1000).toLong() + 1 + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingAccess.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingAccess.kt new file mode 100644 index 0000000000..44d3f6db59 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingAccess.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +/** + * The OS permission that sharing other apps' playback depends on. On Android that is notification + * access, which only the user can grant in system settings; platforms that need nothing pass no + * port at all. + */ +interface NowPlayingAccess { + fun hasAccess(): Boolean + + /** Takes the user to where the access is granted or revoked. */ + fun requestAccess() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisher.kt new file mode 100644 index 0000000000..55a6899d0e --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisher.kt @@ -0,0 +1,156 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.abs + +/** + * Picks what to share out of the tracks each source reports, in priority order (the in-app + * player first), skipping the sources [settings] does not allow. + */ +fun pickNowPlaying( + settings: NowPlayingSettings, + vararg candidates: NowPlaying?, +): NowPlaying? = candidates.firstOrNull { it != null && settings.isAllowed(it.source) } + +/** + * Turns a stream of "what is playing now" (null when nothing is) into NIP-38 `music` statuses. + * + * - A new track is published only after it has played for [Config.startDelayMs], so skipping + * through a playlist does not sign and send an event per skipped song. + * - The status expires when the track ends (NIP-38 asks music statuses for that), so a crash or a + * dead battery never leaves a stale "listening to". When the length is unknown it expires after + * [Config.unknownLengthTtlSeconds] and is refreshed shortly before that while still playing. + * - A seek or a resume that moves the end by more than [Config.toleranceSeconds] republishes the + * same track with the corrected expiration. + * - When playback stops, the status is cleared (blank content, as NIP-38 says) after + * [Config.clearDelayMs], so a short pause or a buffering stall does not clear and re-post it. + * + * Runs on a single coroutine ([run]); a newer value cancels whatever wait the previous one was in. + */ +class NowPlayingPublisher( + private val publish: suspend (track: NowPlaying, expiration: Long) -> Unit, + private val clear: suspend () -> Unit, + private val nowSeconds: () -> Long = TimeUtils::now, + private val config: Config = Config(), +) { + data class Config( + val startDelayMs: Long = 5_000, + val clearDelayMs: Long = 30_000, + val unknownLengthTtlSeconds: Long = 600, + val refreshLeadSeconds: Long = 60, + val toleranceSeconds: Long = 20, + ) + + private var published: NowPlaying? = null + private var publishedExpiration: Long = 0 + + suspend fun run(tracks: Flow) { + tracks + .distinctUntilChanged(::isEquivalent) + .collectLatest { track -> + if (track == null) stopped() else playing(track) + } + } + + private fun isEquivalent( + old: NowPlaying?, + new: NowPlaying?, + ): Boolean = + when { + old == null || new == null -> old == new + old.source != new.source || !old.isSameTrack(new) -> false + else -> isSameEnd(old.endsAt, new.endsAt) + } + + private fun isSameEnd( + a: Long?, + b: Long?, + ) = if (a == null || b == null) a == b else abs(a - b) <= config.toleranceSeconds + + /** The status on relays still shows [track]. */ + private fun isLive(track: NowPlaying) = published?.isSameTrack(track) == true && publishedExpiration > nowSeconds() + + private suspend fun playing(track: NowPlaying) { + if (!isLive(track)) delay(config.startDelayMs) + + while (true) { + val now = nowSeconds() + val expiration = track.endsAt?.takeIf { it > now } ?: (now + config.unknownLengthTtlSeconds) + + if (!isLive(track) || abs(publishedExpiration - expiration) > config.toleranceSeconds) { + send(track, expiration) + } + + // A known end needs nothing more: the next track, a seek or a stop comes in as a new value. + if (track.endsAt != null) return + + val refreshIn = (publishedExpiration - nowSeconds() - config.refreshLeadSeconds).coerceAtLeast(MIN_REFRESH_SECONDS) + delay(refreshIn * 1000) + } + } + + private suspend fun stopped() { + if (published == null) return + + if (publishedExpiration <= nowSeconds()) { + // Relays already dropped it. + published = null + return + } + + delay(config.clearDelayMs) + + try { + clear() + published = null + publishedExpiration = 0 + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("NowPlayingPublisher", "Could not clear the music status", e) + } + } + + private suspend fun send( + track: NowPlaying, + expiration: Long, + ) { + try { + publish(track, expiration) + published = track + publishedExpiration = expiration + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("NowPlayingPublisher", "Could not publish the music status", e) + } + } + + companion object { + private const val MIN_REFRESH_SECONDS = 60L + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt new file mode 100644 index 0000000000..494e2bc298 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt @@ -0,0 +1,109 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote +import com.vitorpamplona.quartz.podcasts.PodcastEpisode + +/** + * Turns the Nostr event behind an in-app player into a [NowPlaying], or null when the event is + * not something to share as a listening status. + * + * Only music tracks (kind 36787), audio tracks (kind 31337) and podcast episodes (both drafts) + * qualify. Everything else the app plays (feed videos, voice messages, live streams) returns null, + * so autoplaying a clip never announces itself as "listening to". + * + * [fallbackTitle] and [fallbackArtist] are what the player shows (the event's title and the + * author's display name) and fill the gaps an event leaves. + */ +object NowPlayingResolver { + /** + * Resolves the `nostr:` URI the in-app player carries (`Note.toNostrUri()`) to its event with + * [findEvent] (given an event id or an `kind:pubkey:d` address) and then calls [fromEvent]. + */ + fun fromNostrUri( + uri: String, + findEvent: (key: String) -> Event?, + fallbackTitle: String? = null, + fallbackArtist: String? = null, + endsAt: Long? = null, + ): NowPlaying? { + val key = + when (val entity = Nip19Parser.uriToRoute(uri)?.entity) { + is NEvent -> entity.hex + is NNote -> entity.hex + is NAddress -> entity.aTag() + else -> null + } ?: return null + + val event = findEvent(key) ?: return null + return fromEvent(event, fallbackTitle, fallbackArtist, endsAt) + } + + fun fromEvent( + event: Event, + fallbackTitle: String? = null, + fallbackArtist: String? = null, + endsAt: Long? = null, + ): NowPlaying? { + val title: String? + val artist: String? + + when (event) { + is MusicTrackEvent -> { + title = event.title() + artist = event.artist() ?: fallbackArtist + } + + is AudioTrackEvent -> { + title = event.subject() + artist = fallbackArtist + } + + is PodcastEpisode -> { + title = event.episodeTitle() + artist = fallbackArtist + } + + else -> { + return null + } + } + + val bestTitle = title?.ifBlank { null } ?: fallbackTitle?.ifBlank { null } ?: return null + + return NowPlaying( + title = bestTitle, + artist = artist?.ifBlank { null }, + source = NowPlayingSource.InApp, + endsAt = endsAt, + addressId = (event as? AddressableEvent)?.address()?.toValue(), + eventId = if (event is AddressableEvent) null else event.id, + ) + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingSettings.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingSettings.kt new file mode 100644 index 0000000000..95139b35f0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingSettings.kt @@ -0,0 +1,89 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +/** + * What this account shares as its NIP-38 `music` status. Everything is off by default: a listening + * status is public, so the user opts in. + * + * - [shareInApp]: tracks and podcast episodes played in Amethyst. + * - [shareOtherApps]: whatever another app on this device reports through the OS media controls + * (needs a platform permission on Android), except the apps in [blockedApps]. + * - [knownApps]: the other apps seen playing so far (id to label), listed on the settings screen + * so the user can block one. + */ +@Immutable +data class NowPlayingSettings( + val shareInApp: Boolean = false, + val shareOtherApps: Boolean = false, + val blockedApps: Set = emptySet(), + val knownApps: Map = emptyMap(), +) { + fun isEnabled() = shareInApp || shareOtherApps + + fun isAllowed(source: NowPlayingSource): Boolean = + when (source) { + NowPlayingSource.InApp -> shareInApp + is NowPlayingSource.OtherApp -> shareOtherApps && source.id !in blockedApps + } +} + +/** + * Per-account holder for [NowPlayingSettings] (`Account.nowPlayingSettings`). The platform + * restores and persists it (Android: `NowPlayingSettingsStore`). + */ +@Stable +class NowPlayingSettingsState { + private val state = MutableStateFlow(NowPlayingSettings()) + + val flow: StateFlow = state + + fun restore(settings: NowPlayingSettings) = state.update { settings } + + fun setShareInApp(enabled: Boolean) = state.update { it.copy(shareInApp = enabled) } + + fun setShareOtherApps(enabled: Boolean) = state.update { it.copy(shareOtherApps = enabled) } + + fun setAppBlocked( + appId: String, + blocked: Boolean, + ) = state.update { it.copy(blockedApps = if (blocked) it.blockedApps + appId else it.blockedApps - appId) } + + /** Records an app seen playing, so the settings screen can offer to block it. */ + fun rememberApp( + appId: String, + label: String, + ) { + if (state.value.knownApps[appId] == label) return + state.update { it.copy(knownApps = it.knownApps + (appId to label)) } + } + + fun forgetApp(appId: String) = + state.update { + it.copy(knownApps = it.knownApps - appId, blockedApps = it.blockedApps - appId) + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingStatusService.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingStatusService.kt new file mode 100644 index 0000000000..cc82f52b05 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingStatusService.kt @@ -0,0 +1,59 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach + +/** + * Wires the platform's playback sources to a [NowPlayingPublisher] under one account's + * [settings]. [sources] are in priority order: the first one playing an allowed track wins (the + * in-app player goes first, since the user is looking at it). + * + * Every other app seen playing while [NowPlayingSettings.shareOtherApps] is on is reported to + * [onOtherAppSeen], so the settings screen can list it and let the user block it. + */ +class NowPlayingStatusService( + private val settings: StateFlow, + private val sources: List>, + private val onOtherAppSeen: (NowPlayingSource.OtherApp) -> Unit, + private val publisher: NowPlayingPublisher, +) { + suspend fun run() { + val observed = + sources.map { source -> + source.onEach { track -> + val app = track?.source as? NowPlayingSource.OtherApp + if (app != null && settings.value.shareOtherApps) onOtherAppSeen(app) + } + } + + val tracks = + combine(listOf(settings) + observed) { values -> + val current = values[0] as NowPlayingSettings + pickNowPlaying(current, *Array(values.size - 1) { values[it + 1] as NowPlaying? }) + } + + publisher.run(tracks) + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStore.kt new file mode 100644 index 0000000000..74da2d9b31 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStore.kt @@ -0,0 +1,108 @@ +/* + * 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.commons.model.preferences + +import androidx.compose.runtime.Stable +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringSetPreferencesKey +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +/** + * Per-account persistence for [NowPlayingSettings]. Keys are namespaced by pubkey because the + * status is published by that account. Loads the saved settings into [state] on construction, then + * writes every later change back. Construct once per account, eagerly. + */ +@Stable +class NowPlayingSettingsStore( + private val store: DataStore, + private val scope: CoroutineScope, + pubKeyHex: HexKey, + private val state: NowPlayingSettingsState, +) { + private val shareInApp = booleanPreferencesKey("$KEY_PREFIX$pubKeyHex.shareInApp") + private val shareOtherApps = booleanPreferencesKey("$KEY_PREFIX$pubKeyHex.shareOtherApps") + private val blockedApps = stringSetPreferencesKey("$KEY_PREFIX$pubKeyHex.blockedApps") + private val knownApps = stringSetPreferencesKey("$KEY_PREFIX$pubKeyHex.knownApps") + + init { + scope.launch { + restoreFromDisk() + // drop(1) skips the value present at collection start, which restoreFromDisk already wrote. + state.flow.drop(1).collect { persist(it) } + } + } + + private suspend fun restoreFromDisk() { + try { + val prefs = store.data.first() + state.restore( + NowPlayingSettings( + shareInApp = prefs[shareInApp] ?: false, + shareOtherApps = prefs[shareOtherApps] ?: false, + blockedApps = prefs[blockedApps] ?: emptySet(), + knownApps = decodeApps(prefs[knownApps] ?: emptySet()), + ), + ) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("NowPlayingSettingsStore") { "Error reading now playing settings: ${e.message}" } + } + } + + private suspend fun persist(value: NowPlayingSettings) { + try { + store.edit { prefs -> + prefs[shareInApp] = value.shareInApp + prefs[shareOtherApps] = value.shareOtherApps + prefs[blockedApps] = value.blockedApps + prefs[knownApps] = encodeApps(value.knownApps) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("NowPlayingSettingsStore") { "Error writing now playing settings: ${e.message}" } + } + } + + companion object { + private const val KEY_PREFIX = "nowPlaying." + private const val SEPARATOR = '\t' + + fun encodeApps(apps: Map): Set = apps.mapTo(mutableSetOf()) { (id, label) -> "$id$SEPARATOR$label" } + + fun decodeApps(raw: Set): Map = + raw + .mapNotNull { entry -> + val split = entry.indexOf(SEPARATOR) + if (split <= 0) null else entry.substring(0, split) to entry.substring(split + 1) + }.toMap() + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCacheTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCacheTest.kt new file mode 100644 index 0000000000..d175a52fb8 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCacheTest.kt @@ -0,0 +1,61 @@ +/* + * 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.commons.model.nip38UserStatuses + +import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.UserContext +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent +import kotlin.test.Test +import kotlin.test.assertEquals + +class UserStatusCacheTest { + private val pubKey = "b".repeat(64) + private val sig = "c".repeat(128) + private val author = User(pubKey, UserContext { addr -> AddressableNote(addr) }) + + private fun status( + id: Char, + content: String, + createdAt: Long, + ) = UserStatusEvent(id.toString().repeat(64), pubKey, createdAt, arrayOf(arrayOf("d", UserStatusEvent.MUSIC)), content, sig) + + @Test + fun aClearedStatusLeavesTheList() { + val cache = UserStatusCache() + val note = AddressableNote(Address(UserStatusEvent.KIND, pubKey, UserStatusEvent.MUSIC)) + + note.loadEvent(status('1', "Song - Band", 100), author, emptyList()) + cache.addStatus(note) + assertEquals(listOf(note), cache.statuses.value) + + // The same address now holds the newer, blank version: nothing to show any more. + note.loadEvent(status('2', "", 200), author, emptyList()) + cache.addStatus(note) + assertEquals(emptyList(), cache.statuses.value) + + // And a new track brings it back. + note.loadEvent(status('3', "Other Song - Band", 300), author, emptyList()) + cache.addStatus(note) + assertEquals(listOf(note), cache.statuses.value) + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt new file mode 100644 index 0000000000..0854204e8e --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt @@ -0,0 +1,215 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@OptIn(ExperimentalCoroutinesApi::class) +class NowPlayingPublisherTest { + private val base = 1_700_000_000L + + private class Recorder { + val published = mutableListOf>() + var clears = 0 + } + + private fun TestScope.now() = base + testScheduler.currentTime / 1000 + + private fun TestScope.start(source: MutableStateFlow): Recorder { + val recorder = Recorder() + val publisher = + NowPlayingPublisher( + publish = { track, expiration -> recorder.published.add(track to expiration) }, + clear = { recorder.clears++ }, + nowSeconds = { now() }, + ) + backgroundScope.launch { publisher.run(source) } + runCurrent() + return recorder + } + + private fun TestScope.track( + title: String, + remainingSeconds: Long? = 180, + ) = NowPlaying( + title = title, + artist = "Artist", + source = NowPlayingSource.InApp, + endsAt = remainingSeconds?.let { now() + it }, + ) + + @Test + fun publishesAfterTheStartDelayWithTheTrackEndAsExpiration() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + val song = track("Song") + source.value = song + advanceTimeBy(4_000) + assertEquals(0, recorder.published.size) + + advanceTimeBy(2_000) + assertEquals(1, recorder.published.size) + assertEquals(song, recorder.published[0].first) + assertEquals(song.endsAt, recorder.published[0].second) + } + + @Test + fun skippingThroughTracksOnlyPublishesTheOneThatKeepsPlaying() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("One") + advanceTimeBy(2_000) + source.value = track("Two") + advanceTimeBy(2_000) + source.value = track("Three") + advanceTimeBy(10_000) + + assertEquals(listOf("Three"), recorder.published.map { it.first.title }) + } + + @Test + fun aShortPauseNeitherClearsNorRepublishes() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("Song", remainingSeconds = 180) + advanceTimeBy(6_000) + assertEquals(1, recorder.published.size) + + source.value = null + advanceTimeBy(10_000) + // Resumed 10s later: the end moved by 10s, within the tolerance. + source.value = track("Song", remainingSeconds = 174) + advanceTimeBy(60_000) + + assertEquals(0, recorder.clears) + assertEquals(1, recorder.published.size) + } + + @Test + fun stoppingClearsTheStatusAfterTheClearDelay() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("Song") + advanceTimeBy(6_000) + source.value = null + advanceTimeBy(29_000) + assertEquals(0, recorder.clears) + + advanceTimeBy(2_000) + assertEquals(1, recorder.clears) + } + + @Test + fun aStatusThatAlreadyExpiredIsNotCleared() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("Short", remainingSeconds = 20) + advanceTimeBy(30_000) + source.value = null + advanceTimeBy(120_000) + + assertEquals(1, recorder.published.size) + assertEquals(0, recorder.clears) + } + + @Test + fun seekingRepublishesWithTheNewEnd() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("Song", remainingSeconds = 180) + advanceTimeBy(6_000) + val seeked = track("Song", remainingSeconds = 60) + source.value = seeked + runCurrent() + + assertEquals(2, recorder.published.size) + assertEquals(seeked.endsAt, recorder.published[1].second) + } + + @Test + fun anUnknownLengthIsRefreshedBeforeItExpires() = + runTest { + val source = MutableStateFlow(null) + val recorder = start(source) + + source.value = track("Radio", remainingSeconds = null) + advanceTimeBy(6_000) + assertEquals(1, recorder.published.size) + assertEquals(now() - 1 + 600, recorder.published[0].second) + + // Refreshed a minute before the 10-minute expiration. + advanceTimeBy(541_000) + assertEquals(2, recorder.published.size) + } + + @Test + fun pickHonorsSettingsAndTheBlockList() { + val inApp = NowPlaying("In app", source = NowPlayingSource.InApp) + val spotify = NowPlaying("Other", source = NowPlayingSource.OtherApp("com.spotify.music", "Spotify")) + + assertNull(pickNowPlaying(NowPlayingSettings(), inApp, spotify)) + assertEquals(inApp, pickNowPlaying(NowPlayingSettings(shareInApp = true, shareOtherApps = true), inApp, spotify)) + assertEquals(spotify, pickNowPlaying(NowPlayingSettings(shareOtherApps = true), inApp, spotify)) + assertEquals(spotify, pickNowPlaying(NowPlayingSettings(shareInApp = true, shareOtherApps = true), null, spotify)) + assertNull( + pickNowPlaying( + NowPlayingSettings(shareOtherApps = true, blockedApps = setOf("com.spotify.music")), + null, + spotify, + ), + ) + } + + @Test + fun statusTextFollowsTheNipExample() { + assertEquals("Intergalactic - Beastie Boys", NowPlaying("Intergalactic", "Beastie Boys", NowPlayingSource.InApp).statusText()) + assertEquals("Intergalactic", NowPlaying("Intergalactic", null, NowPlayingSource.InApp).statusText()) + } + + @Test + fun endsAtAccountsForPositionAndSpeed() { + assertEquals(1_000L + 60 + 1, NowPlaying.endsAt(1_000, durationMs = 180_000, positionMs = 120_000)) + assertEquals(1_000L + 30 + 1, NowPlaying.endsAt(1_000, durationMs = 180_000, positionMs = 120_000, speed = 2f)) + assertNull(NowPlaying.endsAt(1_000, durationMs = null, positionMs = 0)) + assertNull(NowPlaying.endsAt(1_000, durationMs = 1_000, positionMs = 2_000)) + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolverTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolverTest.kt new file mode 100644 index 0000000000..d09a83b28f --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolverTest.kt @@ -0,0 +1,73 @@ +/* + * 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.commons.model.nip38UserStatuses.nowPlaying + +import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class NowPlayingResolverTest { + private val id = "a".repeat(64) + private val pubKey = "b".repeat(64) + private val sig = "c".repeat(128) + + @Test + fun musicTrackPointsAtItsAddress() { + val event = + MusicTrackEvent( + id, + pubKey, + 1L, + arrayOf(arrayOf("d", "song"), arrayOf("title", "Song"), arrayOf("artist", "Band")), + "", + sig, + ) + + val track = NowPlayingResolver.fromEvent(event, fallbackArtist = "Uploader", endsAt = 10L)!! + + assertEquals("Song - Band", track.statusText()) + assertEquals("${MusicTrackEvent.KIND}:$pubKey:song", track.addressId) + assertNull(track.eventId) + assertEquals(10L, track.endsAt) + assertEquals(NowPlayingSource.InApp, track.source) + } + + @Test + fun podcastEpisodeUsesTheShowAsArtistAndPointsAtTheEvent() { + val event = PodcastEpisodeEvent(id, pubKey, 1L, arrayOf(arrayOf("title", "Episode 1")), "", sig) + + val track = NowPlayingResolver.fromEvent(event, fallbackArtist = "The Show")!! + + assertEquals("Episode 1 - The Show", track.statusText()) + assertEquals(id, track.eventId) + assertNull(track.addressId) + } + + @Test + fun otherEventsAreNotShared() { + val event = TextNoteEvent(id, pubKey, 1L, emptyArray(), "a video", sig) + + assertNull(NowPlayingResolver.fromEvent(event, fallbackTitle = "video", fallbackArtist = "someone")) + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStoreTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStoreTest.kt new file mode 100644 index 0000000000..e133edfa5a --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/preferences/NowPlayingSettingsStoreTest.kt @@ -0,0 +1,106 @@ +/* + * 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.commons.model.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringSetPreferencesKey +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import okio.Path.Companion.toOkioPath +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class NowPlayingSettingsStoreTest { + @get:Rule + val folder = TemporaryFolder() + + private fun raw(): DataStore { + val file = File(folder.root, "shared.preferences_pb") + return PreferenceDataStoreFactory.createWithPath( + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + produceFile = { file.toOkioPath() }, + ) + } + + @Test + fun appLabelsSurviveTheEncoding() { + val apps = mapOf("com.spotify.music" to "Spotify", "org.mpris.MediaPlayer2.vlc" to "VLC media player") + + assertEquals(apps, NowPlayingSettingsStore.decodeApps(NowPlayingSettingsStore.encodeApps(apps))) + assertEquals(emptyMap(), NowPlayingSettingsStore.decodeApps(setOf("no-separator", "\tno-id"))) + } + + @Test + fun settingsAreWrittenAndRestoredPerAccount() = + runBlocking { + val store = raw() + val expected = + NowPlayingSettings( + shareInApp = true, + shareOtherApps = true, + blockedApps = setOf("com.google.android.youtube"), + knownApps = mapOf("com.google.android.youtube" to "YouTube", "com.spotify.music" to "Spotify"), + ) + + val writerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val written = NowPlayingSettingsState() + NowPlayingSettingsStore(store, writerScope, "alice", written) + // Let the store finish its initial restore before changing anything. + withTimeout(5_000) { store.data.first() } + Thread.sleep(200) + written.setShareInApp(true) + written.setShareOtherApps(true) + written.rememberApp("com.google.android.youtube", "YouTube") + written.rememberApp("com.spotify.music", "Spotify") + written.setAppBlocked("com.google.android.youtube", true) + + withTimeout(5_000) { + // The block is the last change made, so once it is on disk everything is. + while (store.data.first()[stringSetPreferencesKey("nowPlaying.alice.blockedApps")].isNullOrEmpty()) Thread.sleep(20) + } + writerScope.cancel() + + val readerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val restored = NowPlayingSettingsState() + NowPlayingSettingsStore(store, readerScope, "alice", restored) + withTimeout(5_000) { restored.flow.first { it.isEnabled() } } + assertEquals(expected, restored.flow.value) + + // Another account on the same file starts from the defaults. + val other = NowPlayingSettingsState() + NowPlayingSettingsStore(store, readerScope, "bob", other) + Thread.sleep(200) + assertEquals(NowPlayingSettings(), other.flow.value) + readerScope.cancel() + } +} diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 9b2257719e..5a6a7e8a10 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -6362,4 +6362,17 @@ This file type can run code. Check that the name matches what you meant to get. Save From %1$s + Now Playing + music listening status song podcast spotify nip38 share playing + Share the song or podcast you are listening to as your status. It shows under your name for anyone who sees your profile, and disappears when the track ends or you stop playing. + Share what I play in Amethyst + Music tracks and podcast episodes played in the app. + Share what I play in other apps + Spotify, YouTube Music, podcast players and any other app that shows media controls. + To see what other apps are playing, Amethyst needs notification access. It only reads media controls, never the content of your notifications. + Grant access + Manage access + Notification access is on. + Apps + Apps show up here after they play something. Turn one off to stop sharing it. diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt new file mode 100644 index 0000000000..2ac357765c --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt @@ -0,0 +1,238 @@ +/* + * 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.commons.ui.screen.loggedIn.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.LifecycleResumeEffect +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingAccess +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.now_playing_access_granted +import com.vitorpamplona.amethyst.commons.resources.now_playing_access_needed +import com.vitorpamplona.amethyst.commons.resources.now_playing_apps +import com.vitorpamplona.amethyst.commons.resources.now_playing_apps_empty +import com.vitorpamplona.amethyst.commons.resources.now_playing_description +import com.vitorpamplona.amethyst.commons.resources.now_playing_grant_access +import com.vitorpamplona.amethyst.commons.resources.now_playing_manage_access +import com.vitorpamplona.amethyst.commons.resources.now_playing_settings +import com.vitorpamplona.amethyst.commons.resources.now_playing_share_in_app +import com.vitorpamplona.amethyst.commons.resources.now_playing_share_in_app_description +import com.vitorpamplona.amethyst.commons.resources.now_playing_share_other_apps +import com.vitorpamplona.amethyst.commons.resources.now_playing_share_other_apps_description +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel + +/** + * Settings for sharing what the user listens to as their NIP-38 music status. + * + * [access] is the platform permission other apps' playback needs (Android: notification access); + * null where none is needed. + */ +@Composable +fun NowPlayingSettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, + access: NowPlayingAccess? = null, +) { + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(Res.string.now_playing_settings), nav) + }, + ) { padding -> + Column( + Modifier + .padding(padding) + .verticalScroll(rememberScrollState()), + ) { + NowPlayingSettingsContent(accountViewModel.account.nowPlayingSettings, access) + } + } +} + +/** + * The body of the settings screen, also embedded by front ends with their own settings layout. + * [showInApp] hides the in-app toggle where the app has no music or podcast player of its own. + */ +@Composable +fun NowPlayingSettingsContent( + state: NowPlayingSettingsState, + access: NowPlayingAccess?, + showInApp: Boolean = true, +) { + val settings by state.flow.collectAsState() + + Text( + text = stringRes(Res.string.now_playing_description), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + ) + + if (showInApp) { + SwitchRow( + title = stringRes(Res.string.now_playing_share_in_app), + description = stringRes(Res.string.now_playing_share_in_app_description), + checked = settings.shareInApp, + onCheckedChange = state::setShareInApp, + ) + } + + SwitchRow( + title = stringRes(Res.string.now_playing_share_other_apps), + description = stringRes(Res.string.now_playing_share_other_apps_description), + checked = settings.shareOtherApps, + onCheckedChange = state::setShareOtherApps, + ) + + if (!settings.shareOtherApps) { + Spacer(modifier = Modifier.height(16.dp)) + return + } + + if (access != null) { + AccessRow(access) + } + + HorizontalDivider(thickness = 4.dp, modifier = Modifier.padding(vertical = 8.dp)) + + Text( + text = stringRes(Res.string.now_playing_apps), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp), + ) + + if (settings.knownApps.isEmpty()) { + Text( + text = stringRes(Res.string.now_playing_apps_empty), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) + } else { + settings.knownApps.entries + .sortedBy { it.value.lowercase() } + .forEach { (id, label) -> + SwitchRow( + title = label, + description = id.takeIf { it != label }, + checked = id !in settings.blockedApps, + onCheckedChange = { allowed -> state.setAppBlocked(id, !allowed) }, + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +private fun AccessRow(access: NowPlayingAccess) { + // The user grants access in system settings and comes back, so check again on every resume. + var granted by remember(access) { mutableStateOf(access.hasAccess()) } + LifecycleResumeEffect(access) { + granted = access.hasAccess() + onPauseOrDispose {} + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(if (granted) Res.string.now_playing_access_granted else Res.string.now_playing_access_needed), + fontSize = 13.sp, + color = if (granted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.error, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(16.dp)) + OutlinedButton(onClick = access::requestAccess) { + Text(stringRes(if (granted) Res.string.now_playing_manage_access else Res.string.now_playing_grant_access)) + } + } +} + +@Composable +private fun SwitchRow( + title: String, + description: String?, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + ) + if (description != null) { + Text( + text = description, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + ) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index 0bf5bee248..35f29cf0cc 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -67,6 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_search_keyword import com.vitorpamplona.amethyst.commons.resources.nests_servers_title import com.vitorpamplona.amethyst.commons.resources.notification_settings import com.vitorpamplona.amethyst.commons.resources.notification_settings_search_keywords +import com.vitorpamplona.amethyst.commons.resources.now_playing_search_keywords +import com.vitorpamplona.amethyst.commons.resources.now_playing_settings import com.vitorpamplona.amethyst.commons.resources.ots_explorer_search_keywords import com.vitorpamplona.amethyst.commons.resources.ots_explorer_settings import com.vitorpamplona.amethyst.commons.resources.payment_targets @@ -160,6 +162,7 @@ fun buildSettingsCatalog( symEntry(Res.string.drawer_settings, MaterialSymbols.AutoMirrored.ViewList, Res.string.drawer_search_keywords, Route.DrawerSettings), symEntry(Res.string.video_player_settings, MaterialSymbols.VideoSettings, Res.string.video_player_search_keywords, Route.VideoPlayerSettings), symEntry(Res.string.audio_visualizer_settings, MaterialSymbols.MusicNote, Res.string.audio_visualizer_search_keywords, Route.AudioVisualizerSettings), + symEntry(Res.string.now_playing_settings, MaterialSymbols.MusicNote, Res.string.now_playing_search_keywords, Route.NowPlayingSettings), symEntry(Res.string.favorite_dvms_title, MaterialSymbols.AutoAwesome, Res.string.favorite_dvms_search_keywords, Route.EditFavoriteAlgoFeeds), symEntry(Res.string.profile_badges_title, MaterialSymbols.MilitaryTech, Res.string.profile_badges_search_keywords, Route.ProfileBadges), symEntry(Res.string.payment_targets, MaterialSymbols.Payment, Res.string.payment_targets_search_keywords, Route.EditPaymentTargets), diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 88859f79c6..42ceaf2b33 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccoun import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus import com.vitorpamplona.amethyst.commons.tor.TorType +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NowPlayingSettingsContent import com.vitorpamplona.amethyst.commons.wot.LocalWoTReady import com.vitorpamplona.amethyst.commons.wot.LocalWoTService import com.vitorpamplona.amethyst.desktop.account.AccountManager @@ -109,6 +110,9 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.model.DesktopRelayCategories import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.network.Nip11Fetcher +import com.vitorpamplona.amethyst.desktop.nowPlaying.DesktopNowPlayingCoordinator +import com.vitorpamplona.amethyst.desktop.nowPlaying.DesktopNowPlayingSettings +import com.vitorpamplona.amethyst.desktop.nowPlaying.OsNowPlayingReader import com.vitorpamplona.amethyst.desktop.platform.PlatformInfo import com.vitorpamplona.amethyst.desktop.platform.applyNativeWindowChrome import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore @@ -1459,6 +1463,18 @@ private fun AppInner( onDispose { iAccount.wotService.close() } } + // Shares what other apps on this computer play as the account's NIP-38 + // music status, once the user turns it on in Settings > Now Playing. + if (!account.isReadOnly) { + LaunchedEffect(iAccount) { + DesktopNowPlayingCoordinator( + settings = DesktopNowPlayingSettings.forAccount(account.pubKeyHex), + signer = iAccount.signer, + publishEvent = { relayManager.publish(it, iAccount.outboxHomeRelays()) }, + ).run() + } + } + // Lazy-load Namecoin services. The Core RPC HTTP // client is sourced from the Tor-aware DesktopHttpClient // singleton so .onion RPC URLs route through the @@ -2724,6 +2740,35 @@ private fun settingsEntries( }, ) + if (OsNowPlayingReader.forThisOs() != null) { + add( + SettingsEntry( + SettingsMeta( + id = "nowPlaying", + icon = MaterialSymbols.MusicNote, + title = "Now Playing", + subtitle = "Share what you listen to as your status", + keywords = + persistentListOf( + "music", + "listening", + "status", + "song", + "podcast", + "spotify", + "nip-38", + ), + ), + ) { + NowPlayingSettingsContent( + state = DesktopNowPlayingSettings.forAccount(account.pubKeyHex), + access = null, + showInApp = false, + ) + }, + ) + } + add( SettingsEntry( SettingsMeta( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt new file mode 100644 index 0000000000..911f74aada --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt @@ -0,0 +1,90 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingPublisher +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingStatusService +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Desktop: shares what another app on this computer is playing as the account's NIP-38 music + * status. The OS is polled every [pollMs] only while the account has + * [com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings.shareOtherApps] + * on, so nothing is spawned (and macOS never asks for Automation access) until the user opts in. + * + * Desktop has no music or podcast player of its own, so there is no in-app source here. + */ +class DesktopNowPlayingCoordinator( + private val settings: NowPlayingSettingsState, + private val signer: NostrSigner, + private val publishEvent: (Event) -> Unit, + private val reader: OsNowPlayingReader? = OsNowPlayingReader.forThisOs(), + private val pollMs: Long = 15_000, +) { + /** Runs until the calling coroutine (the logged-in account's composition) is cancelled. */ + suspend fun run() { + val reader = reader ?: return + + coroutineScope { + val otherApps = MutableStateFlow(null) + + launch { + settings.flow + .map { it.shareOtherApps } + .distinctUntilChanged() + .collectLatest { enabled -> + if (!enabled) { + otherApps.value = null + return@collectLatest + } + while (true) { + otherApps.value = reader.read() + delay(pollMs) + } + } + } + + val publisher = + NowPlayingPublisher( + publish = { track, expiration -> publishEvent(UserStatusAction.createMusic(track, expiration, signer)) }, + clear = { publishEvent(UserStatusAction.clearMusic(signer)) }, + ) + + NowPlayingStatusService( + settings = settings.flow, + sources = listOf(otherApps), + onOtherAppSeen = { settings.rememberApp(it.id, it.label) }, + publisher = publisher, + ).run() + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingSettings.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingSettings.kt new file mode 100644 index 0000000000..a5c7af3dc1 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingSettings.kt @@ -0,0 +1,78 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.amethyst.commons.model.preferences.NowPlayingSettingsStore +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch +import java.util.prefs.Preferences + +/** + * Desktop's per-account [NowPlayingSettingsState]s, persisted with `java.util.prefs` like the + * rest of the desktop preferences (the DataStore-backed `NowPlayingSettingsStore` is Android's). + * One state per pubkey for the life of the process, so the settings card and the publisher share it. + */ +object DesktopNowPlayingSettings { + private val prefs: Preferences = Preferences.userNodeForPackage(DesktopNowPlayingSettings::class.java) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val states = mutableMapOf() + + @Synchronized + fun forAccount(pubKeyHex: HexKey): NowPlayingSettingsState = + states.getOrPut(pubKeyHex) { + NowPlayingSettingsState().also { state -> + state.restore(load(pubKeyHex)) + scope.launch { state.flow.drop(1).collect { save(pubKeyHex, it) } } + } + } + + private fun key( + pubKeyHex: HexKey, + name: String, + ) = "nowPlaying.${pubKeyHex.take(16)}.$name" + + private fun load(pubKeyHex: HexKey) = + NowPlayingSettings( + shareInApp = false, + shareOtherApps = prefs.getBoolean(key(pubKeyHex, "shareOtherApps"), false), + blockedApps = split(prefs.get(key(pubKeyHex, "blockedApps"), "")).toSet(), + knownApps = NowPlayingSettingsStore.decodeApps(split(prefs.get(key(pubKeyHex, "knownApps"), "")).toSet()), + ) + + private fun save( + pubKeyHex: HexKey, + settings: NowPlayingSettings, + ) { + prefs.putBoolean(key(pubKeyHex, "shareOtherApps"), settings.shareOtherApps) + prefs.put(key(pubKeyHex, "blockedApps"), settings.blockedApps.joinToString(LINE)) + prefs.put(key(pubKeyHex, "knownApps"), NowPlayingSettingsStore.encodeApps(settings.knownApps).joinToString(LINE)) + } + + private fun split(value: String) = value.split(LINE).filter { it.isNotEmpty() } + + private const val LINE = "\n" +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MacNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MacNowPlayingReader.kt new file mode 100644 index 0000000000..aca2f75602 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MacNowPlayingReader.kt @@ -0,0 +1,98 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * macOS: the system-wide "Now Playing" is a private framework apps can no longer read (macOS + * 15.4+), so this asks the two players that matter most, Spotify and Apple Music, over + * AppleScript. Each is only asked while it is running: addressing an app that is not running + * would launch it. The first time, macOS asks the user to allow Amethyst to control the player. + */ +class MacNowPlayingReader( + private val run: suspend (List) -> String? = { runCommand(it) }, + private val nowSeconds: () -> Long = TimeUtils::now, +) : OsNowPlayingReader { + private class Player( + val bundleId: String, + val label: String, + val executable: String, + // Spotify reports the track duration in milliseconds, Music in seconds. + val durationInMs: Boolean, + ) + + override suspend fun read(): NowPlaying? { + val running = run(listOf("ps", "-A", "-o", "comm=")) ?: return null + + for (player in PLAYERS) { + if (!running.contains(player.executable)) continue + val output = run(listOf("osascript", "-e", script(player))) ?: continue + return parse(player.bundleId, player.label, output, player.durationInMs, nowSeconds()) ?: continue + } + + return null + } + + companion object { + private val PLAYERS = + listOf( + Player("com.spotify.client", "Spotify", "Spotify.app/Contents/MacOS/Spotify", durationInMs = true), + Player("com.apple.Music", "Music", "Music.app/Contents/MacOS/Music", durationInMs = false), + ) + + private fun script(player: Player) = + """ + tell application id "${player.bundleId}" + if player state is playing then + return (name of current track) & tab & (artist of current track) & tab & (duration of current track) & tab & (player position) + end if + end tell + return "" + """.trimIndent() + + /** Parses `titleartistdurationposition seconds`; numbers may use a decimal comma. */ + fun parse( + id: String, + label: String, + output: String, + durationInMs: Boolean, + nowSeconds: Long, + ): NowPlaying? { + val fields = output.trimEnd('\n', '\r').split('\t') + if (fields.size < 4) return null + + val title = fields[0].ifBlank { null } ?: return null + val duration = fields[2].replace(',', '.').toDoubleOrNull() + val durationMs = duration?.let { if (durationInMs) it.toLong() else (it * 1000).toLong() } + val positionMs = fields[3].replace(',', '.').toDoubleOrNull()?.let { (it * 1000).toLong() } ?: 0L + + return NowPlaying( + title = title, + artist = fields[1].ifBlank { null }, + source = NowPlayingSource.OtherApp(id, label), + endsAt = NowPlaying.endsAt(nowSeconds, durationMs, positionMs), + ) + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt new file mode 100644 index 0000000000..d9b4910150 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt @@ -0,0 +1,190 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Linux/BSD: asks every MPRIS player on the session bus (Spotify, VLC, Rhythmbox, browsers, ...) + * what it is playing, through the `dbus-send` tool that ships with D-Bus itself, so no D-Bus + * library is linked in. + */ +class MprisNowPlayingReader( + private val run: suspend (List) -> String? = { runCommand(it) }, + private val nowSeconds: () -> Long = TimeUtils::now, +) : OsNowPlayingReader { + override suspend fun read(): NowPlaying? { + val names = run(LIST_NAMES)?.let(MprisParser::playerNames).orEmpty() + + for (busName in names) { + val properties = run(getAll(busName))?.let(MprisParser::properties) ?: continue + if (properties["PlaybackStatus"]?.firstOrNull() != "Playing") continue + + val identity = run(getIdentity(busName))?.let(MprisParser::properties)?.get("Identity")?.firstOrNull() + return MprisParser.toNowPlaying(busName, identity, properties, nowSeconds()) + } + + return null + } + + companion object { + private val LIST_NAMES = + listOf( + "dbus-send", + "--session", + "--print-reply", + "--dest=org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus.ListNames", + ) + + private fun getAll(busName: String) = + listOf( + "dbus-send", + "--session", + "--print-reply", + "--dest=$busName", + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties.GetAll", + "string:org.mpris.MediaPlayer2.Player", + ) + + private fun getIdentity(busName: String) = + listOf( + "dbus-send", + "--session", + "--print-reply", + "--dest=$busName", + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties.GetAll", + "string:org.mpris.MediaPlayer2", + ) + } +} + +/** Parses the text `dbus-send --print-reply` prints for the MPRIS calls above. */ +object MprisParser { + private const val PREFIX = "org.mpris.MediaPlayer2." + private val INSTANCE_SUFFIX = Regex("""\.instance_?\d+(_\d+)?$""") + + fun playerNames(output: String): List = + output + .lineSequence() + .map { it.trim() } + .filter { it.startsWith("string \"$PREFIX") } + .map { unquote(it.removePrefix("string ")) } + .toList() + + /** The player id the block list keys on: `org.mpris.MediaPlayer2.vlc.instance123` is `vlc`. */ + fun playerId(busName: String): String = busName.removePrefix(PREFIX).replace(INSTANCE_SUFFIX, "") + + /** + * Flattens every `dict entry` of a GetAll reply, nested ones included (the track metadata is + * a dictionary inside the `Metadata` entry), into key -> values. Scalars have one value; + * string arrays such as `xesam:artist` keep all of them. + */ + fun properties(output: String): Map> { + val result = mutableMapOf>() + var expectKey = false + var key: String? = null + var arrayKey: String? = null + + output.lineSequence().map { it.trim() }.forEach { line -> + when { + line.startsWith("dict entry(") -> { + expectKey = true + } + + expectKey && line.startsWith("string ") -> { + key = unquote(line.removePrefix("string ")) + expectKey = false + } + + line.startsWith("variant") -> { + val currentKey = key ?: return@forEach + val value = line.removePrefix("variant").trim() + if (value.startsWith("array [")) { + // Either a string list (artists) or a nested dictionary (Metadata); + // a nested dictionary's entries are picked up by the branches above. + arrayKey = currentKey + } else { + scalar(value)?.let { result.getOrPut(currentKey) { mutableListOf() }.add(it) } + } + key = null + } + + line == "]" -> { + arrayKey = null + } + + arrayKey != null && line.startsWith("string ") -> { + result.getOrPut(arrayKey!!) { mutableListOf() }.add(unquote(line.removePrefix("string "))) + } + } + } + + return result + } + + fun toNowPlaying( + busName: String, + identity: String?, + properties: Map>, + nowSeconds: Long, + ): NowPlaying? { + val title = properties["xesam:title"]?.firstOrNull()?.ifBlank { null } ?: return null + val artist = + properties["xesam:artist"]?.filter { it.isNotBlank() }?.joinToString(", ")?.ifBlank { null } + ?: properties["xesam:albumArtist"]?.firstOrNull()?.ifBlank { null } + + // MPRIS times are microseconds. + val durationMs = properties["mpris:length"]?.firstOrNull()?.toLongOrNull()?.div(1000) + val positionMs = properties["Position"]?.firstOrNull()?.toLongOrNull()?.div(1000) ?: 0L + val rate = properties["Rate"]?.firstOrNull()?.toFloatOrNull() ?: 1f + + val id = playerId(busName) + val label = identity?.ifBlank { null } ?: id.replaceFirstChar { it.uppercase() } + + return NowPlaying( + title = title, + artist = artist, + source = NowPlayingSource.OtherApp(id, label), + endsAt = NowPlaying.endsAt(nowSeconds, durationMs, positionMs, rate), + url = properties["xesam:url"]?.firstOrNull()?.takeIf { it.startsWith("https://") }, + ) + } + + /** `string "x"`, `object path "/x"`, `int64 5`, `double 1`, `boolean true` -> the value. */ + private fun scalar(value: String): String? = + when { + value.startsWith("string ") -> unquote(value.removePrefix("string ")) + value.startsWith("object path ") -> unquote(value.removePrefix("object path ")) + else -> value.substringAfter(' ', "").ifBlank { null } + } + + private fun unquote(value: String): String { + val start = value.indexOf('"') + val end = value.lastIndexOf('"') + return if (start >= 0 && end > start) value.substring(start + 1, end) else value + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt new file mode 100644 index 0000000000..a3f131c7d5 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt @@ -0,0 +1,75 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit + +/** Reads what the OS says another app is playing right now. */ +interface OsNowPlayingReader { + /** The track a desktop app is playing, or null when nothing is (or the OS cannot say). */ + suspend fun read(): NowPlaying? + + companion object { + /** The reader for this OS, or null where there is none. */ + fun forThisOs(osName: String = System.getProperty("os.name").orEmpty()): OsNowPlayingReader? { + val os = osName.lowercase() + return when { + os.contains("linux") || os.contains("bsd") -> MprisNowPlayingReader() + os.contains("mac") -> MacNowPlayingReader() + os.contains("windows") -> WindowsNowPlayingReader() + else -> null + } + } + } +} + +/** Runs a short-lived command and returns its stdout, or null if it failed or took too long. */ +internal suspend fun runCommand( + command: List, + timeoutSeconds: Long = 5, +): String? = + withContext(Dispatchers.IO) { + try { + val process = + ProcessBuilder(command) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start() + process.outputStream.close() + // Wait before reading so a hung command cannot block the read forever. The outputs + // here are a few KB, well under the pipe buffer, so the command never blocks on it. + if (!process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { + process.destroyForcibly() + null + } else if (process.exitValue() != 0) { + null + } else { + process.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + } + } catch (e: Exception) { + // Missing binary (no dbus-send, no osascript) or a denied permission: nothing to report. + Log.d("OsNowPlayingReader") { "${command.first()} failed: ${e.message}" } + null + } + } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/WindowsNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/WindowsNowPlayingReader.kt new file mode 100644 index 0000000000..a48ee1e5b9 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/WindowsNowPlayingReader.kt @@ -0,0 +1,108 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource +import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.Base64 + +/** + * Windows: reads the system media transport controls (the media flyout every player feeds: + * Spotify, the Media Player app, browsers, ...) through the WinRT + * `GlobalSystemMediaTransportControlsSessionManager`, called from the built-in Windows PowerShell + * since the JVM has no WinRT bridge. + */ +class WindowsNowPlayingReader( + private val run: suspend (List) -> String? = { runCommand(it, timeoutSeconds = 10) }, + private val nowSeconds: () -> Long = TimeUtils::now, +) : OsNowPlayingReader { + override suspend fun read(): NowPlaying? { + val output = run(listOf("powershell.exe", "-NoProfile", "-NonInteractive", "-EncodedCommand", ENCODED_SCRIPT)) ?: return null + return parse(output, nowSeconds()) + } + + companion object { + private val SCRIPT = + """ + ${'$'}ErrorActionPreference = 'Stop' + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + Add-Type -AssemblyName System.Runtime.WindowsRuntime + ${'$'}asTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { ${'$'}_.Name -eq 'AsTask' -and ${'$'}_.GetParameters().Count -eq 1 -and ${'$'}_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0] + function Await(${'$'}op, [Type]${'$'}type) { ${'$'}task = ${'$'}asTask.MakeGenericMethod(${'$'}type).Invoke(${'$'}null, @(${'$'}op)); ${'$'}task.Wait(-1) | Out-Null; ${'$'}task.Result } + [void][Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager, Windows.Media.Control, ContentType = WindowsRuntime] + ${'$'}manager = Await ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]::RequestAsync()) ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]) + foreach (${'$'}session in ${'$'}manager.GetSessions()) { + if (${'$'}session.GetPlaybackInfo().PlaybackStatus -ne 'Playing') { continue } + ${'$'}props = Await (${'$'}session.TryGetMediaPropertiesAsync()) ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties]) + ${'$'}timeline = ${'$'}session.GetTimelineProperties() + ${'$'}length = [long](${'$'}timeline.EndTime - ${'$'}timeline.StartTime).TotalMilliseconds + ${'$'}position = [long]${'$'}timeline.Position.TotalMilliseconds + [Console]::Out.WriteLine((${'$'}session.SourceAppUserModelId, ${'$'}props.Title, ${'$'}props.Artist, ${'$'}length, ${'$'}position) -join "`t") + break + } + """.trimIndent() + + // -EncodedCommand takes UTF-16LE in Base64, which sidesteps every quoting rule of the command line. + private val ENCODED_SCRIPT: String = Base64.getEncoder().encodeToString(SCRIPT.toByteArray(Charsets.UTF_16LE)) + + /** Parses `appIdtitleartistlengthMspositionMs`. */ + fun parse( + output: String, + nowSeconds: Long, + ): NowPlaying? { + val fields = + output + .lineSequence() + .map { it.trimEnd('\r') } + .firstOrNull { it.isNotBlank() } + ?.split('\t') ?: return null + if (fields.size < 5) return null + + val appId = fields[0].ifBlank { null } ?: return null + val title = fields[1].ifBlank { null } ?: return null + val durationMs = fields[3].toLongOrNull()?.takeIf { it > 0 } + val positionMs = fields[4].toLongOrNull() ?: 0L + + return NowPlaying( + title = title, + artist = fields[2].ifBlank { null }, + source = NowPlayingSource.OtherApp(appId, appLabel(appId)), + endsAt = NowPlaying.endsAt(nowSeconds, durationMs, positionMs), + ) + } + + /** + * A readable name for an app user model id: `Spotify.exe` is `Spotify`, + * `Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic` is `ZuneMusic`. + */ + fun appLabel(appId: String): String { + val app = appId.substringAfterLast('!').substringBefore('_') + val name = + app + .removeSuffix(".exe") + .removeSuffix(".EXE") + .substringAfterLast('\\') + .substringAfterLast('.') + return name.ifBlank { appId } + } + } +} diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt new file mode 100644 index 0000000000..4fc575d62a --- /dev/null +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt @@ -0,0 +1,201 @@ +/* + * 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.desktop.nowPlaying + +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class OsNowPlayingReaderTest { + private val listNames = + """ + method return time=1700000000.1 sender=org.freedesktop.DBus -> destination=:1.99 serial=3 reply_serial=2 + array [ + string "org.freedesktop.DBus" + string ":1.7" + string "org.mpris.MediaPlayer2.vlc.instance4242" + string "org.mpris.MediaPlayer2.spotify" + ] + """.trimIndent() + + private val vlcPaused = + """ + method return time=1700000000.2 sender=:1.50 -> destination=:1.99 serial=10 reply_serial=2 + array [ + dict entry( + string "PlaybackStatus" + variant string "Paused" + ) + ] + """.trimIndent() + + private val spotifyPlaying = + """ + method return time=1700000000.3 sender=:1.51 -> destination=:1.99 serial=11 reply_serial=2 + array [ + dict entry( + string "PlaybackStatus" + variant string "Playing" + ) + dict entry( + string "Metadata" + variant array [ + dict entry( + string "mpris:trackid" + variant object path "/com/spotify/track/0DiWol3AO6WpXZgp0goxAV" + ) + dict entry( + string "mpris:length" + variant uint64 320000000 + ) + dict entry( + string "xesam:artist" + variant array [ + string "Daft Punk" + string "Romanthony" + ] + ) + dict entry( + string "xesam:title" + variant string "One More Time "Live" – Café" + ) + dict entry( + string "xesam:url" + variant string "https://open.spotify.com/track/0DiWol3AO6WpXZgp0goxAV" + ) + ] + ) + dict entry( + string "Position" + variant int64 20000000 + ) + dict entry( + string "Rate" + variant double 1 + ) + ] + """.trimIndent() + + private val spotifyIdentity = + """ + method return time=1700000000.4 sender=:1.51 -> destination=:1.99 serial=12 reply_serial=2 + array [ + dict entry( + string "Identity" + variant string "Spotify" + ) + dict entry( + string "SupportedMimeTypes" + variant array [ + ] + ) + ] + """.trimIndent() + + @Test + fun mprisFindsThePlayingPlayer() = + runTest { + val reader = + MprisNowPlayingReader( + run = { command -> + val dest = command.firstOrNull { it.startsWith("--dest=") }?.removePrefix("--dest=") + when { + dest == "org.freedesktop.DBus" -> listNames + dest == "org.mpris.MediaPlayer2.vlc.instance4242" -> vlcPaused + command.last() == "string:org.mpris.MediaPlayer2.Player" -> spotifyPlaying + else -> spotifyIdentity + } + }, + nowSeconds = { 1_000L }, + ) + + val track = reader.read()!! + + assertEquals("One More Time \"Live\" – Café", track.title) + assertEquals("Daft Punk, Romanthony", track.artist) + assertEquals(NowPlayingSource.OtherApp("spotify", "Spotify"), track.source) + // 320s long, 20s in. + assertEquals(1_000L + 300 + 1, track.endsAt) + assertEquals("https://open.spotify.com/track/0DiWol3AO6WpXZgp0goxAV", track.url) + } + + /** + * Runs the real reader, `dbus-send` included, against a live session bus. Opt-in: start an + * MPRIS player (or a fake one) under `dbus-run-session` and set MPRIS_LIVE_TITLE to its title. + */ + @Test + fun mprisReadsALiveSessionBus() = + runTest { + val expectedTitle = System.getenv("MPRIS_LIVE_TITLE") ?: return@runTest + assertEquals(expectedTitle, MprisNowPlayingReader().read()?.title) + } + + @Test + fun mprisReportsNothingWhenNoneIsPlaying() = + runTest { + val reader = + MprisNowPlayingReader( + run = { command -> if (command.any { it == "--dest=org.freedesktop.DBus" }) listNames else vlcPaused }, + ) + + assertNull(reader.read()) + } + + @Test + fun mprisPlayerIdDropsTheInstanceSuffix() { + assertEquals("vlc", MprisParser.playerId("org.mpris.MediaPlayer2.vlc.instance4242")) + assertEquals("chromium", MprisParser.playerId("org.mpris.MediaPlayer2.chromium.instance_1_23")) + assertEquals("spotify", MprisParser.playerId("org.mpris.MediaPlayer2.spotify")) + } + + @Test + fun macParsesSpotifyAndMusicUnits() { + val spotify = MacNowPlayingReader.parse("com.spotify.client", "Spotify", "Song\tBand\t200000\t50,5\n", durationInMs = true, nowSeconds = 0)!! + assertEquals("Song", spotify.title) + assertEquals("Band", spotify.artist) + assertEquals(149L + 1, spotify.endsAt) + + val music = MacNowPlayingReader.parse("com.apple.Music", "Music", "Song\tBand\t200.0\t50.0\n", durationInMs = false, nowSeconds = 0)!! + assertEquals(150L + 1, music.endsAt) + + assertNull(MacNowPlayingReader.parse("com.apple.Music", "Music", "\n", durationInMs = false, nowSeconds = 0)) + } + + @Test + fun windowsParsesTheScriptOutput() { + val track = WindowsNowPlayingReader.parse("Spotify.exe\tSong\tBand\t200000\t50000\r\n", nowSeconds = 0)!! + + assertEquals("Song - Band", track.statusText()) + assertEquals(NowPlayingSource.OtherApp("Spotify.exe", "Spotify"), track.source) + assertEquals(150L + 1, track.endsAt) + + assertNull(WindowsNowPlayingReader.parse("", nowSeconds = 0)) + } + + @Test + fun windowsAppLabels() { + assertEquals("Spotify", WindowsNowPlayingReader.appLabel("Spotify.exe")) + assertEquals("ZuneMusic", WindowsNowPlayingReader.appLabel("Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic")) + assertEquals("308046B0AF4A39CB", WindowsNowPlayingReader.appLabel("308046B0AF4A39CB")) + } +} From 69b100187398ce94fbcc8f3054d20e231cf6861b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Sep 2026 22:41:14 -0400 Subject: [PATCH 30/45] fix(buzz): Buzz Desktop interop fixes from a live Buzz <-> Amethyst test - Attachments in Buzz channels/threads upload to the workspace's own media server (https:///upload): server-scoped 55s token, X-SHA-256 header, metadata stripped to Buzz's validator (EXIF/ICC/PNG text), no /media optimization, no fallback or mirroring to public servers. imeta keys Buzz rejects (ox, content-warning, ...) are dropped. - A relay's OK false is now tracked and shown ("The relay refused this message" + its reason) instead of pending forever. - Mentions on Buzz are written as plain @Name + p tag (Buzz showed the raw nostr:nprofile); thread replies get the same. - The account's kind-0 is pushed to a Buzz workspace relay when a channel opens, so Buzz can name, find and add the user. - Learn a relay's filter cap from Buzz's NOTICE ("REQ contains 12 filters, maximum is 10") and resend the over-cap subscription under it. - Open Buzz channels show Join to non-members, Leave only to members, and posting joins first, matching Buzz's own client. - Thread composer sends thread-scoped typing; new forums open on their thread list. - Previews and notification cards show the latest edit with mentions as names; notification cards drop reactions that were deleted. - NIP-19 references embedded in a URL (blossom npub subdomains) are no longer cited/tagged. Co-Authored-By: Claude Opus 5.5 --- .../service/uploads/AndroidMediaUploader.kt | 108 ++++++++++- .../uploads/blossom/BlossomUploader.kt | 5 + .../uploads/hls/HlsBlobUploaderFactory.kt | 4 + .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 5 +- .../buzz/ForumReplyNewMessageViewModel.kt | 8 +- .../loggedIn/chats/feed/ChatDeliveryTicks.kt | 46 +++-- .../loggedIn/chats/feed/ChatMessageCompose.kt | 32 ++-- .../loggedIn/chats/minichat/MinichatScreen.kt | 37 +++- .../relayGroup/RelayGroupChannelListScreen.kt | 5 +- .../relayGroup/RelayGroupMetadataScreen.kt | 12 +- .../relayGroup/RelayGroupTopBar.kt | 29 ++- .../send/ChannelFileUploadDialog.kt | 4 +- .../send/ChannelNewMessageViewModel.kt | 40 +++- .../chats/rooms/ChatroomHeaderCompose.kt | 8 +- .../chats/utils/ChatFileUploadDialog.kt | 26 +-- .../chats/utils/ChatFileUploadState.kt | 16 +- .../amethyst/commons/model/Account.kt | 9 +- .../commons/model/AccountRelayGroupActions.kt | 33 +++- .../model/composer/NewMessageTagger.kt | 23 ++- .../commons/model/mediaServers/ServerName.kt | 16 ++ .../notifications/CardFeedContentState.kt | 10 +- .../chatDelivery/ChatDeliveryTracker.kt | 39 +++- .../ChatDeliveryTrackerRejectionTest.kt | 81 ++++++++ .../composer/NewMessageTaggerKeyParseTest.kt | 25 +++ .../composeResources/values/strings.xml | 1 + .../amethyst/commons/ui/note/types/Text.kt | 27 ++- .../viewmodels/AccountFeedContentStates.kt | 2 + .../commons/viewmodels/AccountViewModel.kt | 11 +- .../quartz/buzz/media/BuzzImeta.kt | 48 +++++ .../quartz/buzz/media/BuzzMediaSanitizer.kt | 176 ++++++++++++++++++ .../RelayInsertConfirmationCollector.kt | 12 +- .../relay/client/pool/PoolRequests.kt | 24 +++ .../relay/client/pool/RelayReqRefusals.kt | 26 ++- .../quartz/nip10Notes/content/NostrUris.kt | 3 +- .../quartz/nip19Bech32/Nip19Parser.kt | 33 +++- .../nipB7Blossom/BlossomAuthorizationEvent.kt | 10 +- .../quartz/buzz/media/BuzzImetaTest.kt | 67 +++++++ .../buzz/media/BuzzMediaSanitizerTest.kt | 94 ++++++++++ .../client/pool/PoolRequestsFilterCapTest.kt | 37 ++++ .../nip10Notes/content/FindNostrUrisTest.kt | 49 +++++ 40 files changed, 1145 insertions(+), 96 deletions(-) create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTrackerRejectionTest.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt index 2e670493a4..c1b538b076 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -40,12 +40,14 @@ import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState.UploadingFinalState import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader +import com.vitorpamplona.quartz.buzz.media.BuzzMediaSanitizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.ciphers.NostrCipher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -243,6 +245,31 @@ class AndroidMediaUploader( return firstError!!.also { updateState(0.0, it) } } + private class SanitizedBlob( + val file: File, + val uri: Uri, + val size: Long, + ) + + /** + * A copy of [uri] without the metadata a Buzz workspace refuses (HTTP 422: EXIF/XMP/ICC + * segments, PNG text chunks, trailing bytes), or null when there was nothing to remove or + * the format isn't one [BuzzMediaSanitizer] rewrites. The caller deletes the file. + */ + private fun sanitizeForBuzzWorkspace( + uri: Uri, + contentType: String?, + context: Context, + ): SanitizedBlob? { + if (!BuzzMediaSanitizer.handles(contentType)) return null + val original = context.contentResolver.openInputStream(uri)?.use { it.readBytes() } ?: return null + val clean = BuzzMediaSanitizer.sanitize(original, contentType) + if (clean.contentEquals(original)) return null + val file = File.createTempFile("buzz-upload-", ".bin", context.cacheDir) + file.writeBytes(clean) + return SanitizedBlob(file, Uri.fromFile(file), clean.size.toLong()) + } + /** * The upload token for one blob, signed at most once and reused by every server tried. * A signer that returned no token is asked again on the next server; only a token or a @@ -279,11 +306,13 @@ class AndroidMediaUploader( forcedSigner: NostrSigner?, context: Context, sharedAuth: SharedUploadAuth, + buzzWorkspace: Boolean = false, ): UploadingFinalState { updateState(0.2, UploadingState.Uploading) // BUD-05: route through /media (optimize) when the user opted in. The forced-signer - // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload. - val useMedia = forcedSigner == null && account.settings.optimizeMediaOnUpload.value + // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload, and so does a + // Buzz workspace, whose relay checks the imeta `x` against the blob it stored. + val useMedia = !buzzWorkspace && forcedSigner == null && account.settings.optimizeMediaOnUpload.value return try { val result = BlossomUploader() @@ -303,6 +332,20 @@ class AndroidMediaUploader( httpAuth = { hash, size, alt -> sharedAuth.get { when { + // Buzz's strict mode demands exactly one `server` tag naming the + // workspace and a token that expires within 60s of signing. + buzzWorkspace -> { + val now = TimeUtils.now() + BlossomAuthorizationEvent.createUploadAuth( + hash = hash, + size = size, + alt = alt, + signer = forcedSigner ?: account.signer, + servers = listOf(serverBaseUrl), + createdAt = now, + expiration = now + BUZZ_UPLOAD_TOKEN_SECS, + ) + } forcedSigner != null -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) useMedia -> account.createBlossomMediaAuth(hash, size, alt) else -> account.createBlossomUploadAuth(hash, size, alt) @@ -314,19 +357,26 @@ class AndroidMediaUploader( ) val finalState = - verifyHeader( - uploadResult = result, - localContentType = contentType, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - originalHash = originalHash, - originalContentType = contentTypeForResult, - ) + if (buzzWorkspace) { + // The workspace already bound the stored blob to our X-SHA-256 and the token's + // `x`, and its /media reads need a signed GET this client doesn't send — so take + // its descriptor rather than downloading the blob back to hash it again. + trustWorkspaceDescriptor(result, contentType, size, contentTypeForResult, originalHash) + } else { + verifyHeader( + uploadResult = result, + localContentType = contentType, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + originalHash = originalHash, + originalContentType = contentTypeForResult, + ) + } // BUD-04: replicate the blob to the user's other Blossom servers for redundancy. // Fire-and-forget on the account scope AFTER the upload is finished: mirroring is // pure background redundancy, so it must never delay, alter, or fail the upload the // user already completed, and must not touch the on-screen progress state. - if (finalState is UploadingState.Finished && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { + if (finalState is UploadingState.Finished && !buzzWorkspace && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { account.scope.launch(Dispatchers.IO) { try { mirrorToOtherServers(result, serverBaseUrl, account) @@ -389,6 +439,27 @@ class AndroidMediaUploader( } } + private fun trustWorkspaceDescriptor( + uploadResult: MediaUploadResult, + localContentType: String?, + localSize: Long?, + originalContentType: String?, + originalHash: String?, + ): UploadingFinalState { + val url = uploadResult.url + val hash = uploadResult.sha256 + if (url.isNullOrBlank() || hash.isNullOrBlank()) return error(UploadError.SERVER_DID_NOT_PROVIDE_URL) + val fileHeader = + FileHeader( + mimeType = uploadResult.type ?: localContentType, + hash = hash, + size = (uploadResult.size ?: localSize)?.toInt() ?: 0, + dim = uploadResult.dimension, + blurHash = uploadResult.blurHash, + ) + return finish(UploadOrchestrator.OrchestratorResult.ServerResult(fileHeader, url, uploadResult.magnet, hash, originalContentType, originalHash)) + } + private suspend fun verifyHeader( uploadResult: MediaUploadResult, localContentType: String?, @@ -536,6 +607,16 @@ class AndroidMediaUploader( uploadBlossomWithFallback(server, account) { baseUrl, auth -> uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, baseUrl, null, null, account, forcedSigner, context, auth) } + // Private to the workspace: never retried on, or mirrored to, a public server. + ServerType.BuzzWorkspace -> { + val clean = sanitizeForBuzzWorkspace(finalUri, compressed.contentType, context) + try { + uploadBlossom(clean?.uri ?: finalUri, compressed.contentType, clean?.size ?: compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context, SharedUploadAuth(), buzzWorkspace = true) + .also { if (it is UploadingState.Error) updateState(0.0, it) } + } finally { + clean?.file?.delete() + } + } } } finally { deleteTempUri(finalUri, uri) @@ -586,6 +667,10 @@ class AndroidMediaUploader( uploadBlossomWithFallback(server, account) { baseUrl, auth -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, auth) } + + ServerType.BuzzWorkspace -> + uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, SharedUploadAuth(), buzzWorkspace = true) + .also { if (it is UploadingState.Error) updateState(0.0, it) } } } finally { deleteTempUri(encrypted.uri, uri) @@ -593,3 +678,6 @@ class AndroidMediaUploader( } } } + +/** Lifetime of a Buzz workspace upload token; Buzz rejects one that outlives 60s from signing. */ +private const val BUZZ_UPLOAD_TOKEN_SECS = 55L diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index f8aaa70f92..61b3fc61de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -192,6 +192,9 @@ class BlossomUploader { requestBuilder .addHeader("Content-Length", length.toString()) + // The blob's hash (BUD-06). Servers that bind the body to the token's `x` tag before + // reading it — Buzz answers 401 without it — need it up front. + .addHeader(SHA256_HEADER, hash) .url(apiUrl) .put(requestBody) @@ -282,3 +285,5 @@ class BlossomUploader { private fun parseResults(body: String): BlossomUploadResult = JsonMapper.fromJson(body) } + +private const val SHA256_HEADER = "X-SHA-256" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsBlobUploaderFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsBlobUploaderFactory.kt index ffc280a8b1..b88fa27907 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsBlobUploaderFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsBlobUploaderFactory.kt @@ -74,6 +74,10 @@ object HlsBlobUploaderFactory { "NIP-95 storage stores each blob as an event and is not suitable for HLS renditions", ) } + + ServerType.BuzzWorkspace -> { + throw IllegalArgumentException("HLS publishing does not target a Buzz workspace's media server") + } } private fun blossomAdapter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index 528deed877..b7c55b98ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.observeChatPreviewText import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -259,10 +260,10 @@ private fun BuzzChannelPreviewLine( summary != null -> summary author != null -> { val authorName by observeUserName(author, accountViewModel) - val body = event.content.take(80) + val body = (lastNote?.let { observeChatPreviewText(it, 80) } ?: "") if (body.isBlank()) authorName else "$authorName: $body" } - else -> event.content.take(80) + else -> (lastNote?.let { observeChatPreviewText(it, 80) } ?: "") } } Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt index d7845566f9..e7fa37627e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt @@ -56,16 +56,16 @@ class ForumReplyNewMessageViewModel : ChannelNewMessageViewModel() { return null } - // Resolve `@`-mentions the same way the chat composer does: the tagger rewrites each name - // into a `nostr:` reference in the body AND collects the cited users (seeded with the reply - // target) so they land as `p` mention tags — without this, a named member was neither - // notified nor linked. + // Resolve `@`-mentions the same way the chat composer does: the tagger collects the cited + // users (seeded with the reply target) so they land as `p` mention tags — without this, a + // named member was neither notified nor linked. The body keeps Buzz's plain `@Name` form. val tagger = NewMessageTagger( message = message.text.toString(), pTags = listOfNotNull(replyTo.value?.author), eTags = listOfNotNull(replyTo.value), dao = accountViewModel, + userMentionsAsNames = true, ) tagger.run() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt index f2f0000f73..f3789c1ae9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.resources.chat_delivery_details_title import com.vitorpamplona.amethyst.commons.resources.chat_delivery_failed import com.vitorpamplona.amethyst.commons.resources.chat_delivery_no_relay_info import com.vitorpamplona.amethyst.commons.resources.chat_delivery_pending +import com.vitorpamplona.amethyst.commons.resources.chat_delivery_rejected import com.vitorpamplona.amethyst.commons.resources.chat_delivery_sending import com.vitorpamplona.amethyst.commons.resources.close import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox @@ -228,6 +229,7 @@ private fun ChatDeliveryDetailDialog( RelayDeliveryRow( relay = relay, accepted = relay in delivery.acceptedRelays || relay in seenOnRelays, + rejection = delivery.rejectedRelays[relay], ) } @@ -309,19 +311,34 @@ private fun RecipientDeliveryRow( private fun RelayDeliveryRow( relay: NormalizedRelayUrl, accepted: Boolean, + rejection: String? = null, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = relay.displayUrl(), - modifier = Modifier.weight(1f), - maxLines = 1, - ) + Column(modifier = Modifier.fillMaxWidth()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = relay.displayUrl(), + modifier = Modifier.weight(1f), + maxLines = 1, + ) - DeliveryStatusTick(accepted) + if (!accepted && rejection != null) { + TickIcon(MaterialSymbols.Block, Res.string.chat_delivery_rejected, MaterialTheme.colorScheme.error) + } else { + DeliveryStatusTick(accepted) + } + } + // The relay's own words: the only clue to WHY a message will never arrive. + if (!accepted && rejection != null) { + Text( + text = rejection.ifBlank { stringRes(Res.string.chat_delivery_rejected) }, + color = MaterialTheme.colorScheme.error, + fontSize = Font12SP, + ) + } } } @@ -360,6 +377,13 @@ private fun RenderDeliveryTicks( else -> Unit } + // Every targeted relay refused it: it will never arrive. Not "pending" — the detail dialog + // behind this tick shows each relay's reason. + if (delivery?.isRejected == true) { + TickIcon(MaterialSymbols.Block, Res.string.chat_delivery_rejected, MaterialTheme.colorScheme.error) + return + } + if (delivery == null) { // Untracked (sent before a restart): the seen-on relay set is the only signal. if (seenSomewhere) { 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 c34785a2c8..edf2ff79a2 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 @@ -49,9 +49,6 @@ import com.vitorpamplona.amethyst.commons.chats.ui.ChatBubbleLayout import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit -import com.vitorpamplona.amethyst.commons.model.latestConcordEdit -import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -59,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.WatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.DisplayZapSplits +import com.vitorpamplona.amethyst.commons.ui.note.types.observeChatMessageEdit import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderBuzzSystemMessage import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatClip import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderConcordTimerNotice @@ -66,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.service.notifications.NotificationContent import com.vitorpamplona.amethyst.ui.note.RenderZapRaiser import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff @@ -669,17 +668,22 @@ fun NoteRow( } /** - * The newest edit overlaying a chat message [note] (Concord kind-3302 or Buzz kind-40003), or null - * when unedited. A message is only ever one kind, so both resolve off the same [Note.edits] and one - * collector on the note's edits flow serves both — recomposing whenever an edit is added or removed. + * The text a one-line chat preview (room list, workspace rows) shows for [note]: the latest edit's + * text when there is one — the room shows the edit, so the list must too — with `nostr:` user + * references rewritten to `@Name`, as the bubble renders them. */ @Composable -fun observeChatEdit(note: Note): Note? { - val latest by - produceState(initialValue = null, note.idHex) { - note.flow().edits.stateFlow.collect { - value = note.latestConcordEdit() ?: note.latestBuzzEdit() ?: note.latestMarmotEdit() - } - } - return latest +fun observeChatPreviewText( + note: Note, + max: Int = 200, +): String { + val content = observeChatEdit(note)?.event?.content ?: note.event?.content ?: "" + return remember(content, max) { NotificationContent.resolveMentions(content, max).text } } + +/** + * The newest edit overlaying a chat message [note] (Concord, Buzz or Marmot), or null when unedited, + * recomposing whenever an edit is added or removed. + */ +@Composable +fun observeChatEdit(note: Note): Note? = observeChatMessageEdit(note) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index f54529cd63..eb61bfed05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -57,9 +57,13 @@ import com.vitorpamplona.amethyst.commons.chats.publicChannels.concord.datasourc import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzTypingState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.mediaServers.buzzWorkspaceServer import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back @@ -87,6 +91,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadD import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toConcordImeta import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.toPlainImetas +import com.vitorpamplona.quartz.buzz.threading.buzzThreadRootForReplyTo +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine @@ -153,13 +159,23 @@ fun MinichatScreen( val scope = rememberCoroutineScope() val context = LocalContext.current val canPost by remember { derivedStateOf { composer.text.isNotBlank() } } + // A thread in a Buzz workspace channel: its media must live on the workspace's own server + // (the relay refuses any other image host) and its typing signal is scoped to the thread. + val buzzGroup = + remember(rootNote) { + rootNote.inGatherers + ?.firstNotNullOfOrNull { it as? RelayGroupChannel } + ?.takeIf { BuzzRelayDialect.isBuzz(it.groupId.relayUrl) } + } val uploadState = - remember { + remember(buzzGroup) { ChatFileUploadState( accountViewModel.account.settings.defaultFileServer, accountViewModel.account.settings.stripLocationOnUpload, - ) + ).apply { lockServer(buzzGroup?.let { buzzWorkspaceServer(it.groupId.relayUrl.url) }) } } + // Client-side throttle for the Buzz kind-20002 typing heartbeat. + val lastTypingSecs = remember { longArrayOf(0L) } Scaffold( topBar = { @@ -249,7 +265,9 @@ fun MinichatScreen( } }, ) - accountViewModel.account.settings.changeDefaultFileServer(uploadState.selectedServer) + if (uploadState.lockedServer == null) { + accountViewModel.account.settings.changeDefaultFileServer(uploadState.selectedServer) + } accountViewModel.account.settings.changeStripLocationOnUpload(uploadState.stripMetadata) } }, @@ -262,6 +280,19 @@ fun MinichatScreen( Column(modifier = EditFieldModifier) { ThinPaddingTextField( state = composer, + onTextChanged = { + val group = buzzGroup + val now = TimeUtils.now() + if (group != null && composer.text.isNotEmpty() && now - lastTypingSecs[0] >= BuzzTypingState.TYPING_HEARTBEAT_SECS) { + lastTypingSecs[0] = now + val rootEvent = rootNote.event + accountViewModel.sendBuzzTyping( + group, + threadRootId = rootEvent?.tags?.buzzThreadRootForReplyTo(rootEvent.id) ?: rootNote.idHex, + replyToId = rootNote.idHex, + ) + } + }, onContentReceived = { uri, mimeType -> uploadState.load(persistentListOf(SelectedMedia(uri, mimeType))) // Same encryption choice the gallery button makes below. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 324fd1b444..f3deacf181 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -120,6 +120,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzAddPeopleDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzImportRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzRelayImportViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzWorkspaceOverflowMenu +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.observeChatPreviewText import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_DM @@ -914,10 +915,10 @@ private fun BuzzDmPreviewLine( summary != null -> summary author != null -> { val authorName by observeUserName(author, accountViewModel) - val body = event.content.take(80) + val body = (lastNote?.let { observeChatPreviewText(it, 80) } ?: "") if (body.isBlank()) authorName else "$authorName: $body" } - else -> event.content.take(80) + else -> (lastNote?.let { observeChatPreviewText(it, 80) } ?: "") } Text( preview, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index e22a0582ec..fb25f5ea3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -156,7 +156,17 @@ fun RelayGroupCreateScreen( accountViewModel = accountViewModel, nav = nav, nip29Support = nip29Support, - onSuccess = { nav.popUpTo(Route.RelayGroup(viewModel.groupId, relay.url), Route.RelayGroupCreate::class) }, + onSuccess = { + // A new forum opens on its thread list, where its posts live — the same screen the + // workspace list opens a forum on — not on the chat timeline a stream channel uses. + val destination = + if (viewModel.isForum) { + Route.RelayGroupThreads(viewModel.groupId, relay.url) + } else { + Route.RelayGroup(viewModel.groupId, relay.url) + } + nav.popUpTo(destination, Route.RelayGroupCreate::class) + }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index 15d335c1f0..6f4ac8d105 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -163,6 +163,10 @@ fun RelayGroupTopBar( var confirmDelete by remember { mutableStateOf(false) } val isBuzzRelay = remember(channel.groupId.relayUrl) { BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) } val huddleLive by observeBuzzHuddleLive(channel, enabled = isBuzzRelay && membership.isMember(), accountViewModel) + // Buzz names members only from profiles on its own relay; make sure mine is there. + LaunchedEffect(channel.groupId.relayUrl, isBuzzRelay) { + if (isBuzzRelay) accountViewModel.account.relayGroups.shareProfileWithBuzzWorkspace(channel.groupId.relayUrl) + } TopBarExtensibleWithBackButton( title = { @@ -269,7 +273,10 @@ fun RelayGroupTopBar( style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - } else if (!displayMembership.isMember() && channel.requiresMembershipToPost()) { + } else if (!displayMembership.isMember() && (channel.requiresMembershipToPost() || (isBuzzRelay && !isDm))) { + // A Buzz open channel takes posts from any workspace member, but channel membership + // is still what Buzz lists, counts and offers in its mention picker — its own client + // shows Join here too, so a reader can become a member. FilledTonalButton(onClick = { // Closed groups need an invite code; open groups join directly. if (channel.isClosed()) { @@ -418,14 +425,18 @@ fun RelayGroupTopBar( } }, ) - DropdownMenuItem( - text = { Text(stringRes(Res.string.leave), color = MaterialTheme.colorScheme.error) }, - onClick = { - menuOpen = false - accountViewModel.leaveRelayGroup(channel) - if (canPop) nav.popBack() - }, - ) + // A Buzz open channel lets a non-member read and post, but there is nothing to + // leave until they join (the Join button above stands in for it). + if (displayMembership.isMember() || !isBuzzRelay) { + DropdownMenuItem( + text = { Text(stringRes(Res.string.leave), color = MaterialTheme.colorScheme.error) }, + onClick = { + menuOpen = false + accountViewModel.leaveRelayGroup(channel) + if (canPop) nav.popBack() + }, + ) + } // Archive/Unarchive (kind-9002 `archived` tag) — a reversible hide-from-the-sidebar, // Buzz-only and admin-gated like Delete but NOT destructive, so no confirm dialog. // A DM is never archived (it has its own hide), so this is channels/forums only. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 539f57af85..67ebe3cce8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -135,7 +135,9 @@ fun ChannelFileUploadDialog( onceUploaded = onUpload, ) - accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer) + if (state.lockedServer == null) { + accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer) + } accountViewModel.account.settings.changeStripLocationOnUpload(state.stripMetadata) }, onCancel, 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 7acbf11897..920362c2e6 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 @@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit import com.vitorpamplona.amethyst.commons.model.location.LocationResult +import com.vitorpamplona.amethyst.commons.model.mediaServers.buzzWorkspaceServer import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership @@ -77,11 +78,13 @@ import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor +import com.vitorpamplona.quartz.buzz.media.BuzzImeta import com.vitorpamplona.quartz.buzz.stream.BuzzChatMessage import com.vitorpamplona.quartz.buzz.stream.BuzzEditTagOverlay import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent import com.vitorpamplona.quartz.buzz.stream.mentions import com.vitorpamplona.quartz.buzz.threading.buzzThreadRootForReplyTo +import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent @@ -419,6 +422,7 @@ open class ChannelNewMessageViewModel : // out so the relay accepts the mention. No-op unless this is a Buzz relay group the user // moderates; guarded so a failed add never blocks the message. channel?.let { autoInviteMentionedBuzzMembers(it, pendingBuzzInviteMentions) } + channel?.let { joinBuzzChannelBeforePosting(it) } // A geohash cell with no resolvable relays has nowhere to publish. Bail before cancel() clears // the composer, so the user keeps their text (and draft) to retry rather than losing it silently. @@ -506,6 +510,16 @@ open class ChannelNewMessageViewModel : } fun pickedMedia(list: ImmutableList) { + // A Buzz relay only accepts imeta URLs under its own /media/, so a Buzz channel's + // attachments go to the workspace's media server and nowhere else. + val channel = channel + uploadState?.lockServer( + if (channel is RelayGroupChannel && BuzzRelayDialect.isBuzz(channel.groupId.relayUrl)) { + buzzWorkspaceServer(channel.groupId.relayUrl.url) + } else { + null + }, + ) uploadState?.load(list) } @@ -616,18 +630,38 @@ open class ChannelNewMessageViewModel : } } + /** + * Posting to a Buzz open channel joins it first. The relay takes a non-member's message there, + * but channel membership is what Buzz lists, counts and offers in its mention picker, and its + * own client requires joining before it lets anyone post — so a poster who never joined was + * invisible on the Buzz side. Best-effort: a failed join must never block the message. + */ + private suspend fun joinBuzzChannelBeforePosting(channel: Channel) { + if (channel !is RelayGroupChannel || !BuzzRelayDialect.isBuzz(channel.groupId.relayUrl)) return + if (channel.event?.isBuzzDm() == true) return + if (channel.membershipOf(accountViewModel.account.userProfile().pubkeyHex).isMember()) return + try { + accountViewModel.account.relayGroups.joinRelayGroup(channel) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("BuzzAutoJoin", "Failed to join ${channel.groupId.id} before posting", e) + } + } + // `protected open` so a specialized composer (e.g. the Buzz forum reply) can reuse this whole // rich EditFieldRow but swap only the event it builds for the composed text. protected open suspend fun createTemplate(): EventTemplate? { val channel = channel ?: return null val messageText = message.text.toString() + val isBuzzChannel = channel is RelayGroupChannel && BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) val tagger = NewMessageTagger( message = messageText, pTags = listOfNotNull(replyTo.value?.author), eTags = listOfNotNull(replyTo.value), dao = accountViewModel, + userMentionsAsNames = isBuzzChannel, ) tagger.run() @@ -639,7 +673,11 @@ open class ChannelNewMessageViewModel : pendingBuzzInviteMentions = tagger.pTags?.filter { it != replyTo.value?.author }.orEmpty() val urls = findURLs(messageText) - val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) + val usedAttachments = + iMetaAttachments.filterIsIn(urls.toSet()).let { + // Buzz refuses the whole message over any imeta key it doesn't know (e.g. `ox`). + if (isBuzzChannel) BuzzImeta.sanitize(it) else it + } val emojis = accountViewModel.account.emoji.findEmojiTags(messageText) val channelRelays = channel.relays() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index ef4b75348f..4f77d73d5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -151,6 +151,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.observeChatPreviewText import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupPreviewText @@ -697,8 +698,9 @@ private fun RelayGroupRoomCompose( val authorName by observeUserName(author, accountViewModel) // A Buzz timeline row (system line, huddle/job activity, diff) carries JSON/diff in its // content, so show its human-readable summary — the same text the in-chat row renders — - // rather than "author: {json}". Plain chat messages fall through to the usual framing. - buzzTimelinePreviewSummary(noteEvent, accountViewModel) ?: "$authorName: ${noteEvent.content.take(200)}" + // rather than "author: {json}". Plain chat messages fall through to the usual framing, + // showing the latest edit with mentions as names. + buzzTimelinePreviewSummary(noteEvent, accountViewModel) ?: "$authorName: ${observeChatPreviewText(lastMessage)}" } else { // Event-less placeholder row. Until the channel's `limit = 1` preview REQ settles we cannot // tell an empty channel from one whose newest message simply hasn't arrived, and claiming @@ -1009,7 +1011,7 @@ private fun RelayGroupServerRoomCompose( val authorName by observeUserName(author, accountViewModel) // Buzz timeline rows (system/huddle/job/diff) carry JSON/diff content — summarize them // like the in-chat row instead of printing raw payload; plain chat falls through. - buzzTimelinePreviewSummary(noteEvent, accountViewModel) ?: "$authorName: ${noteEvent.content.take(200)}" + buzzTimelinePreviewSummary(noteEvent, accountViewModel) ?: "$authorName: ${row.newestMessage?.let { observeChatPreviewText(it) } ?: ""}" } else { stringRes(Res.string.relay_group_no_messages_yet) } 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 488b3b7ea2..e4076dcac0 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 @@ -230,18 +230,20 @@ private fun ImageVideoPostChat( ) } - SettingsRow(Res.string.file_server, Res.string.file_server_description) { - TextSpinner( - label = "", - placeholder = - fileServers - .firstOrNull { it.baseUrl == accountViewModel.account.settings.defaultFileServer.baseUrl } - ?.name - ?: fileServers.firstOrNull()?.name - ?: DEFAULT_MEDIA_SERVERS[0].name, - options = fileServerOptions, - onSelect = { fileUploadState.selectedServer = fileServers[it] }, - ) + if (fileUploadState.lockedServer == null) { + SettingsRow(Res.string.file_server, Res.string.file_server_description) { + TextSpinner( + label = "", + placeholder = + fileServers + .firstOrNull { it.baseUrl == accountViewModel.account.settings.defaultFileServer.baseUrl } + ?.name + ?: fileServers.firstOrNull()?.name + ?: DEFAULT_MEDIA_SERVERS[0].name, + options = fileServerOptions, + onSelect = { fileUploadState.selectedServer = fileServers[it] }, + ) + } } if (fileUploadState.multiOrchestrator?.hasCompressible() == true) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 2703b316e1..242217803c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -43,6 +43,20 @@ class ChatFileUploadState( val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var selectedServer by mutableStateOf(defaultServer) + + /** + * A server this chat must upload to, e.g. a Buzz workspace's own media server, whose relay + * refuses any other image host. While set, the picker is hidden and the choice is not saved + * as the user's default. + */ + var lockedServer by mutableStateOf(null) + private set + + fun lockServer(server: ServerName?) { + lockedServer = server + selectedServer = server ?: defaultServer + } + var caption by mutableStateOf("") var contentWarning by mutableStateOf(false) @@ -73,7 +87,7 @@ class ChatFileUploadState( multiOrchestrator = null mediaUploadTracker.finishUpload() caption = "" - selectedServer = defaultServer + selectedServer = lockedServer ?: defaultServer encryptFiles = true } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt index dca2865779..c44fcaa61f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt @@ -185,6 +185,7 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.FileHeader import com.vitorpamplona.amethyst.commons.util.logTime import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode +import com.vitorpamplona.quartz.buzz.media.BuzzImeta import com.vitorpamplona.quartz.buzz.threading.buzzThread import com.vitorpamplona.quartz.buzz.threading.buzzThreadRootForReplyTo import com.vitorpamplona.quartz.concord.cord03Channels.ChannelChat @@ -2201,7 +2202,9 @@ class Account( // quick reply is notified (`p`) and their reference resolves. The reply-parent author is // already tagged by each builder below, so drop it from the body mentions to avoid a // duplicate `p`. - val tagger = NewMessageTagger(text, dao = LocalCache) + // Buzz shows a `nostr:` reference verbatim, so a Buzz thread names people as plain `@Name`. + val buzzGroup = gatherers?.firstNotNullOfOrNull { it as? RelayGroupChannel }?.takeIf { BuzzRelayDialect.isBuzz(it.groupId.relayUrl) } + val tagger = NewMessageTagger(text, dao = LocalCache, userMentionsAsNames = buzzGroup != null) tagger.run() val mentions = tagger.pTags?.mapNotNull { it.pubkeyHex.takeIf { pk -> pk != rootEvent.pubKey } }.orEmpty() val finalText = appendMediaUrls(tagger.message, imetas) @@ -2237,7 +2240,7 @@ class Account( // their clients no longer thread on. Reading 40002 stays supported (see // [com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply]). // - // Attached media rides as URLs appended to the content. + // Attached media rides as URLs appended to the content, plus Buzz-shaped `imeta`. val root = rootEvent.tags.buzzThreadRootForReplyTo(rootEvent.id) signer.sign( ChatEvent.build(finalText) { @@ -2246,6 +2249,8 @@ class Account( rootNote.author?.pubkeyHex?.let { pTag(PTag(it)) } pTags(mentions.map { PTag(it) }) previous(group.previousEventRefs(pubKey)) + // Buzz refuses the whole event over any imeta key it doesn't allow. + imetas(BuzzImeta.sanitize(imetas)) }, ) } else { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt index b9f51ea649..081d47de10 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupPin import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi @@ -111,14 +112,42 @@ class AccountRelayGroupActions( account.follow(channel) } + // "relay|metadata event id" pairs already pushed this session, so each profile version goes to + // each workspace once. + private val profileSharedWith = MutableStateFlow>(emptySet()) + + /** + * Push my current kind-0 profile to the Buzz workspace at [relay]. Buzz reads member names and + * pictures only from its own relay, and its people search (mentions, Add people, new DM) is by + * name — without this, Buzz showed me as a bare npub and nobody could find or add me. Sends the + * already-signed event as-is (no new signature), once per profile version per workspace. + */ + fun shareProfileWithBuzzWorkspace(relay: NormalizedRelayUrl) { + if (!account.isWriteable()) return + val profile = account.userMetadata.getUserMetadataEvent() ?: return + val key = "${relay.url}|${profile.id}" + while (true) { + val current = profileSharedWith.value + if (key in current) return + if (profileSharedWith.compareAndSet(current, current + key)) break + } + account.client.publish(profile, setOf(relay)) + } + /** * Fire a Buzz kind-20002 typing heartbeat for [channel] to its host relay. Ephemeral * (never stored) and fire-and-forget — no delivery tracking, no local echo (we filter * our own typing in the UI). Throttled by the composer to [BuzzTypingState.TYPING_HEARTBEAT_SECS]. + * [threadRootId]/[replyToId] scope it to a thread, as Buzz's thread composer does. */ - suspend fun sendBuzzTyping(channel: RelayGroupChannel) { + suspend fun sendBuzzTyping( + channel: RelayGroupChannel, + threadRootId: HexKey? = null, + replyToId: HexKey? = null, + ) { if (!account.isWriteable()) return - val signed = account.signer.sign(TypingIndicatorEvent.build(channel.groupId.id)) + // In a thread, the `e` markers scope the signal to it: Buzz shows it in that thread's pane. + val signed = account.signer.sign(TypingIndicatorEvent.build(channel.groupId.id, threadRootId, replyToId)) account.client.publish(signed, setOf(channel.groupId.relayUrl)) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt index 368c2f6d1f..f97915f85b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt @@ -39,11 +39,17 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.toNpub import kotlinx.coroutines.CancellationException +/** + * @param userMentionsAsNames write a user mention as plain `@Name` text instead of a `nostr:` + * reference — the form Buzz clients send and render (the cited user still lands in [pTags], which is + * what notifies them). Buzz shows a `nostr:nprofile…` body verbatim. + */ class NewMessageTagger( var message: String, var pTags: List? = null, var eTags: List? = null, var dao: Dao, + val userMentionsAsNames: Boolean = false, ) { val directMentions = mutableSetOf() val directMentionsNotes = mutableSetOf() @@ -115,11 +121,11 @@ class NewMessageTagger( val results = parseDirtyWordForKey(word) when (val entity = results?.key?.entity) { is NPub -> { - getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord) + userMention(dao.getOrCreateUser(entity.hex), results.restOfWord) } is NProfile -> { - getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord) + userMention(dao.getOrCreateUser(entity.hex), results.restOfWord) } is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { @@ -159,6 +165,19 @@ class NewMessageTagger( }.joinToString("\n") } + private fun userMention( + user: User, + restOfTheWord: String?, + ): String { + if (!userMentionsAsNames) return getNostrAddress(user.toNProfile(), restOfTheWord) + val name = "@" + user.toBestDisplayName() + return when { + restOfTheWord.isNullOrEmpty() -> name + restOfTheWord[0].isLetterOrDigit() -> "$name $restOfTheWord" + else -> name + restOfTheWord + } + } + fun getNostrAddress( bechAddress: String, restOfTheWord: String?, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/mediaServers/ServerName.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/mediaServers/ServerName.kt index 9fdfb3fafd..8c75a91a48 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/mediaServers/ServerName.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/mediaServers/ServerName.kt @@ -33,6 +33,22 @@ enum class ServerType { Blossom, NIP95, NIP96, + + /** + * A Buzz workspace's own Blossom endpoint (`https:///upload`). Blobs there are + * private to the workspace and the relay only accepts `imeta` URLs under its `/media/`, so an + * upload to it never falls back to, or mirrors onto, a public server. + */ + BuzzWorkspace, +} + +/** The media server of the Buzz workspace served at [relayUrl] (`wss://host` → `https://host`). */ +fun buzzWorkspaceServer(relayUrl: String): ServerName { + val host = + relayUrl + .substringAfter("://") + .substringBefore("/") + return ServerName(host, "https://$host", ServerType.BuzzWorkspace) } val DEFAULT_MEDIA_SERVERS: List = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt index 6569bf14c7..c16d050ac9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt @@ -429,8 +429,16 @@ class CardFeedContentState( } } + /** + * Drops cards built from [deletedNotes] — e.g. a reaction its author took back, which used to + * linger on the notification card after it vanished from the message. A card can bundle many + * notes (all reactions to one post), so this rebuilds from the filter rather than editing cards. + */ fun deleteFromFeed(deletedNotes: Set) { - // TODO: Implement deletion of notes from the notification feed + val shown = lastNotes ?: return + if (deletedNotes.none { it in shown }) return + clear() + invalidateData() } fun trimToSize(maxItems: Int) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTracker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTracker.kt index d30a82fb24..db78798f8d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTracker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTracker.kt @@ -76,7 +76,19 @@ data class ChatDelivery( val acceptedRelays: Set = emptySet(), val recipients: List? = null, val sendState: ChatSendState = ChatSendState.SENT, + // Relays that answered `OK false`, with their reason. A later acceptance clears the entry. + val rejectedRelays: Map = emptyMap(), ) { + /** + * No relay took the message and every targeted relay refused it: it will never arrive, so the + * UI must say so (with the relay's reason) instead of showing it as still pending. + */ + val isRejected: Boolean + get() = + acceptedRelays.isEmpty() && + rejectedRelays.isNotEmpty() && + rejectedRelays.keys.containsAll(targetRelays) + /** The other participants' wraps (self-copy excluded); null for rooms. */ val otherRecipients: List? get() = recipients?.filterNot { it.isSelf } @@ -137,7 +149,10 @@ class ChatDeliveryTracker( private val retries = mutableMapOf Unit>() private val okCollector = - RelayInsertConfirmationCollector(client) { eventId, relay -> + RelayInsertConfirmationCollector( + client, + onRelayRejected = { eventId, relay, reason -> onRejected(eventId, relay.url, reason) }, + ) { eventId, relay -> onAccepted(eventId, relay.url) } @@ -297,7 +312,7 @@ class ChatDeliveryTracker( } } - private fun onAccepted( + internal fun onAccepted( eventId: HexKey, relay: NormalizedRelayUrl, ) { @@ -318,6 +333,7 @@ class ChatDeliveryTracker( // send path recorded. sendState = ChatSendState.SENT, acceptedRelays = delivery.acceptedRelays + relay, + rejectedRelays = delivery.rejectedRelays - relay, recipients = delivery.recipients?.map { if (it.recipient == recipient) { @@ -330,11 +346,28 @@ class ChatDeliveryTracker( } else { val flow = deliveries[eventId] ?: return val delivery = flow.value ?: return - flow.value = delivery.copy(sendState = ChatSendState.SENT, acceptedRelays = delivery.acceptedRelays + relay) + flow.value = delivery.copy(sendState = ChatSendState.SENT, acceptedRelays = delivery.acceptedRelays + relay, rejectedRelays = delivery.rejectedRelays - relay) } } } + internal fun onRejected( + eventId: HexKey, + relay: NormalizedRelayUrl, + reason: String, + ) { + if (eventId !in wrapIndex && eventId !in knownIds) return + + lock.withLock { + val noteId = wrapIndex[eventId]?.first ?: eventId + val flow = deliveries[noteId] ?: return + val delivery = flow.value ?: return + // A relay that already accepted the event (e.g. a resend after AUTH) keeps its tick. + if (relay in delivery.acceptedRelays) return + flow.value = delivery.copy(sendState = ChatSendState.SENT, rejectedRelays = delivery.rejectedRelays + (relay to reason)) + } + } + // Must run under [lock]. Also creates entries for ids queried by the UI // before their send registers (compose can win that race), so both paths // share the same flow instance. diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTrackerRejectionTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTrackerRejectionTest.kt new file mode 100644 index 0000000000..07909aae16 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatDelivery/ChatDeliveryTrackerRejectionTest.kt @@ -0,0 +1,81 @@ +/* + * 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.commons.relayClient.chatDelivery + +import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** A relay's refusal (OK false) must surface with its reason, not as a message still pending. */ +class ChatDeliveryTrackerRejectionTest { + private val noteId = "a".repeat(64) + private val relayA = RelayUrlNormalizer.normalizeOrNull("wss://a.example.com")!! + private val relayB = RelayUrlNormalizer.normalizeOrNull("wss://b.example.com")!! + + @Test + fun everyTargetRefusing_isRejectedWithTheReason() { + val tracker = ChatDeliveryTracker(EmptyNostrClient()) + tracker.trackPublic(noteId, setOf(relayA)) + + tracker.onRejected(noteId, relayA, "invalid: imeta url must be a local /media/ path") + + val delivery = tracker.currentFor(noteId)!! + assertTrue(delivery.isRejected) + assertEquals("invalid: imeta url must be a local /media/ path", delivery.rejectedRelays[relayA]) + } + + @Test + fun oneRelayStillOpen_isNotRejectedYet() { + val tracker = ChatDeliveryTracker(EmptyNostrClient()) + tracker.trackPublic(noteId, setOf(relayA, relayB)) + + tracker.onRejected(noteId, relayA, "blocked: no") + + assertFalse(tracker.currentFor(noteId)!!.isRejected) + } + + @Test + fun aLaterAcceptanceClearsTheRefusal() { + val tracker = ChatDeliveryTracker(EmptyNostrClient()) + tracker.trackPublic(noteId, setOf(relayA)) + + tracker.onRejected(noteId, relayA, "rate-limited: slow down") + tracker.onAccepted(noteId, relayA) + + val delivery = tracker.currentFor(noteId)!! + assertFalse(delivery.isRejected) + assertTrue(delivery.rejectedRelays.isEmpty()) + } + + @Test + fun aRefusalAfterAcceptanceKeepsTheTick() { + val tracker = ChatDeliveryTracker(EmptyNostrClient()) + tracker.trackPublic(noteId, setOf(relayA)) + + tracker.onAccepted(noteId, relayA) + tracker.onRejected(noteId, relayA, "duplicate: already have it") + + assertTrue(tracker.currentFor(noteId)!!.rejectedRelays.isEmpty()) + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt index 3cc2675f47..582c55e918 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.getOrCreateAddr import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -184,4 +185,28 @@ class NewMessageTaggerKeyParseTest { ) assertEquals(",", result?.restOfWord) } + + @Test + fun userMentionsAsNamesWritesPlainAtNameAndStillTags() = + runBlocking { + val npub = "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z" + val hex = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + val user = dao.getOrCreateUser(hex) + + val tagger = NewMessageTagger(message = "hi @$npub, there", dao = dao, userMentionsAsNames = true) + tagger.run() + + assertEquals("hi @${user.toBestDisplayName()}, there", tagger.message) + assertEquals(listOf(hex), tagger.pTags?.map { it.pubkeyHex }) + } + + @Test + fun defaultStillWritesNostrUri() = + runBlocking { + val npub = "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z" + val tagger = NewMessageTagger(message = "hi @$npub", dao = dao) + tagger.run() + + assertTrue(tagger.message.startsWith("hi nostr:nprofile1")) + } } diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 9b2257719e..853b5b3d2f 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -5469,6 +5469,7 @@ No relay accepted the KeyPackage. Check your relay settings and try again. Sending\u2026 Could not be sent. Tap to try again. + The relay refused this message %1$d new message %1$d new messages diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt index 9b4cd4de87..ef8420054b 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,6 +37,9 @@ import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit +import com.vitorpamplona.amethyst.commons.model.latestConcordEdit +import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.replyingDirectlyTo import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists @@ -55,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent @@ -62,6 +67,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent enum class ReplyRenderType { FULL, @@ -158,6 +164,10 @@ fun RenderTextEvent( return } + // A chat message edited in place (Buzz kind-40003, Concord, Marmot) shows its newest edit here + // too — notification cards and quotes render it through this path, not the chat bubble. + val chatEdit = if (noteEvent is ChatEvent || noteEvent is StreamMessageV2Event) observeChatMessageEdit(note) else null + LoadDecryptedContent( note, accountViewModel, @@ -171,7 +181,7 @@ fun RenderTextEvent( ?.event ?.content ?: body } else { - body + chatEdit?.event?.content ?: body } val eventContent = remember(newBody) { displayedNoteText(note, newBody) } @@ -223,3 +233,18 @@ fun RenderTextEvent( } } } + +/** The newest in-place edit of a chat message (Concord, Buzz or Marmot), recomposing as edits land. */ +@Composable +fun observeChatMessageEdit(note: Note): Note? { + val latest by + produceState(initialValue = note.latestChatEdit(), note.idHex) { + note + .flow() + .edits.stateFlow + .collect { value = note.latestChatEdit() } + } + return latest +} + +private fun Note.latestChatEdit(): Note? = latestConcordEdit() ?: latestBuzzEdit() ?: latestMarmotEdit() diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt index 0ba87e47e5..5a7dc3fcf6 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt @@ -461,6 +461,8 @@ class AccountFeedContentStates( } fun deleteNotes(newNotes: Set) { + notifications.deleteFromFeed(newNotes) + notificationsFollowing.deleteFromFeed(newNotes) homeLive.deleteFromFeed(newNotes) homeNewThreads.deleteFromFeed(newNotes) homeReplies.deleteFromFeed(newNotes) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt index b53610ad2a..b218de03fc 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt @@ -764,10 +764,13 @@ class AccountViewModel( account.concord.sendConcordTyping(communityId, channelIdHex) } - fun sendBuzzTyping(channel: RelayGroupChannel) = - viewModelScope.launch(Dispatchers.IO) { - account.relayGroups.sendBuzzTyping(channel) - } + fun sendBuzzTyping( + channel: RelayGroupChannel, + threadRootId: HexKey? = null, + replyToId: HexKey? = null, + ) = viewModelScope.launch(Dispatchers.IO) { + account.relayGroups.sendBuzzTyping(channel, threadRootId, replyToId) + } @Immutable data class NoteComposeReportState( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt new file mode 100644 index 0000000000..9e62843c1d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt @@ -0,0 +1,48 @@ +/* + * 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.quartz.buzz.media + +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag + +/** + * Buzz's relay validates every `imeta` tag on ingest (`crates/buzz-relay/src/handlers/imeta.rs`) + * and refuses the WHOLE event on the first key it does not know or a repeated singleton key. + * Generic NIP-92 attachments carry extras Buzz never accepts (`ox`, `content-warning`, …), so a + * message bound for a Buzz workspace keeps only the keys Buzz allows, each singleton once. + */ +object BuzzImeta { + /** `url` is the tag's anchor; it is not part of [IMetaTag.properties]. */ + val ALLOWED_KEYS = setOf("m", "x", "size", "dim", "blurhash", "alt", "thumb", "fallback", "duration", "bitrate", "image", "filename") + + /** Every allowed key but `fallback` may appear only once. */ + private const val REPEATABLE_KEY = "fallback" + + fun sanitize(tag: IMetaTag): IMetaTag = + IMetaTag( + tag.url, + tag.properties + .filterKeys { it in ALLOWED_KEYS } + .mapValues { (key, values) -> if (key == REPEATABLE_KEY) values else values.take(1) } + .filterValues { it.isNotEmpty() }, + ) + + fun sanitize(tags: List): List = tags.map(::sanitize) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt new file mode 100644 index 0000000000..3e6519ed2a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt @@ -0,0 +1,176 @@ +/* + * 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.quartz.buzz.media + +/** + * Makes an image acceptable to a Buzz workspace's media server, which refuses (HTTP 422) any blob + * carrying a metadata channel (`buzz-media/src/validation.rs`): in a JPEG every APP1–APP13/APP15 + * segment (EXIF, XMP, ICC…) and comment, plus any APP0/APP14 that isn't the canonical JFIF/Adobe + * header; in a PNG every text/EXIF/ICC chunk and every ancillary chunk that isn't a known rendering + * hint; and in both, any bytes after the end marker. Removing those leaves the pixels untouched. + * + * Other formats, and input that doesn't parse, come back unchanged — the server stays the judge. + */ +object BuzzMediaSanitizer { + fun sanitize( + bytes: ByteArray, + mimeType: String?, + ): ByteArray = + when (mimeType?.lowercase()) { + "image/jpeg", "image/jpg" -> stripJpeg(bytes) ?: bytes + "image/png" -> stripPng(bytes) ?: bytes + else -> bytes + } + + /** True when [mimeType] is one [sanitize] rewrites. */ + fun handles(mimeType: String?): Boolean = mimeType?.lowercase() in setOf("image/jpeg", "image/jpg", "image/png") + + private fun ByteArray.u8(i: Int) = this[i].toInt() and 0xff + + fun stripJpeg(b: ByteArray): ByteArray? { + if (b.size < 4 || b.u8(0) != 0xff || b.u8(1) != 0xd8) return null + val out = ByteBuilder(b.size) + out.write(b, 0, 2) + var i = 2 + while (i < b.size) { + if (b.u8(i) != 0xff) return null + var j = i + while (j < b.size && b.u8(j) == 0xff) j++ + if (j >= b.size) return null + val marker = b.u8(j) + when { + marker == 0xd9 -> { + out.write(byteArrayOf(0xff.toByte(), 0xd9.toByte()), 0, 2) + return out.toByteArray() + } + + marker == 0xda -> { + // Start of scan: entropy-coded data (and any later tables/scans) runs to the + // last end-of-image marker; whatever trails it is dropped. + val eoi = lastEoi(b) + if (eoi < j) return null + out.write(byteArrayOf(0xff.toByte()), 0, 1) + out.write(b, j, eoi + 2 - j) + return out.toByteArray() + } + + marker in 0xd0..0xd7 || marker == 0x01 -> { + out.write(byteArrayOf(0xff.toByte(), marker.toByte()), 0, 2) + i = j + 1 + } + + else -> { + if (j + 3 > b.size) return null + val len = (b.u8(j + 1) shl 8) or b.u8(j + 2) + if (len < 2) return null + val end = j + 1 + len + if (end > b.size) return null + if (keepJpegSegment(marker, b, j + 3, end)) { + out.write(byteArrayOf(0xff.toByte()), 0, 1) + out.write(b, j, end - j) + } + i = end + } + } + } + return null + } + + private fun keepJpegSegment( + marker: Int, + b: ByteArray, + payloadStart: Int, + end: Int, + ): Boolean { + val payloadLen = end - payloadStart + return when { + marker == 0xe0 -> + payloadLen >= 14 && + b.u8(payloadStart) == 'J'.code && + b.u8(payloadStart + 1) == 'F'.code && + b.u8(payloadStart + 2) == 'I'.code && + b.u8(payloadStart + 3) == 'F'.code && + b.u8(payloadStart + 4) == 0 && + payloadLen == 14 + 3 * b.u8(payloadStart + 12) * b.u8(payloadStart + 13) + + marker == 0xee -> + payloadLen == 12 && + b.decodeToString(payloadStart, payloadStart + 5) == "Adobe" + + marker in 0xe1..0xed || marker == 0xef || marker == 0xfe -> false + + else -> true + } + } + + private fun lastEoi(b: ByteArray): Int { + var k = b.size - 2 + while (k >= 0) { + if (b.u8(k) == 0xff && b.u8(k + 1) == 0xd9) return k + k-- + } + return -1 + } + + private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 'P'.code.toByte(), 'N'.code.toByte(), 'G'.code.toByte(), 0x0d, 0x0a, 0x1a, 0x0a) + + // Ancillary chunks Buzz keeps: pure rendering hints. pHYs is excluded on purpose (identity channel). + private val PNG_RENDERING_CHUNKS = setOf("cHRM", "gAMA", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "sPLT", "acTL", "fcTL", "fdAT") + + fun stripPng(b: ByteArray): ByteArray? { + if (b.size < PNG_SIGNATURE.size) return null + for (k in PNG_SIGNATURE.indices) if (b[k] != PNG_SIGNATURE[k]) return null + val out = ByteBuilder(b.size) + out.write(b, 0, PNG_SIGNATURE.size) + var i = PNG_SIGNATURE.size + while (i + 12 <= b.size) { + val len = (b.u8(i) shl 24) or (b.u8(i + 1) shl 16) or (b.u8(i + 2) shl 8) or b.u8(i + 3) + if (len < 0) return null + val end = i + 12 + len + if (end > b.size) return null + val kind = b.decodeToString(i + 4, i + 8) + val ancillary = (b.u8(i + 4) and 0x20) != 0 + if (!ancillary || kind in PNG_RENDERING_CHUNKS) out.write(b, i, end - i) + i = end + if (kind == "IEND") return out.toByteArray() + } + return null + } + + private class ByteBuilder( + capacity: Int, + ) { + private var buf = ByteArray(maxOf(capacity, 16)) + private var size = 0 + + fun write( + src: ByteArray, + from: Int, + count: Int, + ) { + if (size + count > buf.size) buf = buf.copyOf(maxOf(buf.size * 2, size + count)) + src.copyInto(buf, size, from, from + count) + size += count + } + + fun toByteArray() = buf.copyOf(size) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt index 6b536de30b..3c81bf8b7f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt @@ -29,10 +29,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.utils.Log /** - * Listens to INostrClient's onEvent messages for caching purposes. + * Listens to INostrClient's relay messages and reports `OK`s for published events: [onRelayReceived] for an acceptance and, when given, + * [onRelayRejected] for a definitive refusal with the relay's reason. An `auth-required:` refusal + * is NOT reported — the client authenticates and resends, so it is not the relay's final answer. */ class RelayInsertConfirmationCollector( val client: INostrClient, + val onRelayRejected: ((eventId: HexKey, relay: IRelayClient, reason: String) -> Unit)? = null, val onRelayReceived: (eventId: HexKey, relay: IRelayClient) -> Unit, ) { private val clientListener = @@ -42,8 +45,11 @@ class RelayInsertConfirmationCollector( msgStr: String, msg: Message, ) { - if (msg is OkMessage && msg.success) { + if (msg !is OkMessage) return + if (msg.success) { onRelayReceived(msg.eventId, relay) + } else if (!msg.message.startsWith(AUTH_REQUIRED_PREFIX)) { + onRelayRejected?.invoke(msg.eventId, relay, msg.message) } } } @@ -59,3 +65,5 @@ class RelayInsertConfirmationCollector( client.removeConnectionListener(clientListener) } } + +private const val AUTH_REQUIRED_PREFIX = "auth-required:" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt index 12e1fb959e..1adc50e470 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd @@ -311,6 +312,29 @@ class PoolRequests( } } + is NoticeMessage -> { + // A relay that refuses an over-cap REQ by NOTICE (Buzz: "REQ contains 12 filters, + // maximum is 10") never says WHICH sub it dropped, and that sub would wait for an + // EOSE forever. Once the cap is known, every sub whose last REQ on this relay went + // over it is treated as closed and re-decided, which now narrows it under the cap. + val cap = relayRefusals.onNotice(relay.url, msg.message) ?: return + relayState.forEach { subId, state -> + val cmd = + state.withLock(relay.url) { + val sent = state.currentFilters(relay.url) + if (sent != null && sent.size > cap) { + state.onClosed(relay.url) + decideCommandLocked(state, subId, relay.url) + } else { + null + } + } + if (cmd != null && cmd !is CloseCmd) { + relay.sendOrConnectAndSync(cmd) + } + } + } + is ClosedMessage -> { // Relay-wide capability tracking is independent of any single sub's lock. Run // it first so decideCommandLocked below already sees the block; if it newly diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayReqRefusals.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayReqRefusals.kt index 3c8d87bca4..4ba20dd7a9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayReqRefusals.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayReqRefusals.kt @@ -214,6 +214,23 @@ class RelayReqRefusals( fun disallowedKinds(relay: NormalizedRelayUrl): Set = disallowedKinds[relay] ?: emptySet() + /** The most filters [relay] has said it accepts in one REQ, if it has said so. */ + fun maxFilters(relay: NormalizedRelayUrl): Int? = maxFilters[relay] + + /** + * Learn a filter cap from a NOTICE. Some relays (Buzz) refuse an over-cap REQ with a NOTICE + * instead of a CLOSED, so it names no subscription. Returns the cap when this notice taught + * one, so the caller can re-send whichever REQs went over it. + */ + fun onNotice( + relay: NormalizedRelayUrl, + message: String, + ): Int? { + parseMaxFilters(message) ?: return null + learnMaxFilters(relay, message) + return maxFilters[relay] + } + private fun classify(reason: String): Policy? { val t = reason.lowercase() if (SEARCH_REQUIRED_MARKERS.any { it in t }) return Policy.SEARCH_ONLY @@ -229,10 +246,17 @@ class RelayReqRefusals( // "invalid number of filters: 4" (strfry policy) — the relay refused N, so it takes fewer. private val INVALID_FILTER_COUNT = Regex("""invalid number of filters:?\s*([0-9]+)""") + // "REQ contains 12 filters, maximum is 10" (Buzz, sent as a NOTICE) — names the cap itself. + private val FILTER_COUNT_WITH_MAXIMUM = Regex("""([0-9]+) filters?,?\s*(?:the )?max(?:imum)?(?: is|:)?\s*([0-9]+)""") + fun parseMaxFilters(reason: String): Int? { + val lower = reason.lowercase() + FILTER_COUNT_WITH_MAXIMUM.find(lower)?.let { match -> + return match.groupValues[2].toIntOrNull()?.takeIf { it >= 1 } + } val refusedCount = INVALID_FILTER_COUNT - .find(reason.lowercase()) + .find(lower) ?.groupValues ?.get(1) ?.toIntOrNull() ?: return null diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt index dafd8063e4..6fc4170d86 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/NostrUris.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip10Notes.content import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -fun findNostrUris(content: String) = Nip19Parser.parseAll(content) +/** The NIP-19 references [content] cites — not ones inside a link, like a Blossom `npub1…` subdomain. */ +fun findNostrUris(content: String) = Nip19Parser.parseAllStandalone(content) fun findNostrEventUris(content: String) = Nip19Parser.parseAllEvents(content) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index c7e83091ac..04227c65e7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -242,6 +242,7 @@ object Nip19Parser { content: String, fixed58Prefixes: Array, variablePrefixes: Array, + standaloneOnly: Boolean = false, action: (type: String, key: String, additionalChars: String) -> Unit, ) { var i = 0 @@ -265,7 +266,7 @@ object Nip19Parser { else -> if (nextLower < nextUpper) nextLower else nextUpper } if (i >= len) return - if (isCandidateAt(content, i)) { + if (isCandidateAt(content, i) && !(standaloneOnly && isEmbeddedAt(content, i))) { var end = -1 for (prefix in fixed58Prefixes) { @@ -312,6 +313,23 @@ object Nip19Parser { } } + /** + * True when the entity starting at [at] is glued to the token before it — an `npub1…` that is a + * URL's subdomain (`https://npub1….blossom.band/…`) or part of a path or word — rather than a + * reference on its own. `nostr:` and `@` prefixes are part of a reference, so they are skipped. + */ + private fun isEmbeddedAt( + content: String, + at: Int, + ): Boolean { + var j = at + if (j > 0 && content[j - 1] == '@') j-- + if (j >= 6 && content.regionMatches(j - 6, "nostr:", 0, 6, ignoreCase = true)) j -= 6 + if (j == 0) return false + val c = content[j - 1] + return c.isLetterOrDigit() || c == '/' || c == '.' || c == '-' || c == '_' || c == '=' || c == '?' || c == '&' || c == '#' + } + private fun allBech32( content: String, from: Int, @@ -345,6 +363,19 @@ object Nip19Parser { return returningList } + /** + * Like [parseAll], but only references that stand on their own: an entity glued to the text + * before it (an `npub1…` subdomain of a Blossom URL, say) is not something the author cited, so + * tagging it would `p`-tag (and notify) whoever's key happens to be in a link. + */ + fun parseAllStandalone(content: String): List { + val returningList = mutableListOf() + forEachNip19Match(content, FIXED_58_PREFIXES, VARIABLE_PREFIXES, standaloneOnly = true) { type, key, additionalChars -> + parseComponents(type, key, additionalChars)?.entity?.let { returningList.add(it) } + } + return returningList + } + /** Same scan as [parseAll], restricted to the event-ish entities. */ fun parseAllEvents(content: String): List { val returningList = mutableListOf() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt index e48f60a840..902fedd5a2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt @@ -96,6 +96,10 @@ class BlossomAuthorizationEvent( createdAt: Long = TimeUtils.now(), ) = createAuth("delete", hash, null, alt, signer, servers, createdAt) + /** + * [expiration] defaults to an hour ahead. Servers that bound the token's lifetime (Buzz + * rejects `expiration > created_at + 60` in its strict mode) need a shorter one. + */ suspend fun createUploadAuth( hash: HexKey, size: Long, @@ -103,7 +107,8 @@ class BlossomAuthorizationEvent( signer: NostrSigner, servers: List = emptyList(), createdAt: Long = TimeUtils.now(), - ) = createAuth("upload", hash, size, alt, signer, servers, createdAt) + expiration: Long = TimeUtils.oneHourAhead(), + ) = createAuth("upload", hash, size, alt, signer, servers, createdAt, expiration) /** * BUD-05 media-optimization auth (`t=media`). The [hash] is the sha256 of @@ -127,6 +132,7 @@ class BlossomAuthorizationEvent( signer: NostrSigner, servers: List = emptyList(), createdAt: Long = TimeUtils.now(), + expiration: Long = TimeUtils.oneHourAhead(), ): BlossomAuthorizationEvent { // BUD-11 `server` tags scope the token to specific domains so an upload // or delete token can't be replayed against another server. The value @@ -141,7 +147,7 @@ class BlossomAuthorizationEvent( val tags = listOfNotNull( arrayOf("t", type), - arrayOf("expiration", TimeUtils.oneHourAhead().toString()), + arrayOf("expiration", expiration.toString()), fileSize?.let { arrayOf("size", it.toString()) }, hash?.let { arrayOf("x", it) }, ) + serverTags diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt new file mode 100644 index 0000000000..6c231eb58d --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt @@ -0,0 +1,67 @@ +/* + * 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.quartz.buzz.media + +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import kotlin.test.Test +import kotlin.test.assertEquals + +class BuzzImetaTest { + @Test + fun dropsKeysBuzzRejectsAndKeepsTheRest() { + val tag = + IMetaTag( + "https://ws.example/media/abc.jpg", + mapOf( + "x" to listOf("abc"), + "ox" to listOf("def"), + "m" to listOf("image/jpeg"), + "size" to listOf("2029"), + "dim" to listOf("96x96"), + "blurhash" to listOf("U1Ku"), + "alt" to listOf("caption"), + "content-warning" to listOf("nsfw"), + ), + ) + + val clean = BuzzImeta.sanitize(tag) + + assertEquals("https://ws.example/media/abc.jpg", clean.url) + assertEquals(setOf("x", "m", "size", "dim", "blurhash", "alt"), clean.properties.keys) + } + + @Test + fun keepsOneValueForSingletonsButEveryFallback() { + val tag = + IMetaTag( + "https://ws.example/media/abc.jpg", + mapOf( + "alt" to listOf("one", "two"), + "fallback" to listOf("https://a/1", "https://b/1"), + ), + ) + + val clean = BuzzImeta.sanitize(tag) + + assertEquals(listOf("one"), clean.properties["alt"]) + assertEquals(listOf("https://a/1", "https://b/1"), clean.properties["fallback"]) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt new file mode 100644 index 0000000000..74ffa32777 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt @@ -0,0 +1,94 @@ +/* + * 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.quartz.buzz.media + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class BuzzMediaSanitizerTest { + private fun bytes(vararg v: Int) = ByteArray(v.size) { v[it].toByte() } + + private fun seg( + marker: Int, + payload: ByteArray, + ): ByteArray { + val len = payload.size + 2 + return bytes(0xff, marker, len shr 8, len and 0xff) + payload + } + + private val jfif = seg(0xe0, "JFIF".encodeToByteArray() + bytes(0, 1, 1, 0, 0, 1, 0, 1, 0, 0)) + private val exif = seg(0xe1, "Exif".encodeToByteArray() + bytes(0, 0, 1, 2, 3, 4)) + private val icc = seg(0xe2, "ICC_PROFILE".encodeToByteArray() + bytes(0, 1, 1)) + private val comment = seg(0xfe, "made with a phone".encodeToByteArray()) + private val dqt = seg(0xdb, ByteArray(65) { 1 }) + private val sosAndData = seg(0xda, bytes(1, 1, 0, 0, 63, 0)) + bytes(0x12, 0x34, 0xff, 0x00, 0x56) + private val eoi = bytes(0xff, 0xd9) + + @Test + fun jpegLosesExifIccCommentsAndTrailingBytes() { + val input = bytes(0xff, 0xd8) + jfif + exif + icc + comment + dqt + sosAndData + eoi + "trailer".encodeToByteArray() + + val clean = BuzzMediaSanitizer.sanitize(input, "image/jpeg") + + assertContentEquals(bytes(0xff, 0xd8) + jfif + dqt + sosAndData + eoi, clean) + } + + @Test + fun nonCanonicalApp0IsDropped() { + val jfxx = seg(0xe0, "JFXX".encodeToByteArray() + bytes(0, 0x10, 1, 2, 3)) + val input = bytes(0xff, 0xd8) + jfxx + dqt + sosAndData + eoi + + assertContentEquals(bytes(0xff, 0xd8) + dqt + sosAndData + eoi, BuzzMediaSanitizer.sanitize(input, "image/jpeg")) + } + + private fun chunk( + kind: String, + data: ByteArray, + ) = bytes(data.size shr 24, data.size shr 16 and 0xff, data.size shr 8 and 0xff, data.size and 0xff) + kind.encodeToByteArray() + data + bytes(0, 0, 0, 0) + + private val pngSig = bytes(0x89, 'P'.code, 'N'.code, 'G'.code, 0x0d, 0x0a, 0x1a, 0x0a) + + @Test + fun pngKeepsRenderingChunksAndDropsMetadata() { + val ihdr = chunk("IHDR", ByteArray(13)) + val srgb = chunk("sRGB", bytes(0)) + val text = chunk("tEXt", "Comment\u0000hello".encodeToByteArray()) + val phys = chunk("pHYs", ByteArray(9)) + val iccp = chunk("iCCP", ByteArray(4)) + val idat = chunk("IDAT", bytes(1, 2, 3)) + val iend = chunk("IEND", ByteArray(0)) + val input = pngSig + ihdr + srgb + text + phys + iccp + idat + iend + bytes(9, 9) + + val clean = BuzzMediaSanitizer.sanitize(input, "image/png") + + assertContentEquals(pngSig + ihdr + srgb + idat + iend, clean) + } + + @Test + fun otherFormatsAndBrokenInputPassThrough() { + val webp = "RIFF....WEBP".encodeToByteArray() + assertContentEquals(webp, BuzzMediaSanitizer.sanitize(webp, "image/webp")) + val notJpeg = bytes(1, 2, 3, 4) + assertContentEquals(notJpeg, BuzzMediaSanitizer.sanitize(notJpeg, "image/jpeg")) + assertEquals(true, BuzzMediaSanitizer.handles("IMAGE/JPEG")) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsFilterCapTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsFilterCapTest.kt index bc15867fcf..e5e0427ecb 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsFilterCapTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsFilterCapTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.pool import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -81,8 +82,44 @@ class PoolRequestsFilterCapTest { fun learnsTheCapFromTheRefusal() { assertEquals(3, RelayReqRefusals.parseMaxFilters("ERROR: bad req: filter validation failed: invalid number of filters: 4")) assertEquals(null, RelayReqRefusals.parseMaxFilters("ERROR: bad req: filter validation failed: kind not allowed: 21059")) + assertEquals(10, RelayReqRefusals.parseMaxFilters("invalid message: Invalid message format: REQ contains 12 filters, maximum is 10")) } + /** + * Buzz refuses an over-cap REQ with a NOTICE, which names no subscription — the sub would wait + * for an EOSE forever. The cap it names must be learned and the over-cap sub sent again under it. + */ + @Test + fun aBuzzNoticeResendsTheOverCapSubUnderTheCap() = + kotlinx.coroutines.test.runTest { + val buzz = NormalizedRelayUrl("wss://ws.communities.buzz.xyz/") + val pool = PoolRequests() + pool.addOrUpdate("big", mapOf(buzz to groupFilters(12)), null) + pool.addOrUpdate("small", mapOf(buzz to groupFilters(2)), null) + pool.onConnecting(buzz) + pool.syncState(buzz) { } + + val client = RecordingRelayClient(buzz) + pool.onIncomingMessage(client, NoticeMessage("invalid message: Invalid message format: REQ contains 12 filters, maximum is 10")) + + val resent = client.sent.filterIsInstance() + assertEquals(listOf("big"), resent.map { it.subId }, "only the sub that went over the cap is re-sent") + assertTrue(resent.single().filters.size <= 10) + } + + @Test + fun anUnrelatedNoticeChangesNothing() = + kotlinx.coroutines.test.runTest { + val pool = PoolRequests() + pool.addOrUpdate("big", mapOf(relay to groupFilters(12)), null) + sync(pool, relay) + + val client = RecordingRelayClient(relay) + pool.onIncomingMessage(client, NoticeMessage("rate limited, slow down")) + + assertTrue(client.sent.isEmpty()) + } + @Test fun perGroupFiltersAreMergedUnderTheCapAndSentAgainAtOnce() = kotlinx.coroutines.test.runTest { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt new file mode 100644 index 0000000000..ee392fa8bd --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt @@ -0,0 +1,49 @@ +/* + * 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.quartz.nip10Notes.content + +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** An npub that is part of a link (a Blossom subdomain) is not a citation and must not be tagged. */ +class FindNostrUrisTest { + private val npub = "npub1rfahrcqpwh0rp7f2jcq8gsh4y04wawpthvw3fhmarvt8hsf80mvsr0tlnr" + + @Test + fun npubInsideABlossomHostnameIsNotCited() { + assertTrue(findNostrUris("https://$npub.blossom.band/e069d411.jpg").isEmpty()) + } + + @Test + fun npubInAPathIsNotCited() { + assertTrue(findNostrUris("see https://example.com/p/$npub").isEmpty()) + } + + @Test + fun standaloneReferencesAreStillCited() { + assertEquals(1, findNostrUris("hi nostr:$npub").filterIsInstance().size) + assertEquals(1, findNostrUris("hi @$npub, there").filterIsInstance().size) + assertEquals(1, findNostrUris("$npub at the start").filterIsInstance().size) + assertEquals(1, findNostrUris("($npub)").filterIsInstance().size) + } +} From c4337ecd7d2b337a29d2814edce77ee3f2344c44 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 03:24:59 +0000 Subject: [PATCH 31/45] polish the prediction market and ballot cards Rendered offscreen (ImageComposeScene, phone width, both themes) against the real production events, the cards read flat. Now: - a yes/no market draws its two sides full width, green and red; once resolved the winner keeps its colour and a check, the loser goes quiet - Cancelled is a quiet pill, like Closed, and its reason is gray: called off is not an error - the category reads as published ("ai-llm", not "Ai llm") - blank lines in a description collapse, so a clipped description no longer leaves a hole above the outcomes - a ballot's answers are a small table: one bordered block, a hairline between rows, the answer right-aligned, no chevrons Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012CsFEX4ZT8mL1r8h2M6qfH --- .../amethyst/commons/ui/note/types/Ballot.kt | 38 ++++++-- .../commons/ui/note/types/PredictionMarket.kt | 95 +++++++++++++++++-- 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt index e13767902f..8bff6dff38 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Ballot.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.ui.note.types +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,6 +28,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,8 +38,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -56,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier +import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.ballots.BallotAnswer import com.vitorpamplona.quartz.experimental.ballots.BallotEvent @@ -140,7 +146,17 @@ fun BallotCard(event: BallotEvent) { if (state.answers.isNotEmpty()) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - state.answers.forEach { AnswerRow(it) } + // The answers as a small table: one bordered block, a hairline between rows. + Column( + Modifier + .clip(AnswersShape) + .border(1.dp, MaterialTheme.colorScheme.subtleBorder, AnswersShape), + ) { + state.answers.forEachIndexed { i, answer -> + if (i > 0) HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.subtleBorder) + AnswerRow(answer) + } + } if (state.hiddenAnswers > 0) { Text( @@ -167,25 +183,25 @@ private fun AnswerRow(answer: BallotAnswer) { answer.question } - Row(verticalAlignment = Alignment.Top) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { Text( text = question, style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.grayText, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(0.4f), ) - Icon( - symbol = MaterialSymbols.AutoMirrored.KeyboardArrowRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.padding(horizontal = 4.dp).size(16.dp), - ) Text( text = answer.answer, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.End, maxLines = 3, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(0.6f), @@ -193,6 +209,8 @@ private fun AnswerRow(answer: BallotAnswer) { } } +private val AnswersShape = RoundedCornerShape(10.dp) + @Composable private fun ProofHash(hash: String) { Row(verticalAlignment = Alignment.CenterVertically) { diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt index 468069856b..bb2a22a4c7 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt @@ -131,18 +131,21 @@ private class PredictionMarketCardState( ) { fun isWinner(outcome: String) = resolution != null && outcome.equals(resolution, ignoreCase = true) + /** Two outcomes, one a yes and one a no — "YES"/"NO", "Yes (stays online)"/"No (goes offline)". */ + val isBinary = outcomes.size == 2 && outcomes.any(::isYes) && outcomes.any(::isNo) + companion object { fun from( event: PredictionMarketEvent, now: Long, ) = PredictionMarketCardState( title = event.title() ?: socialPostHeadline(event.content), - description = event.description(), + description = event.description()?.let(::collapseBlankLines), outcomes = event.outcomes().toImmutableList(), status = event.status(now), resolution = event.resolution(), endsAt = event.endsAt(), - category = event.category()?.let(::humanizeCategory), + category = event.category(), isDemo = event.isDemo(), minBetSats = event.minBetSats()?.let(::formatGrouped), maxBetSats = event.maxBetSats()?.let(::formatGrouped), @@ -150,6 +153,13 @@ private class PredictionMarketCardState( cancelReason = event.cancelReason(), ) + fun isYes(outcome: String) = YES.containsMatchIn(outcome) + + fun isNo(outcome: String) = NO.containsMatchIn(outcome) + + private val YES = Regex("""^\s*yes\b""", RegexOption.IGNORE_CASE) + private val NO = Regex("""^\s*no\b""", RegexOption.IGNORE_CASE) + /** The first line of a social-post `content`, for a market with no title anywhere else. */ private fun socialPostHeadline(content: String): String? { if (content.trimStart().startsWith("{")) return null @@ -162,11 +172,13 @@ private class PredictionMarketCardState( return if (value == asLong.toDouble()) "$asLong%" else "$value%" } - private fun humanizeCategory(category: String): String = - category - .replace('-', ' ') - .replace('_', ' ') - .replaceFirstChar { if (it.isLowerCase()) it.titlecaseChar() else it } + /** + * BAO's descriptions carry paragraph breaks ("…UTC.\n\nSource of Truth: …"); clipped to a + * few lines, a blank line reads as a hole in the card. One line break each, never a gap. + */ + private fun collapseBlankLines(text: String): String = text.trim().replace(BLANK_LINES, "\n") + + private val BLANK_LINES = Regex("""\s*\n\s*\n\s*""") } } @@ -274,12 +286,13 @@ private fun MarketStatusPill(status: PredictionMarketStatus) { ) } + // Called off is not an error: quiet, like Closed. PredictionMarketStatus.CANCELLED -> { StatusPill( label = stringRes(Res.string.prediction_market_status_cancelled), symbol = MaterialSymbols.Block, - container = Color(0xFFCF222E), - content = Color.White, + container = MaterialTheme.colorScheme.surfaceVariant, + content = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -318,6 +331,11 @@ private fun MarketOutcomes(state: PredictionMarketCardState) { // A cancelled market has no winner and takes no bets: its outcomes are shown, but muted. val muted = state.status == PredictionMarketStatus.CANCELLED + if (state.isBinary) { + BinaryOutcomes(state, muted) + return + } + FlowRow( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), @@ -336,6 +354,63 @@ private fun MarketOutcomes(state: PredictionMarketCardState) { } } +/** + * A yes/no market as its two sides, side by side and full width — the way betting apps lay out + * Yes and No: green and red while it is undecided; once it resolves the winner keeps its colour + * and a check, and the loser goes quiet. + */ +@Composable +private fun BinaryOutcomes( + state: PredictionMarketCardState, + muted: Boolean, +) { + val decided = state.resolution != null + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + state.outcomes.forEach { outcome -> + val isWinner = state.isWinner(outcome) + val quiet = muted || (decided && !isWinner) + val side = + if (PredictionMarketCardState.isYes(outcome)) { + MaterialTheme.colorScheme.allGoodColor + } else { + MaterialTheme.colorScheme.redColorOnSecondSurface + } + val color = if (quiet) MaterialTheme.colorScheme.placeholderText else side + Row( + modifier = + Modifier + .weight(1f) + .clip(SideShape) + .background(if (quiet) Color.Transparent else side.copy(alpha = if (isWinner) 0.20f else 0.12f)) + .border(1.dp, if (quiet) MaterialTheme.colorScheme.subtleBorder else side.copy(alpha = 0.45f), SideShape) + .padding(horizontal = 10.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (isWinner) { + Icon( + symbol = MaterialSymbols.CheckCircle, + contentDescription = stringRes(Res.string.prediction_market_winning_outcome), + tint = color, + filled = true, + modifier = Modifier.padding(end = 4.dp).size(16.dp), + ) + } + Text( + text = outcome, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = color, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +private val SideShape = RoundedCornerShape(10.dp) + @Composable private fun OutcomeChip( outcome: String, @@ -407,7 +482,7 @@ private fun CancelReason(reason: String) { MarketFootnote( symbol = MaterialSymbols.Block, text = stringRes(Res.string.prediction_market_cancel_reason, reason), - color = MaterialTheme.colorScheme.redColorOnSecondSurface, + color = MaterialTheme.colorScheme.placeholderText, ) } From 0d4241d65591d0113ae2d94557990305227a79d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 03:27:24 +0000 Subject: [PATCH 32/45] feat: redesign the Now Playing settings in the shared settings style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the screen on the SettingsSection / SettingsSwitchTile kit used by Profile UI, Home Tabs and Relay Auth settings: - "How others see it": a preview card with the account's avatar, name and live music status (same icon and color as the real status), with a Live / Example / Off chip. - "Share from": icon-badge switch tiles for Amethyst and other apps, with a quiet "Notification access is on · Manage" line once granted. - A tinted alert card with a single Grant access action while notification access is missing. - "Apps": real launcher icons on Android, colored monogram badges (the Messages settings palette) elsewhere; package ids only when they add information. Adds a headless render test of every state in both themes, which also catches missing string resources; set NOW_PLAYING_RENDER_DIR to write the PNGs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N2naGACCZy1GvqmZNCwpBu --- .../service/nowPlaying/AndroidAppIcon.kt | 66 +++ .../amethyst/ui/navigation/AppNavigation.kt | 3 +- .../composeResources/values/strings.xml | 24 +- .../settings/NowPlayingSettingsScreen.kt | 543 ++++++++++++++---- .../settings/NowPlayingSettingsRenderTest.kt | 156 +++++ 5 files changed, 673 insertions(+), 119 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/AndroidAppIcon.kt create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/AndroidAppIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/AndroidAppIcon.kt new file mode 100644 index 0000000000..7cea29a589 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/AndroidAppIcon.kt @@ -0,0 +1,66 @@ +/* + * 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.service.nowPlaying + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MonogramBadge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** The launcher icon of [packageName] for the now-playing app list, or a monogram if it is hidden. */ +@Composable +fun AndroidAppIcon( + packageName: String, + label: String, +) { + val context = LocalContext.current + val icon by produceState(null, packageName) { + value = + withContext(Dispatchers.IO) { + runCatching { + context.packageManager + .getApplicationIcon(packageName) + .toBitmap(ICON_PX, ICON_PX) + .asImageBitmap() + }.getOrNull() + } + } + + val current = icon + if (current != null) { + Image(bitmap = current, contentDescription = null, modifier = Modifier.fillMaxSize()) + } else { + MonogramBadge(label, size = 36, shape = RoundedCornerShape(10.dp)) + } +} + +private const val ICON_PX = 108 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 4bab06f572..7898b809ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -203,6 +203,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.WorkoutsSc import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages +import com.vitorpamplona.amethyst.service.nowPlaying.AndroidAppIcon import com.vitorpamplona.amethyst.service.nowPlaying.AndroidNowPlayingAccess import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthPromptHost import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages @@ -658,7 +659,7 @@ fun BuildNavigation( composableFromEnd { val context = LocalContext.current val access = remember(context) { AndroidNowPlayingAccess(context.applicationContext) } - NowPlayingSettingsScreen(accountViewModel, nav, access) + NowPlayingSettingsScreen(accountViewModel, nav, access) { appId, label -> AndroidAppIcon(appId, label) } } composableFromEnd { NotificationSettingsScreen(accountViewModel, nav) } composableFromEnd { ResourceUsageScreen(accountViewModel, nav) } diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 5a6a7e8a10..db818a9ed9 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -6364,15 +6364,23 @@ From %1$s Now Playing music listening status song podcast spotify nip38 share playing - Share the song or podcast you are listening to as your status. It shows under your name for anyone who sees your profile, and disappears when the track ends or you stop playing. - Share what I play in Amethyst - Music tracks and podcast episodes played in the app. - Share what I play in other apps - Spotify, YouTube Music, podcast players and any other app that shows media controls. - To see what other apps are playing, Amethyst needs notification access. It only reads media controls, never the content of your notifications. + Your status is public. It appears under your name and disappears when the track ends or you stop playing. + How others see it + Song title - Artist + Live + Example + Off + You + Share from + Amethyst + Music tracks and podcast episodes you play here + Other apps + Spotify, YouTube Music, podcast players and any app with media controls + Notification access needed + Android only shows other apps’ media controls to apps with notification access. Amethyst reads what is playing, never the content of your notifications. Grant access - Manage access - Notification access is on. + Manage + Notification access is on Apps Apps show up here after they play something. Turn one off to stop sharing it. diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt index 2ac357765c..bd1c6890dc 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt @@ -20,21 +20,33 @@ */ package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -43,20 +55,38 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.LifecycleResumeEffect +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.commons.hashtags.Tunestr +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingAccess import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote +import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserName +import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserStatuses import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.now_playing_access_granted import com.vitorpamplona.amethyst.commons.resources.now_playing_access_needed +import com.vitorpamplona.amethyst.commons.resources.now_playing_access_needed_title import com.vitorpamplona.amethyst.commons.resources.now_playing_apps import com.vitorpamplona.amethyst.commons.resources.now_playing_apps_empty import com.vitorpamplona.amethyst.commons.resources.now_playing_description import com.vitorpamplona.amethyst.commons.resources.now_playing_grant_access import com.vitorpamplona.amethyst.commons.resources.now_playing_manage_access +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_example +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_live +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_off +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_sample +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_title +import com.vitorpamplona.amethyst.commons.resources.now_playing_preview_you +import com.vitorpamplona.amethyst.commons.resources.now_playing_section_sources import com.vitorpamplona.amethyst.commons.resources.now_playing_settings import com.vitorpamplona.amethyst.commons.resources.now_playing_share_in_app import com.vitorpamplona.amethyst.commons.resources.now_playing_share_in_app_description @@ -64,20 +94,26 @@ import com.vitorpamplona.amethyst.commons.resources.now_playing_share_other_apps import com.vitorpamplona.amethyst.commons.resources.now_playing_share_other_apps_description import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon /** * Settings for sharing what the user listens to as their NIP-38 music status. * * [access] is the platform permission other apps' playback needs (Android: notification access); - * null where none is needed. + * null where none is needed. [appIcon] draws a real app icon where the platform can (Android); + * without it each app gets a colored monogram. */ @Composable fun NowPlayingSettingsScreen( accountViewModel: AccountViewModel, nav: INav, access: NowPlayingAccess? = null, + appIcon: (@Composable (appId: String, label: String) -> Unit)? = null, ) { Scaffold( topBar = { @@ -85,154 +121,441 @@ fun NowPlayingSettingsScreen( }, ) { padding -> Column( - Modifier - .padding(padding) - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 12.dp), ) { - NowPlayingSettingsContent(accountViewModel.account.nowPlayingSettings, access) + NowPlayingSettingsContent( + state = accountViewModel.account.nowPlayingSettings, + access = access, + appIcon = appIcon, + preview = { isOn -> AccountStatusPreview(accountViewModel, nav, isOn) }, + ) } } } /** - * The body of the settings screen, also embedded by front ends with their own settings layout. - * [showInApp] hides the in-app toggle where the app has no music or podcast player of its own. + * The body of the settings screen: a preview of the status as others see it, the two sources, + * notification access, and the per-app list. It does not scroll, so front ends with their own + * settings layout (desktop) can embed it. [showInApp] hides the in-app source where the app has + * no music or podcast player of its own; [preview] replaces the anonymous preview card with one + * showing the account (it receives whether sharing is on at all). */ @Composable fun NowPlayingSettingsContent( state: NowPlayingSettingsState, access: NowPlayingAccess?, + modifier: Modifier = Modifier, showInApp: Boolean = true, + appIcon: (@Composable (appId: String, label: String) -> Unit)? = null, + preview: @Composable (isOn: Boolean) -> Unit = { isOn -> + NowPlayingPreviewCard( + avatar = { MonogramBadge(stringRes(Res.string.now_playing_preview_you), size = 44, shape = CircleShape) }, + name = stringRes(Res.string.now_playing_preview_you), + liveStatus = null, + isOn = isOn, + ) + }, ) { val settings by state.flow.collectAsState() + val isOn = (showInApp && settings.shareInApp) || settings.shareOtherApps - Text( - text = stringRes(Res.string.now_playing_description), - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), - ) + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + SectionTitle(stringRes(Res.string.now_playing_preview_title)) + preview(isOn) + Text( + text = stringRes(Res.string.now_playing_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 4.dp), + ) + } - if (showInApp) { - SwitchRow( - title = stringRes(Res.string.now_playing_share_in_app), - description = stringRes(Res.string.now_playing_share_in_app_description), - checked = settings.shareInApp, - onCheckedChange = state::setShareInApp, - ) - } - - SwitchRow( - title = stringRes(Res.string.now_playing_share_other_apps), - description = stringRes(Res.string.now_playing_share_other_apps_description), - checked = settings.shareOtherApps, - onCheckedChange = state::setShareOtherApps, - ) - - if (!settings.shareOtherApps) { - Spacer(modifier = Modifier.height(16.dp)) - return - } - - if (access != null) { - AccessRow(access) - } - - HorizontalDivider(thickness = 4.dp, modifier = Modifier.padding(vertical = 8.dp)) - - Text( - text = stringRes(Res.string.now_playing_apps), - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp), - ) - - if (settings.knownApps.isEmpty()) { - Text( - text = stringRes(Res.string.now_playing_apps_empty), - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), - ) - } else { - settings.knownApps.entries - .sortedBy { it.value.lowercase() } - .forEach { (id, label) -> - SwitchRow( - title = label, - description = id.takeIf { it != label }, - checked = id !in settings.blockedApps, - onCheckedChange = { allowed -> state.setAppBlocked(id, !allowed) }, + SettingsSection(Res.string.now_playing_section_sources) { + if (showInApp) { + SettingsSwitchTile( + icon = MaterialSymbols.Headphones, + title = Res.string.now_playing_share_in_app, + description = Res.string.now_playing_share_in_app_description, + checked = settings.shareInApp, + onCheckedChange = state::setShareInApp, ) + SettingsDivider() } - } + SettingsSwitchTile( + icon = MaterialSymbols.Apps, + title = Res.string.now_playing_share_other_apps, + description = Res.string.now_playing_share_other_apps_description, + checked = settings.shareOtherApps, + onCheckedChange = state::setShareOtherApps, + ) + if (settings.shareOtherApps && access != null) { + AccessGrantedRow(access) + } + } - Spacer(modifier = Modifier.height(16.dp)) + if (settings.shareOtherApps && access != null) { + AccessNeededCard(access) + } + + if (settings.shareOtherApps) { + SettingsSection(Res.string.now_playing_apps) { + if (settings.knownApps.isEmpty()) { + Text( + text = stringRes(Res.string.now_playing_apps_empty), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), + ) + } else { + settings.knownApps.entries + .sortedBy { it.value.lowercase() } + .forEachIndexed { index, (id, label) -> + if (index > 0) SettingsDivider() + AppToggleRow( + id = id, + label = label, + checked = id !in settings.blockedApps, + appIcon = appIcon, + onCheckedChange = { allowed -> state.setAppBlocked(id, !allowed) }, + ) + } + } + } + } + } } +/** The account's own avatar, name and live music status, exactly as other users see them. */ @Composable -private fun AccessRow(access: NowPlayingAccess) { - // The user grants access in system settings and comes back, so check again on every resume. - var granted by remember(access) { mutableStateOf(access.hasAccess()) } - LifecycleResumeEffect(access) { - granted = access.hasAccess() - onPauseOrDispose {} - } +private fun AccountStatusPreview( + accountViewModel: AccountViewModel, + nav: INav, + isOn: Boolean, +) { + val user = remember(accountViewModel) { accountViewModel.account.userProfile() } + val name by observeUserName(user, accountViewModel) + val statuses by observeUserStatuses(user, accountViewModel) + val music = statuses.firstOrNull { it.address.dTag == UserStatusEvent.MUSIC } - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(if (granted) Res.string.now_playing_access_granted else Res.string.now_playing_access_needed), - fontSize = 13.sp, - color = if (granted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.error, - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(16.dp)) - OutlinedButton(onClick = access::requestAccess) { - Text(stringRes(if (granted) Res.string.now_playing_manage_access else Res.string.now_playing_grant_access)) + val avatar = @Composable { ClickableUserPicture(user, 44.dp, accountViewModel) } + + if (music == null) { + NowPlayingPreviewCard(avatar, name, liveStatus = null, isOn = isOn) + } else { + LiveMusicStatus(music, accountViewModel) { text -> + NowPlayingPreviewCard(avatar, name, liveStatus = text, isOn = isOn) } } } @Composable -private fun SwitchRow( - title: String, - description: String?, +private fun LiveMusicStatus( + note: AddressableNote, + accountViewModel: AccountViewModel, + content: @Composable (String?) -> Unit, +) { + val noteState by observeNote(note, accountViewModel) + content( + noteState.note.event + ?.content + ?.ifBlank { null }, + ) +} + +/** + * A miniature of how the status renders under the user's name in feeds: the avatar, the name and + * the music line with the same icon and color the real status uses. Shows [liveStatus] when one + * is published, otherwise an example, dimmed while sharing is off. + */ +@Composable +fun NowPlayingPreviewCard( + avatar: @Composable () -> Unit, + name: String, + liveStatus: String?, + isOn: Boolean, +) { + val contentAlpha = if (liveStatus != null || isOn) 1f else 0.55f + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.size(44.dp).clip(CircleShape)) { avatar() } + + Column( + modifier = Modifier.weight(1f).padding(start = 14.dp, end = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = name, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = CustomHashTagIcons.Tunestr, + contentDescription = null, + modifier = Modifier.size(15.dp).padding(end = 4.dp), + tint = MaterialTheme.colorScheme.placeholderText.copy(alpha = contentAlpha), + ) + Text( + text = liveStatus ?: stringRes(Res.string.now_playing_preview_example), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.placeholderText.copy(alpha = contentAlpha), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + StatusChip( + when { + liveStatus != null -> PreviewChip.LIVE + isOn -> PreviewChip.SAMPLE + else -> PreviewChip.OFF + }, + ) + } + } +} + +private enum class PreviewChip { LIVE, SAMPLE, OFF } + +@Composable +private fun StatusChip(chip: PreviewChip) { + val (container, content) = + when (chip) { + PreviewChip.LIVE -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + else -> MaterialTheme.colorScheme.surfaceContainerHighest to MaterialTheme.colorScheme.onSurfaceVariant + } + val label = + when (chip) { + PreviewChip.LIVE -> Res.string.now_playing_preview_live + PreviewChip.SAMPLE -> Res.string.now_playing_preview_sample + PreviewChip.OFF -> Res.string.now_playing_preview_off + } + + Row( + modifier = + Modifier + .clip(RoundedCornerShape(50)) + .background(container) + .padding(horizontal = 10.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (chip == PreviewChip.LIVE) { + Box(Modifier.size(6.dp).clip(CircleShape).background(content)) + Spacer(Modifier.width(6.dp)) + } + Text( + text = stringRes(label), + style = MaterialTheme.typography.labelMedium, + color = content, + ) + } +} + +/** Sub-row under the "Other apps" tile once access is granted: a quiet confirmation + Manage. */ +@Composable +private fun AccessGrantedRow(access: NowPlayingAccess) { + val granted = rememberAccessGranted(access) + if (!granted) return + + Row( + modifier = Modifier.fillMaxWidth().padding(start = 68.dp, end = 8.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SymbolIcon( + symbol = MaterialSymbols.CheckCircle, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringRes(Res.string.now_playing_access_granted), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f).padding(start = 6.dp), + ) + TextButton(onClick = access::requestAccess) { + Text(stringRes(Res.string.now_playing_manage_access)) + } + } +} + +/** The one thing blocking other-app sharing, as a tonal card with a single clear action. */ +@Composable +private fun AccessNeededCard(access: NowPlayingAccess) { + val granted = rememberAccessGranted(access) + if (granted) return + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + // A light wash of the error color rather than the full errorContainer: it flags the one + // step left without shouting louder than the rest of the screen, in either theme. + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.error.copy(alpha = 0.10f), + contentColor = MaterialTheme.colorScheme.onSurface, + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + ) { + Column(Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.error.copy(alpha = 0.16f)), + contentAlignment = Alignment.Center, + ) { + SymbolIcon( + symbol = MaterialSymbols.NotificationsOff, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.error, + ) + } + Text( + text = stringRes(Res.string.now_playing_access_needed_title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(start = 16.dp), + ) + } + Text( + text = stringRes(Res.string.now_playing_access_needed), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 52.dp, top = 4.dp), + ) + Spacer(Modifier.height(12.dp)) + Button( + onClick = access::requestAccess, + modifier = Modifier.align(Alignment.End), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringRes(Res.string.now_playing_grant_access)) + } + } + } +} + +/** The user grants access in system settings and comes back, so this re-checks on every resume. */ +@Composable +private fun rememberAccessGranted(access: NowPlayingAccess): Boolean { + var granted by remember(access) { mutableStateOf(access.hasAccess()) } + LifecycleResumeEffect(access) { + granted = access.hasAccess() + onPauseOrDispose {} + } + return granted +} + +@Composable +private fun AppToggleRow( + id: String, + label: String, checked: Boolean, + appIcon: (@Composable (appId: String, label: String) -> Unit)?, onCheckedChange: (Boolean) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 12.dp), + .clickable { onCheckedChange(!checked) } + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - ) - if (description != null) { + Box(Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)), contentAlignment = Alignment.Center) { + if (appIcon != null) appIcon(id, label) else MonogramBadge(label, size = 36, shape = RoundedCornerShape(10.dp)) + } + Column( + modifier = Modifier.weight(1f).padding(start = 16.dp, end = 12.dp), + ) { + Text(text = label, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) + // The package name tells apart two apps with the same name; skip it when it only + // repeats the name (desktop player ids such as `spotify` or `vlc`). + if (!label.lowercase().filterNot { it.isWhitespace() }.contains(id.lowercase())) { Text( - text = description, - fontSize = 13.sp, + text = id, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } - Spacer(modifier = Modifier.width(16.dp)) - Switch( - checked = checked, - onCheckedChange = onCheckedChange, + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} + +// The accent palette of the Messages settings, so app badges read as the same family. +private val MONOGRAM_COLORS = + listOf( + Color(0xFF2EBD85), + Color(0xFFF6A609), + Color(0xFF2E90FA), + Color(0xFF9E77ED), + Color(0xFF5B6AD0), + Color(0xFFEC4899), + Color(0xFF14B8A6), + Color(0xFFEF4444), + Color(0xFF06B6D4), + ) + +/** A tinted badge with the first letter of [label], colored by the label so it stays stable. */ +@Composable +fun MonogramBadge( + label: String, + size: Int, + shape: Shape, +) { + val accent = MONOGRAM_COLORS[(label.hashCode() and Int.MAX_VALUE) % MONOGRAM_COLORS.size] + Box( + modifier = Modifier.size(size.dp).clip(shape).background(accent.copy(alpha = 0.18f)), + contentAlignment = Alignment.Center, + ) { + Text( + text = label.trim().take(1).uppercase(), + style = if (size >= 40) MaterialTheme.typography.titleMedium else MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = accent, ) } } + +@Composable +private fun SectionTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 4.dp), + ) +} diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt new file mode 100644 index 0000000000..69ee28bc70 --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt @@ -0,0 +1,156 @@ +/* + * 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.commons.ui.screen.loggedIn.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingAccess +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettingsState +import com.vitorpamplona.amethyst.commons.ui.theme.AmethystPreviewTheme +import org.jetbrains.skia.EncodedImageFormat +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.File +import javax.imageio.ImageIO +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Renders the Now Playing settings headlessly in both themes. Like the other render tests it + * checks structure, not exact pixels: every string resolves (a missing one throws here) and the + * screen follows the theme. Set NOW_PLAYING_RENDER_DIR to also write the PNGs for a human to look at. + */ +class NowPlayingSettingsRenderTest { + private val width = 820 + private val height = 1900 + + private class FakeAccess( + val granted: Boolean, + ) : NowPlayingAccess { + override fun hasAccess() = granted + + override fun requestAccess() {} + } + + private fun state(settings: NowPlayingSettings) = NowPlayingSettingsState().apply { restore(settings) } + + private val apps = + mapOf( + "com.spotify.music" to "Spotify", + "com.google.android.apps.youtube.music" to "YouTube Music", + "de.danoeh.antennapod" to "AntennaPod", + ) + + private val scenarios: Map Unit> = + mapOf( + "off" to { NowPlayingSettingsContent(state(NowPlayingSettings()), FakeAccess(false)) }, + "needs-access" to { + NowPlayingSettingsContent(state(NowPlayingSettings(shareInApp = true, shareOtherApps = true)), FakeAccess(false)) + }, + "on" to { + NowPlayingSettingsContent( + state( + NowPlayingSettings( + shareInApp = true, + shareOtherApps = true, + knownApps = apps, + blockedApps = setOf("com.google.android.apps.youtube.music"), + ), + ), + FakeAccess(true), + preview = { isOn -> + NowPlayingPreviewCard( + avatar = { MonogramBadge("Vitor", size = 44, shape = CircleShape) }, + name = "Vitor", + liveStatus = "One More Time - Daft Punk", + isOn = isOn, + ) + }, + ) + }, + "desktop" to { + NowPlayingSettingsContent( + state(NowPlayingSettings(shareOtherApps = true, knownApps = mapOf("spotify" to "Spotify", "vlc" to "VLC media player"))), + access = null, + showInApp = false, + ) + }, + ) + + private fun render( + dark: Boolean, + content: @Composable () -> Unit, + ): BufferedImage { + val scene = + ImageComposeScene(width = width, height = height, density = Density(2f)) { + AmethystPreviewTheme(dark = dark) { + Column( + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + content() + } + } + } + return try { + val png = scene.render().encodeToData(EncodedImageFormat.PNG)!!.bytes + System.getenv("NOW_PLAYING_RENDER_DIR")?.let { dir -> + val name = scenarios.entries.first { it.value === content }.key + File(dir, "now-playing-$name-${if (dark) "dark" else "light"}.png").writeBytes(png) + } + ImageIO.read(ByteArrayInputStream(png)) + } finally { + scene.close() + } + } + + private fun BufferedImage.distinctColours(): Int { + val seen = mutableSetOf() + for (x in 0 until width step 3) { + for (y in 0 until height step 3) seen += getRGB(x, y) + } + return seen.size + } + + @Test + fun everyStateRendersInBothThemes() { + scenarios.values.forEach { content -> + val light = render(false, content) + val dark = render(true, content) + + assertTrue(light.distinctColours() > 20, "the light screen drew ${light.distinctColours()} colours") + assertTrue(dark.distinctColours() > 20, "the dark screen drew ${dark.distinctColours()} colours") + assertTrue(light.getRGB(2, 2) != dark.getRGB(2, 2), "the screen ignored the theme") + } + } +} From 864fcae134f3597bb202ea1ca1a44004da3fa62d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Oct 2026 08:46:03 -0400 Subject: [PATCH 33/45] fix(buzz): fixes from the Buzz-side retest - The profile now actually reaches the workspace: Buzz refuses an old kind-0 ("event timestamp too far from server time"), so sign a fresh copy with the same content for that relay only, once per profile version. It follows the profile note, and the top bar's Buzz check is reactive (a cold start opened the channel before the relay was marked Buzz, so the push never ran and Join stayed hidden). - Attachments are written as `![image](url)` in Buzz messages: Buzz only draws media linked that way, and showed a bare URL as a link. - A Buzz @mention notifies even from someone I don't follow (workspaces are membership-gated, like Concord). - Forum comments count as replies of their post, so the forum list shows the right reply count; forums opened from Messages land on their threads. - Kind-9 notification cards and quotes show the latest edit. Co-Authored-By: Claude Opus 5.5 --- .../relayGroup/RelayGroupTopBar.kt | 14 +++++++-- .../send/ChannelNewMessageViewModel.kt | 3 +- .../chats/rooms/ChatroomHeaderCompose.kt | 10 ++++++- .../amethyst/commons/model/Account.kt | 3 +- .../commons/model/AccountRelayGroupActions.kt | 29 ++++++++++++++----- .../commons/model/cache/EventCache.kt | 7 +++++ .../dal/NotificationFeedFilter.kt | 27 ++++++++++++++++- .../amethyst/commons/ui/note/types/Chat.kt | 7 +++-- .../quartz/buzz/media/BuzzImeta.kt | 20 +++++++++++++ .../quartz/buzz/media/BuzzImetaTest.kt | 19 ++++++++++++ 10 files changed, 123 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index 6f4ac8d105..8422bc9b5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -161,11 +161,19 @@ fun RelayGroupTopBar( var showInvite by remember { mutableStateOf(false) } var showJoinCode by remember { mutableStateOf(false) } var confirmDelete by remember { mutableStateOf(false) } - val isBuzzRelay = remember(channel.groupId.relayUrl) { BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) } + // Reactive: a relay is only marked Buzz once its first verified Buzz event lands, which can be + // after this screen opens (e.g. straight from Messages on a cold start). A remembered snapshot + // stayed false for the screen's life, hiding Join and never sharing the profile. + val buzzRelays by BuzzRelayDialect.flow.collectAsStateWithLifecycle() + val isBuzzRelay = channel.groupId.relayUrl in buzzRelays val huddleLive by observeBuzzHuddleLive(channel, enabled = isBuzzRelay && membership.isMember(), accountViewModel) - // Buzz names members only from profiles on its own relay; make sure mine is there. + // Buzz names members only from profiles on its own relay; make sure mine is there. Follows the + // profile note so a profile that loads after the screen opens (or is edited later) still goes out. LaunchedEffect(channel.groupId.relayUrl, isBuzzRelay) { - if (isBuzzRelay) accountViewModel.account.relayGroups.shareProfileWithBuzzWorkspace(channel.groupId.relayUrl) + if (!isBuzzRelay) return@LaunchedEffect + accountViewModel.account.userMetadata + .getUserMetadataFlow() + .collect { accountViewModel.account.relayGroups.shareProfileWithBuzzWorkspace(channel.groupId.relayUrl) } } TopBarExtensibleWithBackButton( 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 920362c2e6..cba4b4a5ed 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 @@ -901,7 +901,8 @@ open class ChannelNewMessageViewModel : val threadRoot = parent?.let { it.event?.tags?.buzzThreadRootForReplyTo(it.idHex) ?: it.idHex } BuzzChatMessage.build( channelId = channel.groupId.id, - content = tagger.message, + // Buzz draws an attachment only where the body links it as `![image](url)`. + content = BuzzImeta.markdownMediaBody(tagger.message, usedAttachments), threadRoot = threadRoot, replyTo = parent?.idHex, // `p` mentions for everyone cited in the body (plus the reply target, which the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 4f77d73d5d..572158dea8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -161,6 +161,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberM import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent +import com.vitorpamplona.quartz.buzz.workspace.isBuzzForum import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -812,7 +813,14 @@ fun RelayGroupRow( accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() .value, - onClick = { nav.nav(Route.RelayGroup(channel.groupId.id, channel.groupId.relayUrl.url)) }, + onClick = { + // A Buzz forum's posts are its threads; open it there, as the workspace list does. + if (channel.event?.isBuzzForum() == true) { + nav.nav(Route.RelayGroupThreads(channel.groupId.id, channel.groupId.relayUrl.url)) + } else { + nav.nav(Route.RelayGroup(channel.groupId.id, channel.groupId.relayUrl.url)) + } + }, onLongClick = { menuOpen = true }, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt index c44fcaa61f..886cbeea46 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Account.kt @@ -2243,7 +2243,8 @@ class Account( // Attached media rides as URLs appended to the content, plus Buzz-shaped `imeta`. val root = rootEvent.tags.buzzThreadRootForReplyTo(rootEvent.id) signer.sign( - ChatEvent.build(finalText) { + // Buzz draws an attachment only where the body links it as `![image](url)`. + ChatEvent.build(BuzzImeta.markdownMediaBody(finalText, imetas)) { hTag(group.groupId.id) buzzThread(root, rootEvent.id) rootNote.author?.pubkeyHex?.let { pTag(PTag(it)) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt index 081d47de10..8a944a14d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.quartz.buzz.workspace.BUZZ_VISIBILITY_PRIVATE import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.PublishResult import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllWithHooks @@ -112,26 +113,37 @@ class AccountRelayGroupActions( account.follow(channel) } - // "relay|metadata event id" pairs already pushed this session, so each profile version goes to - // each workspace once. + // "relay|profile content" pairs already pushed this session, so each profile version goes to each + // workspace once. Keyed on content, not event id: the copy we sign comes back from the relay as a + // newer kind-0 with the same content, and must not trigger another signature. private val profileSharedWith = MutableStateFlow>(emptySet()) /** * Push my current kind-0 profile to the Buzz workspace at [relay]. Buzz reads member names and * pictures only from its own relay, and its people search (mentions, Add people, new DM) is by - * name — without this, Buzz showed me as a bare npub and nobody could find or add me. Sends the - * already-signed event as-is (no new signature), once per profile version per workspace. + * name — without this, Buzz showed me as a bare npub and nobody could find or add me. + * + * Buzz refuses an event whose timestamp is far from its clock ("event timestamp too far from + * server time"), so the already-signed profile — usually days old — can't be relayed as-is. Like + * Buzz's own client, this signs a fresh copy with the same content and tags, and sends it ONLY to + * that workspace, once per profile version. */ - fun shareProfileWithBuzzWorkspace(relay: NormalizedRelayUrl) { + suspend fun shareProfileWithBuzzWorkspace(relay: NormalizedRelayUrl) { if (!account.isWriteable()) return val profile = account.userMetadata.getUserMetadataEvent() ?: return - val key = "${relay.url}|${profile.id}" + val key = "${relay.url}|${profile.content.hashCode()}|${profile.tags.contentDeepHashCode()}" while (true) { val current = profileSharedWith.value if (key in current) return if (profileSharedWith.compareAndSet(current, current + key)) break } - account.client.publish(profile, setOf(relay)) + val fresh = + if (TimeUtils.now() - profile.createdAt < BUZZ_FRESH_PROFILE_SECS) { + profile + } else { + account.signer.sign(TimeUtils.now(), MetadataEvent.KIND, profile.tags, profile.content) + } + account.client.publish(fresh, setOf(relay)) } /** @@ -692,3 +704,6 @@ internal fun buzzRoleFor(roles: List): String { else -> BUZZ_ROLE_MEMBER } } + +/** A profile signed this recently is within Buzz's timestamp window and can be relayed as-is. */ +private const val BUZZ_FRESH_PROFILE_SECS = 60L diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index eab486d0d5..76a2b68b82 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -1382,6 +1382,13 @@ open class EventCache : listOfNotNull(event.parentPullRequestId()?.let { checkGetOrCreateNote(it) }) } + is ForumCommentEvent -> { + // A Buzz forum comment counts as a reply of its thread's root post (and of the comment + // it answers, when nested), so the forum list's reply count sees it. A direct reply + // carries only the `reply` marker, which then IS the root. + listOfNotNull(event.threadRoot(), event.replyTo()).distinct().map { getOrCreateNote(it) } + } + is GitStatusEvent -> { // A status event roots itself at a patch/PR/issue via a // marked-`root` `e` tag; link only that so the transition diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt index a6c26603e3..5b30df6d39 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel @@ -406,6 +407,7 @@ class NotificationFeedFilter( override fun feed(): List { val filterParams = buildFilterParams(account) + com.vitorpamplona.quartz.utils.Log val notifications = LocalCache.notes.filterIntoSet { _, note -> @@ -435,6 +437,7 @@ class NotificationFeedFilter( private fun innerApplyFilter(collection: Collection): Set { val filterParams = buildFilterParams(account) + com.vitorpamplona.quartz.utils.Log return collection.filterTo(HashSet()) { acceptableEvent(it, filterParams) } } @@ -474,6 +477,25 @@ class NotificationFeedFilter( ?.pubkeyHex == me } + /** + * A Buzz workspace message that `p`-tags me — Buzz only `p`-tags @mentions. A workspace is a + * closed, membership-gated space, so a coworker's @mention is relevant whether or not I follow + * them (like Concord), and it should not depend on the per-kind reply heuristics either: Buzz + * mentions name no event of mine. + */ + private fun isBuzzMentionOfMe( + note: Note, + me: HexKey, + ): Boolean { + val event = note.event + if (event !is ChatEvent && event !is StreamMessageV2Event) return false + if (!event.isTaggedUser(me)) return false + val group = LocalCache.getRelayGroupChannelForContent(note) + com.vitorpamplona.quartz.utils.Log + if (group == null) return false + return BuzzRelayDialect.isBuzz(group.groupId.relayUrl) + } + /** * A Buzz chat **thread reply** into one of my messages, when the reply carries no `p` tag. * @@ -509,6 +531,9 @@ class NotificationFeedFilter( filterParams: FilterByListParams, ): Boolean { val loggedInUserHex = account.userProfile().pubkeyHex + if (it.event?.content?.startsWith("BZ-6") == true) { + com.vitorpamplona.quartz.utils.Log + } // When the user opts out of seeing Messages on the Notification tab, drop // direct/group message events (DMs and Marmot group chats) entirely. @@ -666,7 +691,7 @@ class NotificationFeedFilter( val isReactionToMe = isReactionToMyEvent(it, loggedInUserHex) // Same no-`p`-tag rescue for Buzz thread replies into my messages. - val isThreadReplyToMe = isBuzzThreadReplyToMyEvent(it, loggedInUserHex) + val isThreadReplyToMe = isBuzzThreadReplyToMyEvent(it, loggedInUserHex) || isBuzzMentionOfMe(it, loggedInUserHex) // Concord CHAT (a message/reply) honors the "Messages in notifications" toggle that silences DMs // and Marmot groups above. A reaction isn't a message — regular reactions ignore that toggle, so diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Chat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Chat.kt index 5127725c1e..d53d5f2161 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Chat.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Chat.kt @@ -58,14 +58,17 @@ fun RenderChat( isBoostedNote: Boolean = false, ) { val noteEvent = note.event ?: return - val eventContent = remember(noteEvent) { noteEvent.content } + // The newest in-place edit (Buzz, Concord, Marmot) wins — the chat room shows it, so a + // notification card or quote of the same message must too. + val content = observeChatMessageEdit(note)?.event?.content ?: noteEvent.content + val eventContent = content // Content actually handed to the media renderer. The shared renderer only shows media whose URL // appears in the text, but some NIP-C7/Concord clients (e.g. Ditto/Soapbox Armada) attach an // image purely as a NIP-92 `imeta` tag and leave the content empty. Append any imeta URL missing // from the content so those attachments render — symmetric to ChannelChat.imageMessage, which // appends the URL on send. Encrypted-blob key/nonce are already registered by URL, so the shared // pipeline fetches and decrypts them transparently. - val displayContent = remember(noteEvent) { appendMissingImetaUrls(noteEvent.content, noteEvent) } + val displayContent = remember(noteEvent, content) { appendMissingImetaUrls(content, noteEvent) } // A boosted note inside a zap/nutzap/onchain activity card is always shown as a // compact 2-line preview, even when the logged-in user is only a zap-split diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt index 9e62843c1d..8357b4e686 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImeta.kt @@ -45,4 +45,24 @@ object BuzzImeta { ) fun sanitize(tags: List): List = tags.map(::sanitize) + + /** + * Buzz's clients draw an attachment only where the body links it in markdown — `![image](url)` + * or `![video](url)`, the form their composer writes — and show a bare URL as a plain link. So + * each attachment URL that stands alone in [content] becomes that markdown; one already inside + * markdown is left alone. + */ + fun markdownMediaBody( + content: String, + attachments: List, + ): String { + var out = content + attachments.forEach { tag -> + val url = tag.url + if (url.isBlank() || out.contains("]($url)")) return@forEach + val label = if (tag.properties["m"]?.firstOrNull()?.startsWith("video/") == true) "video" else "image" + out = out.replace(url, "![$label]($url)") + } + return out + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt index 6c231eb58d..68fc606c47 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzImetaTest.kt @@ -64,4 +64,23 @@ class BuzzImetaTest { assertEquals(listOf("one"), clean.properties["alt"]) assertEquals(listOf("https://a/1", "https://b/1"), clean.properties["fallback"]) } + + @Test + fun bareAttachmentUrlsBecomeBuzzMarkdown() { + val img = IMetaTag("https://ws.example/media/a.jpg", mapOf("m" to listOf("image/jpeg"))) + val vid = IMetaTag("https://ws.example/media/b.mp4", mapOf("m" to listOf("video/mp4"))) + + assertEquals( + "look ![image](https://ws.example/media/a.jpg) and ![video](https://ws.example/media/b.mp4)", + BuzzImeta.markdownMediaBody("look https://ws.example/media/a.jpg and https://ws.example/media/b.mp4", listOf(img, vid)), + ) + } + + @Test + fun urlsAlreadyInMarkdownStayAsTheyAre() { + val img = IMetaTag("https://ws.example/media/a.jpg", mapOf("m" to listOf("image/jpeg"))) + val body = "![image](https://ws.example/media/a.jpg)" + + assertEquals(body, BuzzImeta.markdownMediaBody(body, listOf(img))) + } } From 70252ea08a4246ece601c2e218f55c9ed59ba812 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:02:32 +0000 Subject: [PATCH 34/45] fix: kind-38000 audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Unrecognised 38000s fell back to a plain Event, which stores do not key by `d` (they test `is AddressableEvent`): a spam bot republishing one address kept every copy, an `a`-tag deletion missed them, and a newer junk version never replaced an older recommendation. They are now UnrecognizedKind38000Event — addressable, never indexed, no card — and EventCache files them as addressables instead of logging each one's JSON at WARN. - The card's blank-line regex rescanned long whitespace runs from every position: a crafted ~64 KB description froze the main thread for seconds. Collapsed line by line instead. - The fee printed Double.toString ("4.21%" beside German "100.000 sats", "NaN%" for junk). New locale-aware formatDecimal (expect/actual); non-finite or negative fees are dropped. - The status badge and the closing-time line read the clock at different moments, and a market declared closed before its end read "Closed ". Both now read the same moment; a closed-early market states no closing time. - Outcomes are (id, label): `resolution` names the id, the card shows the label — `["outcome","opt1","Foundry USA"]` and `{"id":"A", "label":"Foundry"}` resolved to "A" now mark their winner. Deduped by set and capped at 64 against crafted floods. - isMintRecommendation accepts any `k` naming a mint kind (not only the first) and a recommendation that names its mint only by `a`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012CsFEX4ZT8mL1r8h2M6qfH --- .../commons/model/cache/EventCache.kt | 3 ++ .../amethyst/commons/util/GroupedNumber.kt | 6 +++ .../commons/util/GroupedNumber.ios.kt | 13 ++++++ .../commons/util/GroupedNumber.jvmAndroid.kt | 5 +++ .../commons/ui/note/types/PredictionMarket.kt | 45 ++++++++++++------- .../PredictionMarketDetails.kt | 40 ++++++++++++----- .../PredictionMarketEvent.kt | 7 ++- .../PredictionMarketOutcome.kt | 42 +++++++++++++++++ .../predictionMarkets/TagArrayExt.kt | 14 +++--- .../recommendation/MintRecommendationEvent.kt | 25 ++++++++--- .../UnrecognizedKind38000Event.kt | 44 ++++++++++++++++++ .../quartz/utils/EventFactory.kt | 8 ++-- .../PredictionMarketEventTest.kt | 29 ++++++++++++ .../quartz/utils/EventFactoryKind38000Test.kt | 38 ++++++++++------ 14 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketOutcome.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/UnrecognizedKind38000Event.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index ca70a9b4ff..3d05686f7e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -390,6 +390,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent +import com.vitorpamplona.quartz.nip87Ecash.recommendation.UnrecognizedKind38000Event import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -3967,6 +3968,8 @@ open class EventCache : // Other apps' kind-38000 formats, split from mint recommendations by EventFactory. is BallotEvent, is PredictionMarketEvent, + // Unread junk on 38000 still supersedes an older version at its address. + is UnrecognizedKind38000Event, -> consumeBaseReplaceable(event, relay, wasVerified) // ============================================================ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt index 89e3f1a75e..3f3c034ad3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt @@ -22,3 +22,9 @@ package com.vitorpamplona.amethyst.commons.util /** [value] with the locale's digit grouping: `1,234,567` in English, `1.234.567` in German. */ expect fun formatGrouped(value: Long): String + +/** [value] with the locale's decimal mark and at most [maxFractionDigits] digits after it: `4.21` in English, `4,21` in German. */ +expect fun formatDecimal( + value: Double, + maxFractionDigits: Int, +): String diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt index 22e852b588..8cb15ad3f1 100644 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt @@ -23,9 +23,22 @@ package com.vitorpamplona.amethyst.commons.util import platform.Foundation.NSNumber import platform.Foundation.NSNumberFormatter import platform.Foundation.NSNumberFormatterDecimalStyle +import platform.Foundation.numberWithDouble import platform.Foundation.numberWithLongLong actual fun formatGrouped(value: Long): String { val formatter = NSNumberFormatter().apply { numberStyle = NSNumberFormatterDecimalStyle } return formatter.stringFromNumber(NSNumber.numberWithLongLong(value)) ?: value.toString() } + +actual fun formatDecimal( + value: Double, + maxFractionDigits: Int, +): String { + val formatter = + NSNumberFormatter().apply { + numberStyle = NSNumberFormatterDecimalStyle + maximumFractionDigits = maxFractionDigits.toULong() + } + return formatter.stringFromNumber(NSNumber.numberWithDouble(value)) ?: value.toString() +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt index b2e00ab2b1..c145f8ecf0 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt @@ -23,3 +23,8 @@ package com.vitorpamplona.amethyst.commons.util import java.text.NumberFormat actual fun formatGrouped(value: Long): String = NumberFormat.getNumberInstance().format(value) + +actual fun formatDecimal( + value: Double, + maxFractionDigits: Int, +): String = NumberFormat.getNumberInstance().apply { maximumFractionDigits = maxFractionDigits }.format(value) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt index bb2a22a4c7..f811a041e7 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/PredictionMarket.kt @@ -80,9 +80,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.redColorOnSecondSurface import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.ui.theme.warningColorOnSecondSurface +import com.vitorpamplona.amethyst.commons.util.formatDecimal import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent +import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketOutcome import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketStatus import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList @@ -117,7 +119,7 @@ fun RenderPredictionMarket( private class PredictionMarketCardState( val title: String?, val description: String?, - val outcomes: ImmutableList, + val outcomes: ImmutableList, val status: PredictionMarketStatus?, val resolution: String?, val endsAt: Long?, @@ -128,11 +130,13 @@ private class PredictionMarketCardState( val maxBetSats: String?, val feePercent: String?, val cancelReason: String?, + /** The moment the status was read, so the closing-time line agrees with the status badge. */ + val now: Long, ) { - fun isWinner(outcome: String) = resolution != null && outcome.equals(resolution, ignoreCase = true) + fun isWinner(outcome: PredictionMarketOutcome) = outcome.isNamedBy(resolution) /** Two outcomes, one a yes and one a no — "YES"/"NO", "Yes (stays online)"/"No (goes offline)". */ - val isBinary = outcomes.size == 2 && outcomes.any(::isYes) && outcomes.any(::isNo) + val isBinary = outcomes.size == 2 && outcomes.any { isYes(it.label) } && outcomes.any { isNo(it.label) } companion object { fun from( @@ -141,7 +145,7 @@ private class PredictionMarketCardState( ) = PredictionMarketCardState( title = event.title() ?: socialPostHeadline(event.content), description = event.description()?.let(::collapseBlankLines), - outcomes = event.outcomes().toImmutableList(), + outcomes = event.outcomeOptions().toImmutableList(), status = event.status(now), resolution = event.resolution(), endsAt = event.endsAt(), @@ -151,6 +155,7 @@ private class PredictionMarketCardState( maxBetSats = event.maxBetSats()?.let(::formatGrouped), feePercent = event.feePercent()?.let(::formatPercent), cancelReason = event.cancelReason(), + now = now, ) fun isYes(outcome: String) = YES.containsMatchIn(outcome) @@ -166,19 +171,21 @@ private class PredictionMarketCardState( return content.lineSequence().firstOrNull { it.isNotBlank() }?.trim() } - /** 4.21 → "4.21%", 2.0 → "2%". */ - private fun formatPercent(value: Double): String { - val asLong = value.toLong() - return if (value == asLong.toDouble()) "$asLong%" else "$value%" - } + /** 4.21 → "4.21%" ("4,21%" in German), 2.0 → "2%"; nothing for NaN, infinities or a negative. */ + private fun formatPercent(value: Double): String? = if (value.isFinite() && value >= 0) "${formatDecimal(value, 2)}%" else null /** * BAO's descriptions carry paragraph breaks ("…UTC.\n\nSource of Truth: …"); clipped to a * few lines, a blank line reads as a hole in the card. One line break each, never a gap. */ - private fun collapseBlankLines(text: String): String = text.trim().replace(BLANK_LINES, "\n") - - private val BLANK_LINES = Regex("""\s*\n\s*\n\s*""") + private fun collapseBlankLines(text: String): String = + // Line by line rather than a regex: a run of whitespace in a crafted event made the + // obvious pattern rescan it from every position — seconds on the main thread. + text + .lineSequence() + .map { it.trim() } + .filter { it.isNotEmpty() } + .joinToString("\n") } } @@ -227,7 +234,7 @@ fun PredictionMarketCard( if (state.status == PredictionMarketStatus.CANCELLED) { state.cancelReason?.let { CancelReason(it) } } else { - state.endsAt?.let { ClosingTime(it, state.status) } + state.endsAt?.let { ClosingTime(it, state.status, state.now) } } MarketTerms(state.minBetSats, state.maxBetSats, state.feePercent) @@ -341,7 +348,7 @@ private fun MarketOutcomes(state: PredictionMarketCardState) { verticalArrangement = Arrangement.spacedBy(6.dp), ) { shown.forEach { outcome -> - OutcomeChip(outcome, isWinner = state.isWinner(outcome), muted = muted) + OutcomeChip(outcome.label, isWinner = state.isWinner(outcome), muted = muted) } if (hidden > 0) { Text( @@ -370,7 +377,7 @@ private fun BinaryOutcomes( val isWinner = state.isWinner(outcome) val quiet = muted || (decided && !isWinner) val side = - if (PredictionMarketCardState.isYes(outcome)) { + if (PredictionMarketCardState.isYes(outcome.label)) { MaterialTheme.colorScheme.allGoodColor } else { MaterialTheme.colorScheme.redColorOnSecondSurface @@ -397,7 +404,7 @@ private fun BinaryOutcomes( ) } Text( - text = outcome, + text = outcome.label, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = color, @@ -460,8 +467,12 @@ private fun OutcomeChip( private fun ClosingTime( endsAt: Long, status: PredictionMarketStatus?, + now: Long, ) { - val stillOpen = status == PredictionMarketStatus.OPEN && endsAt > TimeUtils.now() + // Read against the same moment as the status badge, so the two never disagree. A market + // declared closed or settled before its end has no closing time worth stating. + val stillOpen = status == PredictionMarketStatus.OPEN && endsAt > now + if (!stillOpen && endsAt > now) return val text = if (stillOpen) { stringRes(Res.string.prediction_market_closes_in, timeAheadNoDot(endsAt)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt index b1a72b6463..a8ee4c5204 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketDetails.kt @@ -38,7 +38,7 @@ import kotlinx.serialization.json.longOrNull class PredictionMarketDetails( val title: String?, val description: String?, - val outcomes: List, + val outcomes: List, val category: String?, val status: String?, val reason: String?, @@ -95,24 +95,40 @@ class PredictionMarketDetails( private fun JsonObject.double(key: String): Double? = primitive(key)?.doubleOrNull /** - * `outcomes` as display strings: a plain string per outcome (current shape), or an object - * per outcome whose `label` (else `id`, `name`) names it (BAO Fund and first shapes). + * `outcomes`: a plain string per outcome (current shape), or an object per outcome with an + * `id` and a `label` (else `name`) (BAO Fund and first shapes). Once per id, at most + * [PredictionMarketOutcome.MAX]. */ - private fun JsonObject.outcomes(): List { + private fun JsonObject.outcomes(): List { val array = this["outcomes"] as? JsonArray ?: return emptyList() - val result = ArrayList(array.size) + val result = LinkedHashMap(minOf(array.size, PredictionMarketOutcome.MAX)) for (element in array) { - val name = outcomeName(element) - if (name != null && name !in result) result.add(name) + if (result.size >= PredictionMarketOutcome.MAX) break + val outcome = outcomeOf(element) ?: continue + if (outcome.id !in result) result[outcome.id] = outcome } - return result + return result.values.toList() } - private fun outcomeName(element: JsonElement): String? = + private fun outcomeOf(element: JsonElement): PredictionMarketOutcome? = when (element) { - is JsonPrimitive -> element.takeIf { it.isString }?.content?.ifBlank { null } - is JsonObject -> element.text("label") ?: element.text("id") ?: element.text("name") - else -> null + is JsonPrimitive -> { + element + .takeIf { it.isString } + ?.content + ?.ifBlank { null } + ?.let { PredictionMarketOutcome(it, it) } + } + + is JsonObject -> { + val label = element.text("label") ?: element.text("name") + val id = element.text("id") ?: label + if (id == null) null else PredictionMarketOutcome(id, label ?: id) + } + + else -> { + null + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt index 5b23c4f6ee..a81262523f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEvent.kt @@ -81,12 +81,15 @@ class PredictionMarketEvent( fun description() = dataDetails?.description ?: contentDetails?.description - /** The outcomes one can bet on: the `outcome` tags, else the JSON `outcomes`. */ - fun outcomes(): List = + /** The outcomes one can bet on, id and label: the `outcome` tags, else the JSON `outcomes`. */ + fun outcomeOptions(): List = tags.marketOutcomes().ifEmpty { dataDetails?.outcomes?.ifEmpty { null } ?: contentDetails?.outcomes ?: emptyList() } + /** The outcomes as a reader sees them, by label. */ + fun outcomes(): List = outcomeOptions().map { it.label } + /** The winning outcome, once resolved. */ fun resolution() = tags.marketResolution() ?: dataDetails?.resolution ?: contentDetails?.resolution diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketOutcome.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketOutcome.kt new file mode 100644 index 0000000000..24ca9a1548 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketOutcome.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.predictionMarkets + +import androidx.compose.runtime.Immutable + +/** + * One answer a market can resolve to. BAO writes some as a bare word (`["outcome","YES"]`, a + * JSON string) and some as an id with a label (`["outcome","opt1","Foundry USA"]`, + * `{"id":"A","label":"Yes"}`): a `resolution` names the [id], a reader is shown the [label]. + */ +@Immutable +data class PredictionMarketOutcome( + val id: String, + val label: String, +) { + /** Whether [resolution] names this outcome — by its id, or by its label as some markets do. */ + fun isNamedBy(resolution: String?): Boolean = resolution != null && (id.equals(resolution, ignoreCase = true) || label.equals(resolution, ignoreCase = true)) + + companion object { + /** More than any market offers; a crafted event with thousands is cut here. */ + const val MAX = 64 + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt index c4cdd43c4a..c5b308ec43 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/TagArrayExt.kt @@ -57,15 +57,17 @@ fun TagArray.marketNetwork() = firstNonBlankValue("network", "n") fun TagArray.marketCategory() = firstNonBlankValue("category", "c") /** - * Every `outcome` tag's value, in order. The BAO Fund shape writes `["outcome", id, label]`; - * the id is what a `resolution` names, so it is the one returned. + * Every `outcome` tag, in order, once per id and at most [PredictionMarketOutcome.MAX]. The BAO + * Fund shape writes `["outcome", id, label]`; the current shape a bare word, its own label. */ -fun TagArray.marketOutcomes(): List { - val result = ArrayList(2) +fun TagArray.marketOutcomes(): List { + val result = LinkedHashMap(4) fastForEach { - if (it.size > 1 && it[0] == "outcome" && it[1].isNotBlank() && it[1] !in result) result.add(it[1]) + if (result.size < PredictionMarketOutcome.MAX && it.size > 1 && it[0] == "outcome" && it[1].isNotBlank() && it[1] !in result) { + result[it[1]] = PredictionMarketOutcome(it[1], it.getOrNull(2)?.ifBlank { null } ?: it[1]) + } } - return result + return result.values.toList() } /** The winning outcome, once the market is resolved. */ diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt index 962b0142ed..ec26ccff8e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/MintRecommendationEvent.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.fastAny -import com.vitorpamplona.quartz.nip01Core.core.fastFirstOrNull import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor import com.vitorpamplona.quartz.nip50Search.SearchableEvent @@ -73,17 +72,29 @@ class MintRecommendationEvent( * the kind number alone does not say what an event is. A recommendation names the kind of * the mint it recommends in its `k` tag — 38172 (Cashu) or 38173 (Fedimint). Some early * recommendations carry no `k` at all; those are accepted when they point at a mint URL - * (`u`). A `k` naming anything else is not a mint recommendation, whatever else it carries. + * (`u`) or at the mint's announcement (an `a` to a 38172/38173 address). A `k` naming + * only other kinds is not a mint recommendation, whatever else it carries. */ fun isMintRecommendation(tags: TagArray): Boolean { - val kTag = tags.fastFirstOrNull { it.size > 1 && it[0] == "k" } - if (kTag != null) { - val mintKind = kTag[1].toIntOrNull() - return mintKind == CashuMintEvent.KIND || mintKind == FedimintEvent.KIND + // Any `k` naming a mint's kind: a relay answering `#k=38172` may hand back an event + // whose first `k` is something else. + if (tags.fastAny { it.size > 1 && it[0] == "k" && it[1].toIntOrNull().isMintKind() }) return true + if (tags.fastAny { it.size > 1 && it[0] == "k" && it[1].isNotBlank() }) return false + return tags.fastAny { + it.size > 1 && + ( + (it[0] == MintUrlTag.TAG_NAME && it[1].isNotBlank()) || + // The mint's own announcement, by address: `38172::`. + (it[0] == "a" && (it[1].startsWith(CASHU_ADDRESS) || it[1].startsWith(FEDIMINT_ADDRESS))) + ) } - return tags.fastAny { it.size > 1 && it[0] == MintUrlTag.TAG_NAME && it[1].isNotBlank() } } + private fun Int?.isMintKind() = this == CashuMintEvent.KIND || this == FedimintEvent.KIND + + private val CASHU_ADDRESS = "${CashuMintEvent.KIND}:" + private val FEDIMINT_ADDRESS = "${FedimintEvent.KIND}:" + /** * [mintKind] must be [CashuMintEvent.KIND] or [FedimintEvent.KIND]: the `k` tag it writes is * what [isMintRecommendation] reads back, so any other value signs into a plain kind-38000 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/UnrecognizedKind38000Event.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/UnrecognizedKind38000Event.kt new file mode 100644 index 0000000000..5f39ac627e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip87Ecash/recommendation/UnrecognizedKind38000Event.kt @@ -0,0 +1,44 @@ +/* + * 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.quartz.nip87Ecash.recommendation + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +/** + * A kind-38000 event that is none of the formats Amethyst reads on that number — not a NIP-87 + * [MintRecommendationEvent], a ballot or a prediction market. Most of them are `d`-only spam. + * + * Still addressable, because the kind is: stores key it by `d` (`event is AddressableEvent`), so + * a newer version replaces an older one and an `a`-tag deletion reaches it, as NIP-01 requires + * of every kind in 30000–39999. Not a SearchableEvent, so it is never indexed, and no card is + * drawn for it. + */ +@Immutable +class UnrecognizedKind38000Event( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, MintRecommendationEvent.KIND, tags, content, sig) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index c3d7d2969b..b46d9aa31e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -372,6 +372,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent +import com.vitorpamplona.quartz.nip87Ecash.recommendation.UnrecognizedKind38000Event import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -762,14 +763,15 @@ class EventFactory { // ballots, and most of what a big relay holds for it is spam (`d`-only "sybil test // votes"). Parsing all of it as a recommendation rendered markets, ballots and spam // as ecash mint cards. Disambiguate by tags, recommendation first — its `k` (the - // recommended mint's kind) or, on old events without one, its `u` (mint URL) — and - // fall back to a plain Event so junk gets no card and no search index entry. + // recommended mint's kind) or, on old events without one, its `u` (mint URL) or the + // mint's `a` — and fall back to UnrecognizedKind38000Event: still addressable, so + // stores replace and delete it by `d`, but never indexed and drawn as no card. MintRecommendationEvent.KIND -> when { MintRecommendationEvent.isMintRecommendation(tags) -> MintRecommendationEvent(id, pubKey, createdAt, tags, content, sig) BallotEvent.isBallot(tags) -> BallotEvent(id, pubKey, createdAt, tags, content, sig) PredictionMarketEvent.isPredictionMarket(tags) -> PredictionMarketEvent(id, pubKey, createdAt, tags, content, sig) - else -> Event(id, pubKey, createdAt, kind, tags, content, sig) + else -> UnrecognizedKind38000Event(id, pubKey, createdAt, tags, content, sig) } MediaFollowListEvent.KIND -> MediaFollowListEvent(id, pubKey, createdAt, tags, content, sig) MediaStarterPackEvent.KIND -> MediaStarterPackEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt index 48eeb74508..b1b361a774 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/predictionMarkets/PredictionMarketEventTest.kt @@ -281,4 +281,33 @@ class PredictionMarketEventTest { assertEquals(1, seen) } } + + @Test + fun outcomesKeepTheirIdForTheWinnerAndShowTheirLabel() { + // `resolution` names the id; a reader is shown the label. + val tagged = + market( + arrayOf( + arrayOf("market", "m"), + arrayOf("outcome", "opt1", "Foundry USA"), + arrayOf("outcome", "opt2", "AntPool"), + arrayOf("resolution", "opt1"), + ), + "", + ) + assertEquals(listOf("Foundry USA", "AntPool"), tagged.outcomes()) + val winner = tagged.outcomeOptions().single { it.isNamedBy(tagged.resolution()) } + assertEquals(PredictionMarketOutcome("opt1", "Foundry USA"), winner) + + val json = market(arrayOf(arrayOf("market", "m")), """{"outcomes":[{"id":"A","label":"Foundry"},{"id":"B","label":"Other"}]}""") + assertEquals(listOf("Foundry", "Other"), json.outcomes()) + assertTrue(json.outcomeOptions().first().isNamedBy("a")) + } + + @Test + fun aCraftedFloodOfOutcomesIsCut() { + val flood = Array(5000) { arrayOf("outcome", "o$it") } + val event = market(arrayOf(arrayOf("market", "m"), *flood), "") + assertEquals(PredictionMarketOutcome.MAX, event.outcomeOptions().size) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt index f622681611..ddc351887c 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/EventFactoryKind38000Test.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.utils import com.vitorpamplona.quartz.experimental.ballots.BallotEvent import com.vitorpamplona.quartz.experimental.predictionMarkets.PredictionMarketEvent +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync @@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent +import com.vitorpamplona.quartz.nip87Ecash.recommendation.UnrecognizedKind38000Event import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -105,29 +107,39 @@ class EventFactoryKind38000Test { } @Test - fun spamAndOneOffsArePlainEvents() { + fun spamAndOneOffsAreUnrecognizedButStillAddressable() { listOf( Kind38000Fixtures.SPAM_SYBIL_VOTE, Kind38000Fixtures.SPAM_APP_MANIFEST, ).forEach { val event = parse(it) - assertEquals(Event::class, event::class) + assertIs(event) + // Stores key 30000–39999 by `d` only when the event is addressable: replacement and + // `a`-tag deletion must still reach the spam. + assertIs(event) assertFalse(event is SearchableEvent) } } @Test - fun aKNamingAnotherKindIsNotARecommendationEvenWithAMintUrl() { - assertEquals(Event::class, build(arrayOf("k", "1"), arrayOf("u", "https://mint.example"))::class) - // The first k decides. - assertEquals(Event::class, build(arrayOf("k", "1"), arrayOf("k", "38172"))::class) + fun aKNamingOnlyOtherKindsIsNotARecommendationEvenWithAMintUrl() { + assertIs(build(arrayOf("k", "1"), arrayOf("u", "https://mint.example"))) + // Any k naming a mint kind counts, wherever it sits. + assertIs(build(arrayOf("k", "1"), arrayOf("k", "38172"))) assertIs(build(arrayOf("k", "38173"), arrayOf("k", "1"))) } + @Test + fun aRecommendationNamingItsMintOnlyByAddressIsOne() { + assertIs(build(arrayOf("d", "x"), arrayOf("a", "38172:${"a".repeat(64)}:x"))) + assertIs(build(arrayOf("d", "x"), arrayOf("a", "38173:${"a".repeat(64)}:x"))) + assertIs(build(arrayOf("d", "x"), arrayOf("a", "30023:${"a".repeat(64)}:x"))) + } + @Test fun aBlankMintUrlWithoutKIsNotARecommendation() { - assertEquals(Event::class, build(arrayOf("d", "x"), arrayOf("u", " "))::class) - assertEquals(Event::class, build(arrayOf("u"))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("d", "x"), arrayOf("u", " "))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("u"))::class) } @Test @@ -142,7 +154,7 @@ class EventFactoryKind38000Test { @Test fun aBlankElectionIsNotABallot() { - assertEquals(Event::class, build(arrayOf("election", ""))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("election", ""))::class) } @Test @@ -151,10 +163,10 @@ class EventFactoryKind38000Test { assertIs(build(arrayOf("outcome", "YES"), arrayOf("outcome", "NO"))) assertIs(build(arrayOf("type", "binary"), arrayOf("end", "1"))) - assertEquals(Event::class, build(arrayOf("market", " "))::class) - assertEquals(Event::class, build(arrayOf("outcome", "YES"))::class) - assertEquals(Event::class, build(arrayOf("type", "binary"))::class) - assertEquals(Event::class, build(arrayOf("end", "1"))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("market", " "))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("outcome", "YES"))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("type", "binary"))::class) + assertEquals(UnrecognizedKind38000Event::class, build(arrayOf("end", "1"))::class) } @Test From 89598b9f26ba3199b5a94bee774d00f24bc163f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:04:35 +0000 Subject: [PATCH 35/45] docs: kind-38000 fallback is UnrecognizedKind38000Event Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012CsFEX4ZT8mL1r8h2M6qfH --- .../searchable-events/references/searchable-kinds.md | 9 +++++---- .../com/vitorpamplona/quartz/utils/EventFactory.kt | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index 1a95ebd664..02761064ca 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -158,10 +158,11 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. may hold encrypted private items in `content`, which must never be indexed. ‡ **Kind 38000 is shared** by NIP-87 mint recommendations and two app formats, and `EventFactory` -picks the class by tags: `MintRecommendationEvent` when the first `k` is 38172/38173 (or, with no -`k`, a non-blank `u`); else `BallotEvent` on a non-blank `election`; else `PredictionMarketEvent` -on a `market`, ≥2 `outcome`s, or `type` + `end`; else a plain, unsearchable `Event` (the spam -votes that make up most of the kind). Kind-level probes (`EventFactory.probe`) answer as +picks the class by tags: `MintRecommendationEvent` when any `k` is 38172/38173 (or, with no +`k`, a non-blank `u` or an `a` to a 38172/38173 address); else `BallotEvent` on a non-blank +`election`; else `PredictionMarketEvent` on a `market`, ≥2 `outcome`s, or `type` + `end`; else +`UnrecognizedKind38000Event` — addressable (stores still key it by `d`) but unsearchable (the +spam votes that make up most of the kind). Kind-level probes (`EventFactory.probe`) answer as `MintRecommendationEvent`. A market's `title()`/`description()` come from its `data` tag JSON, the `title` tag, or JSON `content`, in that order. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index b46d9aa31e..8e4e8e53c9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -966,8 +966,8 @@ class EventFactory { * * For most kinds that is just [create] with no tags. It is not for kinds whose class is * chosen by tags, where a tagless event can land on a different class than the kind's - * real events. Kind 38000 is the case that matters: with no tags it is a plain [Event] - * (junk), yet every typed shape of it — mint recommendation, ballot, prediction market — + * real events. Kind 38000 is the case that matters: with no tags it is an + * [UnrecognizedKind38000Event] (junk, unsearchable), yet every typed shape of it — mint recommendation, ballot, prediction market — * is a searchable, renderable class, so it answers as its primary class. (The other * tag-split kinds, 39005 and 20001, already land on a typed class with no tags.) * From e63488f664b7cec54e80ff086e7ab997e2389fa4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 1 Oct 2026 09:11:49 -0400 Subject: [PATCH 36/45] fix(buzz): audit fixes for the Buzz interop PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove leftover diagnostic statements in NotificationFeedFilter (one ran a content check on every notification candidate). - Join on a Buzz open channel joins directly: every Buzz channel carries the `closed` tag, so the button opened the invite-code dialog instead. - Profile push: a signer failure no longer crashes the channel screen, and a failed attempt is retried later instead of being marked done. - `OK false "duplicate: …"` counts as accepted, not as a refusal. - Auto-join on post happens once per channel instead of on every post until the relay's roster catches up (each re-sent 9021 and re-published 10009). - The thread screen's Buzz check is reactive (cold-start trap, as in the top bar), so uploads and typing target the workspace once it is recognized. - Only domain-label/glued NIP-19 entities are skipped as citations; path links such as njump.me/npub1… keep being cited as before. - JPEG sanitizer walks entropy-coded scans: drops metadata after a scan and ends at the real EOI, so appended payloads containing FF D9 are cut. - observeChatMessageEdit resolves its initial value once per note instead of on every recomposition; forum-comment reply links validate their ids. Co-Authored-By: Claude Opus 5.5 --- .../loggedIn/chats/minichat/MinichatScreen.kt | 11 ++-- .../relayGroup/RelayGroupTopBar.kt | 16 +++++- .../send/ChannelNewMessageViewModel.kt | 8 +++ .../commons/model/AccountRelayGroupActions.kt | 21 ++++--- .../commons/model/cache/EventCache.kt | 2 +- .../dal/NotificationFeedFilter.kt | 9 +-- .../amethyst/commons/ui/note/types/Text.kt | 5 +- .../quartz/buzz/media/BuzzMediaSanitizer.kt | 55 ++++++++++++------- .../RelayInsertConfirmationCollector.kt | 9 ++- .../quartz/nip19Bech32/Nip19Parser.kt | 15 +++-- .../buzz/media/BuzzMediaSanitizerTest.kt | 19 +++++++ .../nip10Notes/content/FindNostrUrisTest.kt | 11 +++- 12 files changed, 126 insertions(+), 55 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index eb61bfed05..59dc042f44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -161,12 +161,11 @@ fun MinichatScreen( val canPost by remember { derivedStateOf { composer.text.isNotBlank() } } // A thread in a Buzz workspace channel: its media must live on the workspace's own server // (the relay refuses any other image host) and its typing signal is scoped to the thread. - val buzzGroup = - remember(rootNote) { - rootNote.inGatherers - ?.firstNotNullOfOrNull { it as? RelayGroupChannel } - ?.takeIf { BuzzRelayDialect.isBuzz(it.groupId.relayUrl) } - } + // Reactive on the dialect: a relay is marked Buzz only once its first verified Buzz event lands, + // which a cold start can reach after this screen opens. + val buzzRelays by BuzzRelayDialect.flow.collectAsStateWithLifecycle() + val relayGroup = remember(rootNote) { rootNote.inGatherers?.firstNotNullOfOrNull { it as? RelayGroupChannel } } + val buzzGroup = relayGroup?.takeIf { it.groupId.relayUrl in buzzRelays } val uploadState = remember(buzzGroup) { ChatFileUploadState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt index 8422bc9b5f..77cc0e4caa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupTopBar.kt @@ -173,7 +173,15 @@ fun RelayGroupTopBar( if (!isBuzzRelay) return@LaunchedEffect accountViewModel.account.userMetadata .getUserMetadataFlow() - .collect { accountViewModel.account.relayGroups.shareProfileWithBuzzWorkspace(channel.groupId.relayUrl) } + .collect { + try { + accountViewModel.account.relayGroups.shareProfileWithBuzzWorkspace(channel.groupId.relayUrl) + } catch (e: Exception) { + if (e is CancellationException) throw e + // A refused/failed signature must not take down the screen; it retries next time. + Log.w("RelayGroupTopBar", "Could not share the profile with ${channel.groupId.relayUrl.url}", e) + } + } } TopBarExtensibleWithBackButton( @@ -286,8 +294,10 @@ fun RelayGroupTopBar( // is still what Buzz lists, counts and offers in its mention picker — its own client // shows Join here too, so a reader can become a member. FilledTonalButton(onClick = { - // Closed groups need an invite code; open groups join directly. - if (channel.isClosed()) { + // Closed groups need an invite code; open groups join directly. Every Buzz channel + // carries the `closed` tag, so a Buzz OPEN channel (one that doesn't require + // membership to post) joins directly too. + if (channel.isClosed() && channel.requiresMembershipToPost()) { showJoinCode = true } else { requested = true 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 cba4b4a5ed..6357c2a0ed 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 @@ -115,6 +115,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip22Comments.notify import com.vitorpamplona.quartz.nip28PublicChat.base.notify import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip29RelayGroups.hTag import com.vitorpamplona.quartz.nip29RelayGroups.moderation.previous import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag @@ -139,6 +140,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.cancellation.CancellationException @Stable @@ -630,6 +632,9 @@ open class ChannelNewMessageViewModel : } } + // Buzz channels this composer has already tried to join (see [joinBuzzChannelBeforePosting]). + private val buzzJoinAttempted: MutableSet = ConcurrentHashMap.newKeySet() + /** * Posting to a Buzz open channel joins it first. The relay takes a non-member's message there, * but channel membership is what Buzz lists, counts and offers in its mention picker, and its @@ -640,6 +645,9 @@ open class ChannelNewMessageViewModel : if (channel !is RelayGroupChannel || !BuzzRelayDialect.isBuzz(channel.groupId.relayUrl)) return if (channel.event?.isBuzzDm() == true) return if (channel.membershipOf(accountViewModel.account.userProfile().pubkeyHex).isMember()) return + // Once per channel: the relay's roster (39002) lags the join, and every post before it + // lands would otherwise re-send the kind-9021 and re-publish the kind-10009 list. + if (!buzzJoinAttempted.add(channel.groupId)) return try { accountViewModel.account.relayGroups.joinRelayGroup(channel) } catch (e: Exception) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt index 8a944a14d6..1abf949bab 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/AccountRelayGroupActions.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupPin import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi @@ -137,13 +138,19 @@ class AccountRelayGroupActions( if (key in current) return if (profileSharedWith.compareAndSet(current, current + key)) break } - val fresh = - if (TimeUtils.now() - profile.createdAt < BUZZ_FRESH_PROFILE_SECS) { - profile - } else { - account.signer.sign(TimeUtils.now(), MetadataEvent.KIND, profile.tags, profile.content) - } - account.client.publish(fresh, setOf(relay)) + try { + val fresh = + if (TimeUtils.now() - profile.createdAt < BUZZ_FRESH_PROFILE_SECS) { + profile + } else { + account.signer.sign(TimeUtils.now(), MetadataEvent.KIND, profile.tags, profile.content) + } + account.client.publish(fresh, setOf(relay)) + } catch (e: Throwable) { + // Not shared (signer refused, timed out, …): forget the attempt so a later visit retries. + profileSharedWith.update { it - key } + throw e + } } /** diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 76a2b68b82..1ecddfa028 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -1386,7 +1386,7 @@ open class EventCache : // A Buzz forum comment counts as a reply of its thread's root post (and of the comment // it answers, when nested), so the forum list's reply count sees it. A direct reply // carries only the `reply` marker, which then IS the root. - listOfNotNull(event.threadRoot(), event.replyTo()).distinct().map { getOrCreateNote(it) } + listOfNotNull(event.threadRoot(), event.replyTo()).distinct().mapNotNull { checkGetOrCreateNote(it) } } is GitStatusEvent -> { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt index 5b30df6d39..4a9979e5e1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/dal/NotificationFeedFilter.kt @@ -407,7 +407,6 @@ class NotificationFeedFilter( override fun feed(): List { val filterParams = buildFilterParams(account) - com.vitorpamplona.quartz.utils.Log val notifications = LocalCache.notes.filterIntoSet { _, note -> @@ -437,7 +436,6 @@ class NotificationFeedFilter( private fun innerApplyFilter(collection: Collection): Set { val filterParams = buildFilterParams(account) - com.vitorpamplona.quartz.utils.Log return collection.filterTo(HashSet()) { acceptableEvent(it, filterParams) } } @@ -490,9 +488,7 @@ class NotificationFeedFilter( val event = note.event if (event !is ChatEvent && event !is StreamMessageV2Event) return false if (!event.isTaggedUser(me)) return false - val group = LocalCache.getRelayGroupChannelForContent(note) - com.vitorpamplona.quartz.utils.Log - if (group == null) return false + val group = LocalCache.getRelayGroupChannelForContent(note) ?: return false return BuzzRelayDialect.isBuzz(group.groupId.relayUrl) } @@ -531,9 +527,6 @@ class NotificationFeedFilter( filterParams: FilterByListParams, ): Boolean { val loggedInUserHex = account.userProfile().pubkeyHex - if (it.event?.content?.startsWith("BZ-6") == true) { - com.vitorpamplona.quartz.utils.Log - } // When the user opts out of seeing Messages on the Notification tab, drop // direct/group message events (DMs and Marmot group chats) entirely. diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt index ef8420054b..c89ac89230 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/Text.kt @@ -237,8 +237,11 @@ fun RenderTextEvent( /** The newest in-place edit of a chat message (Concord, Buzz or Marmot), recomposing as edits land. */ @Composable fun observeChatMessageEdit(note: Note): Note? { + // Resolved once per note, not on every recomposition: produceState's initialValue argument is + // re-evaluated each time this runs, and these feeds recompose a lot. + val initial = remember(note) { note.latestChatEdit() } val latest by - produceState(initialValue = note.latestChatEdit(), note.idHex) { + produceState(initialValue = initial, note.idHex) { note .flow() .edits.stateFlow diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt index 3e6519ed2a..d3a228fdc7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizer.kt @@ -45,6 +45,12 @@ object BuzzMediaSanitizer { private fun ByteArray.u8(i: Int) = this[i].toInt() and 0xff + /** + * Walks the JPEG marker by marker, entropy-coded scans included, so a metadata segment after a + * scan (progressive files interleave tables and scans) is dropped like one before it, and the + * file ends at the end-of-image marker that closes the last scan — not at whatever `FFD9` some + * appended payload (a motion-photo video, an extra JPEG) happens to contain. + */ fun stripJpeg(b: ByteArray): ByteArray? { if (b.size < 4 || b.u8(0) != 0xff || b.u8(1) != 0xd8) return null val out = ByteBuilder(b.size) @@ -58,17 +64,7 @@ object BuzzMediaSanitizer { val marker = b.u8(j) when { marker == 0xd9 -> { - out.write(byteArrayOf(0xff.toByte(), 0xd9.toByte()), 0, 2) - return out.toByteArray() - } - - marker == 0xda -> { - // Start of scan: entropy-coded data (and any later tables/scans) runs to the - // last end-of-image marker; whatever trails it is dropped. - val eoi = lastEoi(b) - if (eoi < j) return null - out.write(byteArrayOf(0xff.toByte()), 0, 1) - out.write(b, j, eoi + 2 - j) + out.write(EOI, 0, 2) return out.toByteArray() } @@ -77,6 +73,8 @@ object BuzzMediaSanitizer { i = j + 1 } + marker == 0xd8 -> return null + else -> { if (j + 3 > b.size) return null val len = (b.u8(j + 1) shl 8) or b.u8(j + 2) @@ -84,16 +82,38 @@ object BuzzMediaSanitizer { val end = j + 1 + len if (end > b.size) return null if (keepJpegSegment(marker, b, j + 3, end)) { - out.write(byteArrayOf(0xff.toByte()), 0, 1) + out.write(FF, 0, 1) out.write(b, j, end - j) } i = end + if (marker == 0xda) { + // Entropy-coded data runs to the next marker that isn't a stuffed byte + // (FF 00) or a restart (FF D0-D7). + val scanEnd = endOfScan(b, end) ?: return null + out.write(b, end, scanEnd - end) + i = scanEnd + } } } } return null } + private fun endOfScan( + b: ByteArray, + from: Int, + ): Int? { + var k = from + while (k + 1 < b.size) { + if (b.u8(k) == 0xff) { + val next = b.u8(k + 1) + if (next != 0x00 && next != 0xff && next !in 0xd0..0xd7) return k + } + k++ + } + return null + } + private fun keepJpegSegment( marker: Int, b: ByteArray, @@ -121,14 +141,9 @@ object BuzzMediaSanitizer { } } - private fun lastEoi(b: ByteArray): Int { - var k = b.size - 2 - while (k >= 0) { - if (b.u8(k) == 0xff && b.u8(k + 1) == 0xd9) return k - k-- - } - return -1 - } + private val FF = byteArrayOf(0xff.toByte()) + + private val EOI = byteArrayOf(0xff.toByte(), 0xd9.toByte()) private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 'P'.code.toByte(), 'N'.code.toByte(), 'G'.code.toByte(), 0x0d, 0x0a, 0x1a, 0x0a) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt index 3c81bf8b7f..c4046ccbfc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt @@ -31,7 +31,8 @@ import com.vitorpamplona.quartz.utils.Log /** * Listens to INostrClient's relay messages and reports `OK`s for published events: [onRelayReceived] for an acceptance and, when given, * [onRelayRejected] for a definitive refusal with the relay's reason. An `auth-required:` refusal - * is NOT reported — the client authenticates and resends, so it is not the relay's final answer. + * is NOT reported — the client authenticates and resends, so it is not the relay's final answer — + * and a `duplicate:` refusal counts as an acceptance. */ class RelayInsertConfirmationCollector( val client: INostrClient, @@ -46,7 +47,9 @@ class RelayInsertConfirmationCollector( msg: Message, ) { if (msg !is OkMessage) return - if (msg.success) { + // NIP-01: "duplicate:" means the relay already has the event. Most relays send it + // with `true`, some with `false`; either way the event is there. + if (msg.success || msg.message.startsWith(DUPLICATE_PREFIX)) { onRelayReceived(msg.eventId, relay) } else if (!msg.message.startsWith(AUTH_REQUIRED_PREFIX)) { onRelayRejected?.invoke(msg.eventId, relay, msg.message) @@ -67,3 +70,5 @@ class RelayInsertConfirmationCollector( } private const val AUTH_REQUIRED_PREFIX = "auth-required:" + +private const val DUPLICATE_PREFIX = "duplicate:" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 04227c65e7..1eef01d926 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -314,9 +314,9 @@ object Nip19Parser { } /** - * True when the entity starting at [at] is glued to the token before it — an `npub1…` that is a - * URL's subdomain (`https://npub1….blossom.band/…`) or part of a path or word — rather than a - * reference on its own. `nostr:` and `@` prefixes are part of a reference, so they are skipped. + * True when the entity starting at [at] is glued to the word before it (`xnpub1…`, `a.npub1…`) + * rather than standing on its own. `nostr:` and `@` prefixes are part of a reference, so they are + * skipped; a `/` is not glue, so a path link like `njump.me/npub1…` still counts as cited. */ private fun isEmbeddedAt( content: String, @@ -327,9 +327,12 @@ object Nip19Parser { if (j >= 6 && content.regionMatches(j - 6, "nostr:", 0, 6, ignoreCase = true)) j -= 6 if (j == 0) return false val c = content[j - 1] - return c.isLetterOrDigit() || c == '/' || c == '.' || c == '-' || c == '_' || c == '=' || c == '?' || c == '&' || c == '#' + return c.isLetterOrDigit() || c == '.' || c == '-' || c == '_' } + /** The entity is a domain label — `npub1….blossom.band` — so it names a host, not a person. */ + private fun isDomainLabel(additionalChars: String): Boolean = additionalChars.length > 1 && additionalChars[0] == '.' && additionalChars[1].isLetterOrDigit() + private fun allBech32( content: String, from: Int, @@ -371,7 +374,9 @@ object Nip19Parser { fun parseAllStandalone(content: String): List { val returningList = mutableListOf() forEachNip19Match(content, FIXED_58_PREFIXES, VARIABLE_PREFIXES, standaloneOnly = true) { type, key, additionalChars -> - parseComponents(type, key, additionalChars)?.entity?.let { returningList.add(it) } + if (!isDomainLabel(additionalChars)) { + parseComponents(type, key, additionalChars)?.entity?.let { returningList.add(it) } + } } return returningList } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt index 74ffa32777..36d60cf45a 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/media/BuzzMediaSanitizerTest.kt @@ -91,4 +91,23 @@ class BuzzMediaSanitizerTest { assertContentEquals(notJpeg, BuzzMediaSanitizer.sanitize(notJpeg, "image/jpeg")) assertEquals(true, BuzzMediaSanitizer.handles("IMAGE/JPEG")) } + + @Test + fun metadataAfterAScanIsDroppedToo() { + val secondScan = seg(0xda, bytes(1, 1, 0, 0, 63, 0)) + bytes(0x77, 0x01) + val input = bytes(0xff, 0xd8) + jfif + dqt + sosAndData + exif + seg(0xc4, ByteArray(5) { 2 }) + secondScan + eoi + + val clean = BuzzMediaSanitizer.sanitize(input, "image/jpeg") + + assertContentEquals(bytes(0xff, 0xd8) + jfif + dqt + sosAndData + seg(0xc4, ByteArray(5) { 2 }) + secondScan + eoi, clean) + } + + @Test + fun appendedPayloadEndsAtTheRealEndOfImage() { + // A motion photo appends a video (or another JPEG) that can itself contain FF D9. + val appended = bytes(0x00, 0x11, 0xff, 0xd9, 0x22) + val input = bytes(0xff, 0xd8) + jfif + dqt + sosAndData + eoi + appended + + assertContentEquals(bytes(0xff, 0xd8) + jfif + dqt + sosAndData + eoi, BuzzMediaSanitizer.sanitize(input, "image/jpeg")) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt index ee392fa8bd..6985559b3f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/content/FindNostrUrisTest.kt @@ -35,8 +35,15 @@ class FindNostrUrisTest { } @Test - fun npubInAPathIsNotCited() { - assertTrue(findNostrUris("see https://example.com/p/$npub").isEmpty()) + fun npubGluedToAWordIsNotCited() { + assertTrue(findNostrUris("x$npub").isEmpty()) + } + + @Test + fun aPathLinkToAProfileIsStillCited() { + // njump-style links have always been cited; only domain labels are excluded. + assertEquals(1, findNostrUris("see https://njump.me/$npub").filterIsInstance().size) + assertEquals(1, findNostrUris("end of sentence nostr:$npub.").filterIsInstance().size) } @Test From 96a0fe1d3a43ff367b69c72ca05f5a5aaed0a93e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:12:06 +0000 Subject: [PATCH 37/45] fix: now-playing audit fixes (privacy, ordering, churn, cancellation) Bugs: - Desktop MPRIS: browsers put the playing tab's page URL in xesam:url, which went out as the status' `r` tag. Only Spotify track/episode links are shared now; pages and file paths never are. - In-app: only the most recently started player was considered, so an unmuted feed video started after a song hid the song. All audible players are checked, newest first, and the first track wins. - In-app: a position update moved its player to the end of the registry, making it "current" over players started later. - Relays are woken before publishing, not after. - The listener service no longer logs the account in: AppModules already does on every process start, and a second call raced it. - Desktop commands are interruptible and their process is killed on the way out, so turning sharing off or quitting no longer waits on (or leaks) a running PowerShell. Performance: - Players and other apps that report their position every second no longer re-emit downstream (NowPlaying.isSameMoment, 5s tolerance). - In-app items are resolved (NIP-19 parse + cache lookup) once per item, not on every timeline tick of a live stream. - App labels are cached instead of a PackageManager IPC per callback. - The notification listener asks only for ongoing notifications. - MPRIS player names are cached; desktop polls every 30s while nothing plays (15s while playing). - The settings screen checks notification access once per resume. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N2naGACCZy1GvqmZNCwpBu --- amethyst/src/main/AndroidManifest.xml | 5 ++ .../service/nowPlaying/InAppPlayback.kt | 25 ++++++--- .../nowPlaying/NowPlayingStatusCoordinator.kt | 52 ++++++++++++++----- .../nowPlaying/OtherAppsNowPlayingService.kt | 30 ++++++----- .../nowPlaying/NowPlaying.kt | 18 +++++++ .../nowPlaying/NowPlayingResolver.kt | 18 ++++--- .../nowPlaying/NowPlayingPublisherTest.kt | 14 +++++ .../settings/NowPlayingSettingsScreen.kt | 12 ++--- .../DesktopNowPlayingCoordinator.kt | 12 +++-- .../nowPlaying/MprisNowPlayingReader.kt | 19 ++++++- .../desktop/nowPlaying/OsNowPlayingReader.kt | 27 ++++++---- .../nowPlaying/OsNowPlayingReaderTest.kt | 17 ++++++ 12 files changed, 186 insertions(+), 63 deletions(-) diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index d5d91e424e..467e9ac3d9 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -573,6 +573,11 @@ + + > = playing - /** The most recently started playback, if any. */ - fun current(all: Map): InAppPlayback? = all.values.lastOrNull() - fun update( player: Player, playback: InAppPlayback?, ) { val key = System.identityHashCode(player) playing.update { current -> + val previous = current[key] when { - playback == null -> if (key in current) current - key else current - current[key] == playback -> current - // Remove first so a new track moves to the end and becomes the current one. + playback == null -> if (previous != null) current - key else current + // Live streams and some players report timeline/position changes every few + // seconds; keeping the same map avoids re-running everything downstream. + playback.isSameMoment(previous) -> current + // The same item with a corrected end keeps its place; a new item moves to the + // end, so iterating backwards visits the most recently started first. + previous?.callbackUri == playback.callbackUri -> current + (key to playback) else -> (current - key) + (key to playback) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt index 4998f945a2..40485ae3df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/NowPlayingStatusCoordinator.kt @@ -68,22 +68,25 @@ class NowPlayingStatusCoordinator( } private suspend fun run(account: Account) { + // Every player that is audibly playing, most recently started first: the first one that + // is a music track or podcast episode wins, so an unmuted feed video started after a song + // does not hide the song. val inAppTracks = inApp - .map { InAppPlaybackRegistry.current(it) } - .distinctUntilChanged() - .map { playback -> playback?.let(::resolve) } + .map { playing -> playing.values.reversed().firstNotNullOfOrNull(::resolve) } .flowOn(Dispatchers.IO) val publisher = NowPlayingPublisher( + // Relays are woken first: in the background the pool is paused, and the event + // waits in the outbox until a relay connects. publish = { track, expiration -> - account.publishNowPlaying(track, expiration) holdRelays() + account.publishNowPlaying(track, expiration) }, clear = { - account.clearNowPlaying() holdRelays() + account.clearNowPlaying() }, ) @@ -95,14 +98,36 @@ class NowPlayingStatusCoordinator( ).run() } - private fun resolve(playback: InAppPlayback): NowPlaying? = - NowPlayingResolver.fromNostrUri( - uri = playback.callbackUri, - findEvent = { key -> LocalCache.getNoteIfExists(key)?.event }, - fallbackTitle = playback.title, - fallbackArtist = playback.artist, - endsAt = playback.endsAt, - ) + /** + * What a player's item is as a status, without its end time. Keyed by the item's `nostr:` URI + * so the NIP-19 parse and the cache lookup run once per item, not once per position update. + * Only touched from the single flow collecting [inApp]. + */ + private val resolved = HashMap() + + private class Resolution( + val track: NowPlaying?, + ) + + private fun resolve(playback: InAppPlayback): NowPlaying? { + val cached = + resolved[playback.callbackUri] ?: run { + val key = NowPlayingResolver.noteKey(playback.callbackUri) + val event = + if (key != null) { + // Not loaded yet: resolve again on the next update instead of caching a miss. + LocalCache.getNoteIfExists(key)?.event ?: return null + } else { + null + } + val track = event?.let { NowPlayingResolver.fromEvent(it, playback.title, playback.artist) } + + if (resolved.size >= MAX_RESOLVED) resolved.clear() + Resolution(track).also { resolved[playback.callbackUri] = it } + } + + return cached.track?.copy(endsAt = playback.endsAt) + } private fun holdRelays() { relayHold?.cancel() @@ -114,5 +139,6 @@ class NowPlayingStatusCoordinator( companion object { private const val RELAY_HOLD_MS = 30_000L + private const val MAX_RESOLVED = 64 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt index 6a8a69718a..a461d2b516 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/nowPlaying/OtherAppsNowPlayingService.kt @@ -27,6 +27,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.media.MediaMetadata import android.media.session.MediaController +import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.media.session.PlaybackState import android.os.Build @@ -36,7 +37,6 @@ import android.os.SystemClock import android.provider.Settings import android.service.notification.NotificationListenerService import androidx.core.app.NotificationManagerCompat -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource import com.vitorpamplona.quartz.utils.Log @@ -51,6 +51,8 @@ object OtherAppsPlaybackRegistry { val flow: StateFlow = playing fun update(track: NowPlaying?) { + // Many players report their position every second; only a real change goes downstream. + if (track != null && track.isSameMoment(playing.value)) return playing.value = track } } @@ -64,12 +66,17 @@ object OtherAppsPlaybackRegistry { * "Notification access" screen ([openAccessSettings]); until then the system never binds it. * Whether anything is published is still up to the account's now-playing settings. * - * Runs in the main process (it is not declared in `:napplet`), where the account lives. + * Runs in the main process (it is not declared in `:napplet`), where the account lives. The + * system may start that process just to bind this listener; AppModules then logs the saved + * account in on its own, as it does on every start, so the publisher has it. */ class OtherAppsNowPlayingService : NotificationListenerService() { private val handler = Handler(Looper.getMainLooper()) private var sessionManager: MediaSessionManager? = null - private val watched = mutableMapOf>() + private val watched = mutableMapOf>() + + // App labels come from PackageManager, an IPC; players report state up to once a second. + private val labels = mutableMapOf() private val sessionsListener = MediaSessionManager.OnActiveSessionsChangedListener { controllers -> @@ -89,9 +96,6 @@ class OtherAppsNowPlayingService : NotificationListenerService() { // Access was revoked between the bind and this call. Log.w("OtherAppsNowPlaying", "Notification access is not granted", e) } - - // The system can start the process just to bind this listener; the publisher needs the account. - Amethyst.instance.sessionManager.loginWithDefaultAccountIfLoggedOff() } override fun onListenerDisconnected() { @@ -111,14 +115,14 @@ class OtherAppsNowPlayingService : NotificationListenerService() { private fun watch(controllers: List) { val others = controllers.filter { it.packageName != packageName } - val keys = others.map { it.sessionToken.toString() }.toSet() + val keys = others.mapTo(mutableSetOf()) { it.sessionToken } watched.keys.filter { it !in keys }.forEach { key -> watched.remove(key)?.let { (controller, callback) -> controller.unregisterCallback(callback) } } others.forEach { controller -> - val key = controller.sessionToken.toString() + val key = controller.sessionToken if (key !in watched) { val callback = object : MediaController.Callback() { @@ -182,10 +186,12 @@ class OtherAppsNowPlayingService : NotificationListenerService() { } private fun appLabel(packageName: String): String = - try { - packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() - } catch (_: PackageManager.NameNotFoundException) { - packageName + labels.getOrPut(packageName) { + try { + packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, 0)).toString() + } catch (_: PackageManager.NameNotFoundException) { + packageName + } } companion object { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt index ca57ed9045..32287b1968 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlaying.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlin.math.abs /** Where a track is being played. */ @Immutable @@ -72,7 +73,24 @@ data class NowPlaying( addressId == other.addressId && eventId == other.eventId + /** + * Same track from the same source, ending within [toleranceSeconds] of [other]. Sources call + * this to drop the position ticks some players report every second, which would otherwise + * re-emit a "new" value (and re-run everything downstream) for a status that has not changed. + */ + fun isSameMoment( + other: NowPlaying?, + toleranceSeconds: Long = MOMENT_TOLERANCE_SECONDS, + ): Boolean { + if (other == null || source != other.source || !isSameTrack(other)) return false + val a = endsAt + val b = other.endsAt + return if (a == null || b == null) a == b else abs(a - b) <= toleranceSeconds + } + companion object { + const val MOMENT_TOLERANCE_SECONDS = 5L + /** * When a track [durationMs] long, now at [positionMs] and playing at [speed], ends, * measured from [nowSeconds]. Null when the duration is unknown or the track already ended. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt index 494e2bc298..8d203fc781 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingResolver.kt @@ -53,18 +53,20 @@ object NowPlayingResolver { fallbackArtist: String? = null, endsAt: Long? = null, ): NowPlaying? { - val key = - when (val entity = Nip19Parser.uriToRoute(uri)?.entity) { - is NEvent -> entity.hex - is NNote -> entity.hex - is NAddress -> entity.aTag() - else -> null - } ?: return null - + val key = noteKey(uri) ?: return null val event = findEvent(key) ?: return null return fromEvent(event, fallbackTitle, fallbackArtist, endsAt) } + /** The cache key (event id, or `kind:pubkey:d` address) a `nostr:` note URI points at. */ + fun noteKey(uri: String): String? = + when (val entity = Nip19Parser.uriToRoute(uri)?.entity) { + is NEvent -> entity.hex + is NNote -> entity.hex + is NAddress -> entity.aTag() + else -> null + } + fun fromEvent( event: Event, fallbackTitle: String? = null, diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt index 0854204e8e..c4761cb6b1 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/nowPlaying/NowPlayingPublisherTest.kt @@ -29,7 +29,9 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class NowPlayingPublisherTest { @@ -205,6 +207,18 @@ class NowPlayingPublisherTest { assertEquals("Intergalactic", NowPlaying("Intergalactic", null, NowPlayingSource.InApp).statusText()) } + @Test + fun aPositionTickIsTheSameMomentButASeekIsNot() { + val song = NowPlaying("Song", "Band", NowPlayingSource.InApp, endsAt = 1_000) + + assertTrue(song.isSameMoment(song.copy(endsAt = 1_004))) + assertFalse(song.isSameMoment(song.copy(endsAt = 1_060))) + assertFalse(song.isSameMoment(song.copy(title = "Other"))) + assertFalse(song.isSameMoment(song.copy(source = NowPlayingSource.OtherApp("x", "X")))) + assertFalse(song.isSameMoment(null)) + assertTrue(song.copy(endsAt = null).isSameMoment(song.copy(endsAt = null))) + } + @Test fun endsAtAccountsForPositionAndSpeed() { assertEquals(1_000L + 60 + 1, NowPlaying.endsAt(1_000, durationMs = 180_000, positionMs = 120_000)) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt index bd1c6890dc..973e5381aa 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt @@ -163,6 +163,8 @@ fun NowPlayingSettingsContent( ) { val settings by state.flow.collectAsState() val isOn = (showInApp && settings.shareInApp) || settings.shareOtherApps + // One check per resume for both the confirmation row and the alert card. + val accessGranted = access == null || rememberAccessGranted(access) Column( modifier = modifier.fillMaxWidth(), @@ -197,12 +199,12 @@ fun NowPlayingSettingsContent( checked = settings.shareOtherApps, onCheckedChange = state::setShareOtherApps, ) - if (settings.shareOtherApps && access != null) { + if (settings.shareOtherApps && access != null && accessGranted) { AccessGrantedRow(access) } } - if (settings.shareOtherApps && access != null) { + if (settings.shareOtherApps && access != null && !accessGranted) { AccessNeededCard(access) } @@ -375,9 +377,6 @@ private fun StatusChip(chip: PreviewChip) { /** Sub-row under the "Other apps" tile once access is granted: a quiet confirmation + Manage. */ @Composable private fun AccessGrantedRow(access: NowPlayingAccess) { - val granted = rememberAccessGranted(access) - if (!granted) return - Row( modifier = Modifier.fillMaxWidth().padding(start = 68.dp, end = 8.dp, bottom = 6.dp), verticalAlignment = Alignment.CenterVertically, @@ -403,9 +402,6 @@ private fun AccessGrantedRow(access: NowPlayingAccess) { /** The one thing blocking other-app sharing, as a tonal card with a single clear action. */ @Composable private fun AccessNeededCard(access: NowPlayingAccess) { - val granted = rememberAccessGranted(access) - if (granted) return - Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt index 911f74aada..16da363837 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/DesktopNowPlayingCoordinator.kt @@ -37,7 +37,8 @@ import kotlinx.coroutines.launch /** * Desktop: shares what another app on this computer is playing as the account's NIP-38 music - * status. The OS is polled every [pollMs] only while the account has + * status. The OS is polled every [pollMs] while something plays ([idlePollMs] while nothing + * does), and only while the account has * [com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSettings.shareOtherApps] * on, so nothing is spawned (and macOS never asks for Automation access) until the user opts in. * @@ -49,6 +50,7 @@ class DesktopNowPlayingCoordinator( private val publishEvent: (Event) -> Unit, private val reader: OsNowPlayingReader? = OsNowPlayingReader.forThisOs(), private val pollMs: Long = 15_000, + private val idlePollMs: Long = 30_000, ) { /** Runs until the calling coroutine (the logged-in account's composition) is cancelled. */ suspend fun run() { @@ -67,8 +69,12 @@ class DesktopNowPlayingCoordinator( return@collectLatest } while (true) { - otherApps.value = reader.read() - delay(pollMs) + val track = reader.read() + // Keep the last value when only the position moved, so nothing downstream re-runs. + if (track == null || !track.isSameMoment(otherApps.value)) otherApps.value = track + // Each poll spawns processes (PowerShell on Windows is the heavy one), so + // look less often while nothing plays; a new song shows up within 30s. + delay(if (track == null) idlePollMs else pollMs) } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt index d9b4910150..9290de3642 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/MprisNowPlayingReader.kt @@ -33,14 +33,21 @@ class MprisNowPlayingReader( private val run: suspend (List) -> String? = { runCommand(it) }, private val nowSeconds: () -> Long = TimeUtils::now, ) : OsNowPlayingReader { + // A player's name never changes while its bus name lives; asking once saves a process per poll. + private val identities = mutableMapOf() + override suspend fun read(): NowPlaying? { val names = run(LIST_NAMES)?.let(MprisParser::playerNames).orEmpty() + identities.keys.retainAll(names.toSet()) for (busName in names) { val properties = run(getAll(busName))?.let(MprisParser::properties) ?: continue if (properties["PlaybackStatus"]?.firstOrNull() != "Playing") continue - val identity = run(getIdentity(busName))?.let(MprisParser::properties)?.get("Identity")?.firstOrNull() + val identity = + identities.getOrPut(busName) { + run(getIdentity(busName))?.let(MprisParser::properties)?.get("Identity")?.firstOrNull() + } return MprisParser.toNowPlaying(busName, identity, properties, nowSeconds()) } @@ -170,10 +177,18 @@ object MprisParser { artist = artist, source = NowPlayingSource.OtherApp(id, label), endsAt = NowPlaying.endsAt(nowSeconds, durationMs, positionMs, rate), - url = properties["xesam:url"]?.firstOrNull()?.takeIf { it.startsWith("https://") }, + url = properties["xesam:url"]?.firstOrNull()?.let(::shareableUrl), ) } + /** + * Only links that name a track are shared. Browsers fill `xesam:url` with the page of whatever + * tab is playing, and a local player with a file path: neither belongs in a public status. + */ + fun shareableUrl(url: String): String? = url.takeIf { SHAREABLE_URL.matches(it) } + + private val SHAREABLE_URL = Regex("""^https://open\.spotify\.com/(intl-[a-z-]+/)?(track|episode)/[A-Za-z0-9]+(\?.*)?$""") + /** `string "x"`, `object path "/x"`, `int64 5`, `double 1`, `boolean true` -> the value. */ private fun scalar(value: String): String? = when { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt index a3f131c7d5..7a8cb58c6d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.desktop.nowPlaying import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlaying import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.runInterruptible import java.util.concurrent.TimeUnit /** Reads what the OS says another app is playing right now. */ @@ -45,31 +45,38 @@ interface OsNowPlayingReader { } } -/** Runs a short-lived command and returns its stdout, or null if it failed or took too long. */ +/** + * Runs a short-lived command and returns its stdout, or null if it failed or took too long. + * Interruptible, so turning sharing off or closing the app does not wait out a slow command, and + * the process never outlives the call. + */ internal suspend fun runCommand( command: List, timeoutSeconds: Long = 5, ): String? = - withContext(Dispatchers.IO) { + runInterruptible(Dispatchers.IO) { + var process: Process? = null try { - val process = + val started = ProcessBuilder(command) .redirectError(ProcessBuilder.Redirect.DISCARD) .start() - process.outputStream.close() + process = started + started.outputStream.close() // Wait before reading so a hung command cannot block the read forever. The outputs // here are a few KB, well under the pipe buffer, so the command never blocks on it. - if (!process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { - process.destroyForcibly() - null - } else if (process.exitValue() != 0) { + if (!started.waitFor(timeoutSeconds, TimeUnit.SECONDS) || started.exitValue() != 0) { null } else { - process.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + started.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } } + } catch (e: InterruptedException) { + throw e } catch (e: Exception) { // Missing binary (no dbus-send, no osascript) or a denied permission: nothing to report. Log.d("OsNowPlayingReader") { "${command.first()} failed: ${e.message}" } null + } finally { + process?.takeIf { it.isAlive }?.destroyForcibly() } } diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt index 4fc575d62a..02e0bc97c2 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt @@ -161,6 +161,23 @@ class OsNowPlayingReaderTest { assertNull(reader.read()) } + @Test + fun mprisSharesTrackLinksButNeverPagesOrFiles() { + assertEquals( + "https://open.spotify.com/track/0DiWol3AO6WpXZgp0goxAV", + MprisParser.shareableUrl("https://open.spotify.com/track/0DiWol3AO6WpXZgp0goxAV"), + ) + assertEquals( + "https://open.spotify.com/intl-pt/episode/4rOoJ6Egrf8K2IrywzwOMk?si=abc", + MprisParser.shareableUrl("https://open.spotify.com/intl-pt/episode/4rOoJ6Egrf8K2IrywzwOMk?si=abc"), + ) + // Chromium reports the playing tab's page; VLC the file it opened. + assertNull(MprisParser.shareableUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")) + assertNull(MprisParser.shareableUrl("https://mail.example.com/inbox/secret-thread")) + assertNull(MprisParser.shareableUrl("file:///home/me/Music/song.mp3")) + assertNull(MprisParser.shareableUrl("https://open.spotify.com.evil.example/track/abc")) + } + @Test fun mprisPlayerIdDropsTheInstanceSuffix() { assertEquals("vlc", MprisParser.playerId("org.mpris.MediaPlayer2.vlc.instance4242")) From 322726ab517ee3722f2143f096d37c3815de44ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 13:44:27 +0000 Subject: [PATCH 38/45] feat: compact link preview from , meta description and favicon Pages without OpenGraph (e.g. most nsites) never got a preview card: the fetch was only kept when it found an image, so they fell back to a bare link even when they had a perfectly good title and description. - MetaTagsParser can now also yield the document <title> and icon <link>s (opt-in via includeTitleAndIcons, used by HtmlParser). - OpenGraphParser falls back to <title> when no og/twitter/meta title exists, and picks the best icon (scalable > largest declared size; apple-touch-icon defaults to 180px; mask-icon ignored). - UrlInfoItem keeps a page with a title or description, and resolves its icon (falling back to /favicon.ico at the origin). - UrlPreviewCard renders a short, wide card (icon left; title, description and host right) when there is no image, with a globe placeholder until the icon loads. The composer's thumbnail falls back to the icon too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdPp7UiETpzv8jQ5qfAFqp --- .../ui/note/creators/previews/PreviewUrl.kt | 10 +- .../amethyst/commons/preview/HtmlParser.kt | 2 +- .../commons/preview/MetaTagsParser.kt | 123 +++++++++++- .../commons/preview/OpenGraphParser.kt | 67 ++++++- .../amethyst/commons/preview/UrlInfoItem.kt | 26 ++- .../preview/TitleAndIconFallbackTest.kt | 179 +++++++++++++++++ .../amethyst/commons/preview/UrlPreview.kt | 1 + .../preview/UrlInfoItemTextPreviewTest.kt | 87 ++++++++ .../commons/ui/components/UrlPreviewCard.kt | 187 +++++++++++++++--- 9 files changed, 637 insertions(+), 45 deletions(-) create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index 72aa4b9509..1c50e33e81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -239,11 +241,13 @@ private fun MyLoadUrlPreviewDirect( ) } else { Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.aspectRatio(1f)) { + // A page with no OpenGraph image still has its icon: shown whole, not cropped. + val hasImage = state.previewInfo.imageUrlFullPath.isNotBlank() AsyncImage( - model = state.previewInfo.imageUrlFullPath, + model = if (hasImage) state.previewInfo.imageUrlFullPath else state.previewInfo.iconUrlFullPath, contentDescription = state.previewInfo.title, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), + contentScale = if (hasImage) ContentScale.Crop else ContentScale.Fit, + modifier = if (hasImage) Modifier.fillMaxSize() else Modifier.fillMaxSize().padding(20.dp), ) Text( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlParser.kt index 823862bf31..27571bebae 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/HtmlParser.kt @@ -49,7 +49,7 @@ class HtmlParser { ?: bodyBytes.bomCharsetName() ?: HtmlCharsetParser.detectCharset(bodyBytes) val content = decodeBytes(bodyBytes, name) - MetaTagsParser.parse(content) + MetaTagsParser.parse(content, includeTitleAndIcons = true) } private fun ByteArray.bomCharsetName(): String? { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt index 62e46ced82..6fefdf4dc2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt @@ -22,8 +22,21 @@ package com.vitorpamplona.amethyst.commons.preview import com.vitorpamplona.amethyst.commons.util.codePointToChars +/** The `<head>` element a [MetaTag] was read from. */ +enum class HeadElement { + /** A `<meta>` tag, the only kind [MetaTagsParser.parse] yields unless asked for more. */ + META, + + /** The document's `<title>`, its text carried as the `content` attribute. */ + TITLE, + + /** A `<link>` whose `rel` names an icon (`icon`, `shortcut icon`, `apple-touch-icon`, …). */ + LINK, +} + data class MetaTag( private val attrs: Map<String, String>, + val element: HeadElement = HeadElement.META, ) { /** * Returns a value of an attribute specified by its name (case insensitive), or empty string if it doesn't exist. @@ -43,7 +56,16 @@ object MetaTagsParser { private const val NO_QUOTE = ' ' private const val META = "meta" + private const val LINK = "link" private const val HEAD = "head" + private const val ICON = "icon" + private const val REL = "rel" + private const val CONTENT = "content" + + // A `<title>` is only a fallback label, so one that runs on (an unclosed tag swallowing the + // page, a spam keyword dump) is cut rather than carried into the preview cache whole. + private const val MAX_TITLE_LENGTH = 300 + private val WHITESPACE_RUN = Regex("\\s+") // Elements whose content is text rather than markup: script and style hold raw text, title and // textarea hold character data. A `<` inside any of them is not a tag. @@ -66,26 +88,81 @@ object MetaTagsParser { /** The `</head>` that ends the interesting part of the document. */ HEAD_END, + /** A closed `<title>`: its text is [TagScanner.textStart]..<[TagScanner.textEnd]. */ + TITLE, + + /** A `<link …>` whose attribute span mentions `icon`; same span fields as [META]. */ + LINK, + /** Anything else: other elements, comments, declarations, unparseable markup. */ OTHER, } /** * Lazily parse a partial HTML document and extract meta tags. + * + * With [includeTitleAndIcons], the document's `<title>` and its icon `<link>`s are yielded + * too, tagged by [MetaTag.element]. They are what a page without OpenGraph still offers for a + * preview: browsers show exactly these two in a tab. */ - fun parse(input: String): Sequence<MetaTag> = + fun parse( + input: String, + includeTitleAndIcons: Boolean = false, + ): Sequence<MetaTag> = sequence { val s = TagScanner(input) while (!s.exhausted()) { - val kind = s.nextTag() - if (kind == TagKind.HEAD_END) break - if (kind == TagKind.META) { - val attrs = parseAttrs(input, s.attrsStart, s.attrsEnd) ?: continue - yield(MetaTag(attrs)) + when (s.nextTag()) { + TagKind.HEAD_END -> { + break + } + + TagKind.META -> { + val attrs = parseAttrs(input, s.attrsStart, s.attrsEnd) ?: continue + yield(MetaTag(attrs)) + } + + TagKind.TITLE -> { + if (includeTitleAndIcons) { + val text = titleText(input, s.textStart, s.textEnd) + if (text.isNotEmpty()) yield(MetaTag(mapOf(CONTENT to text), HeadElement.TITLE)) + } + } + + TagKind.LINK -> { + if (includeTitleAndIcons) { + val attrs = parseAttrs(input, s.attrsStart, s.attrsEnd) ?: continue + if (isIconRel(attrs[REL])) yield(MetaTag(attrs, HeadElement.LINK)) + } + } + + TagKind.OTHER -> {} } } } + /** + * Whether a `rel` names an icon: one of its space-separated tokens is `icon` or an + * `apple-touch-icon` variant. `mask-icon` is deliberately not one -- it is Safari's monochrome + * pinned-tab silhouette, which renders as a black blob anywhere else. + */ + private fun isIconRel(rel: String?): Boolean = + rel != null && + rel.split(' ', '\t', '\n', '\r', '\u000C').any { + it.equals(ICON, ignoreCase = true) || it.startsWith("apple-touch-icon", ignoreCase = true) + } + + /** The `<title>`'s character data, with references resolved and whitespace collapsed. */ + private fun titleText( + input: String, + from: Int, + to: Int, + ): String { + val raw = input.substring(from, to) + val decoded = if (raw.indexOf('&') < 0) raw else raw.replace(Attrs.RE_CHAR_REF, Attrs.Companion::replaceCharRefs) + return decoded.replace(WHITESPACE_RUN, " ").trim().take(MAX_TITLE_LENGTH) + } + private class TagScanner( private val input: String, ) { @@ -98,6 +175,12 @@ object MetaTagsParser { var attrsEnd = 0 private set + /** Character-data span of the `<title>` [nextTag] last reported as [TagKind.TITLE]. */ + var textStart = 0 + private set + var textEnd = 0 + private set + fun exhausted(): Boolean = p >= length /** @@ -128,6 +211,20 @@ object MetaTagsParser { p = if (end < 0) length else end + 1 } + /** True when `input[from..<to]` contains [lower], ASCII-case-insensitively. */ + private fun spanContains( + from: Int, + to: Int, + lower: String, + ): Boolean { + var i = from + while (i + lower.length <= to) { + if (input.regionMatches(i, lower, 0, lower.length, ignoreCase = true)) return true + i++ + } + return false + } + /** Leaves [p] on the `</name` that closes a raw-text element, or at the end of the input. */ private fun skipRawText(endTag: String) { var i = p @@ -215,14 +312,24 @@ object MetaTagsParser { // the same way an unbalanced quote inside a comment does. Switching on the name length // first keeps the common tag (a `<div>`, a `<link>`) down to one comparison. when (nameEnd - nameStart) { - META.length -> if (nameIs(nameStart, nameEnd, META)) return TagKind.META + META.length -> { + if (nameIs(nameStart, nameEnd, META)) return TagKind.META + // Most `<link>`s are stylesheets and preloads; checking the raw span for `icon` + // keeps their attributes from ever being parsed into a map. + if (nameIs(nameStart, nameEnd, LINK) && spanContains(attrsStart, attrsEnd, ICON)) return TagKind.LINK + } - STYLE.length -> + STYLE.length -> { if (nameIs(nameStart, nameEnd, STYLE)) { skipRawText(STYLE_END) } else if (nameIs(nameStart, nameEnd, TITLE)) { + textStart = p skipRawText(TITLE_END) + textEnd = p + // An unclosed title runs to the end of the input: that is not a title. + if (p < length) return TagKind.TITLE } + } SCRIPT.length -> if (nameIs(nameStart, nameEnd, SCRIPT)) skipRawText(SCRIPT_END) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt index 9e4714756b..3c0dfe3667 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/OpenGraphParser.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.preview class OpenGraphParser { class Result( + /** The OpenGraph/Twitter/meta title, or the document's `<title>` when it declares none. */ val title: String, val description: String, val image: String, @@ -35,6 +36,8 @@ class OpenGraphParser { val videoType: String = "", /** `og:type` — what the page says it *is*, e.g. `music.song`, `video.other`, `article`. */ val type: String = "", + /** The best icon `<link>` the page declares, verbatim (may be relative). Empty when none. */ + val icon: String = "", ) companion object { @@ -112,6 +115,17 @@ class OpenGraphParser { ) private val CONTENT = "content" + private val HREF = "href" + private val REL = "rel" + private val SIZES = "sizes" + private val TYPE = "type" + + // How an icon with no `sizes` is ranked. An `apple-touch-icon` is 180px by Apple's + // convention; a plain `icon` without sizes is usually the 16/32px tab favicon. A scalable + // one (SVG, or `sizes="any"`) beats every bitmap: it is sharp at any card size. + private const val APPLE_TOUCH_ICON_DEFAULT_SIZE = 180 + private const val ICON_DEFAULT_SIZE = 32 + private const val SCALABLE_ICON_SIZE = Int.MAX_VALUE } /** Which field of [Result] a meta tag's key fills, or null when the key is not one we read. */ @@ -156,8 +170,33 @@ class OpenGraphParser { var video = "" var videoType = "" var type = "" + var documentTitle = "" + var icon = "" + var iconSize = -1 metaTags.forEach { + when (it.element) { + HeadElement.META -> {} + + HeadElement.TITLE -> { + if (documentTitle.isEmpty()) documentTitle = it.attr(CONTENT) + return@forEach + } + + HeadElement.LINK -> { + val href = it.attr(HREF) + if (href.isNotBlank()) { + val size = iconSize(it) + // Strictly greater, so among equals the first declared wins. + if (size > iconSize) { + icon = href + iconSize = size + } + } + return@forEach + } + } + // A meta tag names its key in exactly one of these three attributes, but which one // varies by site, so each is tried in turn until one is a key we read. val field = @@ -179,6 +218,32 @@ class OpenGraphParser { null -> Unit } } - return Result(title, description, image, audio, audioType, video, videoType, type) + return Result(title.ifEmpty { documentTitle }, description, image, audio, audioType, video, videoType, type, icon) + } + + /** The pixel size an icon `<link>` is ranked by: its largest declared size, or a default. */ + private fun iconSize(link: MetaTag): Int { + val sizes = link.attr(SIZES) + if (sizes.contains("any", ignoreCase = true)) return SCALABLE_ICON_SIZE + if (link.attr(TYPE).equals("image/svg+xml", ignoreCase = true)) return SCALABLE_ICON_SIZE + if (link + .attr(HREF) + .substringBefore('?') + .substringBefore('#') + .endsWith(".svg", ignoreCase = true) + ) { + return SCALABLE_ICON_SIZE + } + + // `sizes="16x16 32x32"`: rank by the largest, which is the one a decoder will pick. + val declared = + sizes + .split(' ') + .mapNotNull { it.lowercase().substringBefore('x', "").toIntOrNull() } + .maxOrNull() + if (declared != null) return declared + + val isAppleTouch = link.attr(REL).contains("apple-touch-icon", ignoreCase = true) + return if (isAppleTouch) APPLE_TOUCH_ICON_DEFAULT_SIZE else ICON_DEFAULT_SIZE } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt index 5cb955845d..00b24466cf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt @@ -43,6 +43,8 @@ class UrlInfoItem( val videoType: String = "", /** `og:type` — what the page says it is, e.g. `music.song`, `video.other`, `article`. */ val type: String = "", + /** The page's best declared icon `<link>` (favicon / apple-touch-icon). Empty when none. */ + val icon: String = "", ) { /** The page's host, or null when [url] is not an absolute URL. */ val verifiedHost = absoluteUrlHost(url) @@ -66,6 +68,14 @@ class UrlInfoItem( // which is what it did before. Only the playable gate below treats unresolvable as refusal. val imageUrlFullPath = resolve(image) ?: image + /** + * The page's icon, absolute: the one it declared, else `/favicon.ico` at its origin -- the + * same guess every browser makes, since most sites that declare nothing still serve one. + * Null for a non-HTML URL (an image or a video is its own preview) or an unresolvable one. + */ + val iconUrlFullPath: String? = + if (mimeType.startsWith("text/html")) resolve(icon) ?: resolve(DEFAULT_FAVICON) else null + /** * The declared `og:video`, absolute, but only when the declaration holds up: a [videoType] the * renderer can actually play (a `video/` MIME, an `audio/` one, or an HLS playlist type), or @@ -119,13 +129,25 @@ class UrlInfoItem( else -> audioType.ifEmpty { null } } + /** + * Whether the page has text to show without an image: a title (OpenGraph's or the document's + * own `<title>`) or a description. Such a page gets the compact icon + text card rather than + * a bare link. + */ + val hasTextPreview: Boolean = title.isNotBlank() || description.isNotBlank() + /** * Whether the fetch produced something worth rendering. An image is the usual evidence, but a * page that declared playable media counts too — a track page that ships no cover art would * otherwise be thrown away as Empty and fall back to a bare link, which is exactly the player - * we went to the trouble of finding. + * we went to the trouble of finding. So does a page with no OpenGraph at all that still has a + * title or description: it renders as the compact card instead of a bare link. */ - fun fetchComplete(): Boolean = url.isNotEmpty() && (image.isNotEmpty() || playableMediaUrl != null) + fun fetchComplete(): Boolean = url.isNotEmpty() && (image.isNotEmpty() || playableMediaUrl != null || hasTextPreview) fun allFetchComplete(): Boolean = title.isNotEmpty() && description.isNotEmpty() && image.isNotEmpty() + + companion object { + private const val DEFAULT_FAVICON = "/favicon.ico" + } } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt new file mode 100644 index 0000000000..d0f9db83cc --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt @@ -0,0 +1,179 @@ +/* + * 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.commons.preview + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * A page with no OpenGraph still has what a browser tab shows: its `<title>` and its icon + * `<link>`s. These are read only when asked for, and only ever as a fallback. + */ +class TitleAndIconFallbackTest { + private fun headTags(html: String) = MetaTagsParser.parse(html, includeTitleAndIcons = true).toList() + + private fun extract(html: String) = OpenGraphParser().extractUrlInfo(MetaTagsParser.parse(html, includeTitleAndIcons = true)) + + // Trimmed from the nsite that prompted this: no og:*, no twitter:*, only a title, a meta + // description and an SVG favicon. + private val noOpenGraphPage = + """ + |<!DOCTYPE html> + |<html lang="en"> + |<head> + |<meta charset="utf-8"> + |<meta name="viewport" content="width=device-width, initial-scale=1"> + |<title>Private Provider — Confidential AI + | + | + | + | + |

Hi

+ | + """.trimMargin() + + @Test + fun aPageWithoutOpenGraphStillYieldsTitleDescriptionAndIcon() { + val info = extract(noOpenGraphPage) + + assertEquals("Private Provider — Confidential AI", info.title) + assertEquals("A phone-first AI workspace for Android.", info.description) + assertEquals("favicon.svg", info.icon) + assertEquals("", info.image) + } + + @Test + fun theDefaultParseStillYieldsOnlyMetaTags() { + val tags = MetaTagsParser.parse(noOpenGraphPage).toList() + + assertEquals(3, tags.size) + assertTrue(tags.all { it.element == HeadElement.META }) + } + + @Test + fun onlyIconLinksAreYielded() { + val tags = headTags(noOpenGraphPage) + + val links = tags.filter { it.element == HeadElement.LINK } + assertEquals(1, links.size) + assertEquals("favicon.svg", links[0].attr("href")) + } + + @Test + fun openGraphTitleWinsOverTheDocumentTitle() { + val info = + extract( + """ + | + | Document Title + | + | + """.trimMargin(), + ) + + assertEquals("OG Title", info.title) + } + + @Test + fun titleWhitespaceIsCollapsed() { + val info = extract("\n Spread\n\t Out ") + + assertEquals("Spread Out", info.title) + } + + @Test + fun anUnclosedTitleIsNotATitle() { + val info = extract("runs to the end of the truncated body") + + assertEquals("", info.title) + } + + @Test + fun aTitleOutsideTheHeadIsIgnored() { + // An SVG in the body carries its own <title>; the scan stops at </head> before it. + val info = extract("<head></head><body><svg><title>Close") + + assertEquals("", info.title) + } + + @Test + fun theFirstTitleWins() { + val info = extract("FirstSecond") + + assertEquals("First", info.title) + } + + @Test + fun theLargestIconWins() { + val info = + extract( + """ + | + | + | + | + | + | + """.trimMargin(), + ) + + assertEquals("/apple-touch-icon.png", info.icon) + } + + @Test + fun aScalableIconBeatsBitmaps() { + val info = + extract( + """ + | + | + | + | + """.trimMargin(), + ) + + assertEquals("/icon.svg?v=2", info.icon) + } + + @Test + fun maskIconIsNotUsed() { + // Safari's monochrome pinned-tab silhouette renders as a black blob anywhere else. + val info = + extract( + """ + | + | + | + | + """.trimMargin(), + ) + + assertEquals("/favicon-32.png", info.icon) + } + + @Test + fun iconLinksAfterTheHeadAreIgnored() { + val info = extract("""""") + + assertEquals("", info.icon) + } +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlPreview.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlPreview.kt index 78a918541f..3410eec81c 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlPreview.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlPreview.kt @@ -77,6 +77,7 @@ class UrlPreview { data.video, data.videoType, data.type, + data.icon, ) } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt new file mode 100644 index 0000000000..eaeeeb96a4 --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt @@ -0,0 +1,87 @@ +/* + * 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.commons.preview + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * A page with no OpenGraph image is still worth a card when it has text: the compact icon + text + * card, built from its title, description and favicon. + */ +class UrlInfoItemTextPreviewTest { + private fun page( + url: String = "https://example.nsite.lol/", + title: String = "", + description: String = "", + image: String = "", + icon: String = "", + mimeType: String = "text/html; charset=utf-8", + ) = UrlInfoItem(url = url, title = title, description = description, image = image, mimeType = mimeType, icon = icon) + + @Test + fun aTitleAloneIsEnoughToKeepThePreview() { + assertTrue(page(title = "Private Provider").fetchComplete()) + } + + @Test + fun aDescriptionAloneIsEnoughToKeepThePreview() { + assertTrue(page(description = "A workspace").fetchComplete()) + } + + @Test + fun aPageWithNothingStaysEmpty() { + assertFalse(page().fetchComplete()) + assertFalse(page(title = " ").fetchComplete()) + } + + @Test + fun aRelativeIconIsResolvedAgainstThePage() { + assertEquals( + "https://example.nsite.lol/docs/favicon.svg", + page(url = "https://example.nsite.lol/docs/index.html", icon = "favicon.svg").iconUrlFullPath, + ) + } + + @Test + fun noDeclaredIconFallsBackToTheOriginFavicon() { + assertEquals( + "https://example.nsite.lol/favicon.ico", + page(url = "https://example.nsite.lol/a/b/c.html").iconUrlFullPath, + ) + } + + @Test + fun aNonHttpIconFallsBackToTheOriginFavicon() { + assertEquals( + "https://example.nsite.lol/favicon.ico", + page(icon = "file:///etc/passwd").iconUrlFullPath, + ) + } + + @Test + fun nonHtmlUrlsHaveNoIcon() { + assertNull(page(url = "https://example.com/a.png", image = "https://example.com/a.png", mimeType = "image/png").iconUrlFullPath) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt index a084cb0edf..59265eb13d 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt @@ -21,24 +21,33 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -50,9 +59,12 @@ import com.vitorpamplona.amethyst.commons.resources.link_actions_dialog_title import com.vitorpamplona.amethyst.commons.resources.url_preview_open_in_browser import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size14Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.commons.ui.theme.previewCardImageModifier import kotlinx.coroutines.launch @@ -101,22 +113,42 @@ fun UrlPreviewCard( } } - Column( - modifier = - MaterialTheme.colorScheme.innerPostModifier - .combinedClickable( - onClick = { - if (onCardClick != null) { - onCardClick() - } else { - runCatching { uri.openUri(url) } - } - }, - onLongClick = { - popupExpanded.value = true - }, - ), - ) { + val cardModifier = + MaterialTheme.colorScheme.innerPostModifier + .combinedClickable( + onClick = { + if (onCardClick != null) { + onCardClick() + } else { + runCatching { uri.openUri(url) } + } + }, + onLongClick = { + popupExpanded.value = true + }, + ) + + // Only meaningful when the card's own tap does something else (e.g. opening the comment + // thread); otherwise it would duplicate the card's open-in-browser tap. + val onOpenInBrowser: (() -> Unit)? = onCardClick?.let { { runCatching { uri.openUri(url) } } } + + // A page with no picture to lead with -- most often one with no OpenGraph at all, previewed + // from its ``, meta description and favicon -- gets the short, wide card. Painting the + // big layout without its image would leave a text block that only looks broken. + if (previewInfo.imageUrlFullPath.isBlank() && previewInfo.hasTextPreview) { + CompactUrlPreviewCard(previewInfo, cardModifier, onOpenInBrowser) + } else { + LargeUrlPreviewCard(previewInfo, cardModifier, onOpenInBrowser) + } +} + +@Composable +private fun LargeUrlPreviewCard( + previewInfo: UrlInfoItem, + modifier: Modifier, + onOpenInBrowser: (() -> Unit)?, +) { + Column(modifier = modifier) { // A Loaded preview no longer implies an image: a player page that ships no cover art is // kept (it has media to play), and painting its empty string left a blank 180dp box. if (previewInfo.imageUrlFullPath.isNotBlank()) { @@ -141,20 +173,7 @@ fun UrlPreviewCard( overflow = TextOverflow.Ellipsis, ) - // Only meaningful when the card's own tap does something else (e.g. opening the - // comment thread); otherwise it would duplicate the card's open-in-browser tap. - if (onCardClick != null) { - IconButton( - onClick = { runCatching { uri.openUri(url) } }, - ) { - Icon( - symbol = MaterialSymbols.AutoMirrored.OpenInNew, - contentDescription = stringRes(Res.string.url_preview_open_in_browser), - modifier = Size14Modifier, - tint = Color.Gray, - ) - } - } + onOpenInBrowser?.let { OpenInBrowserButton(it) } } Text( @@ -177,3 +196,111 @@ fun UrlPreviewCard( Spacer(modifier = DoubleVertSpacer) } } + +/** + * The short, wide card: the site's icon on the left, its title, description and host on the + * right. Built from what every page has even without OpenGraph -- what a browser tab shows. + */ +@Composable +private fun CompactUrlPreviewCard( + previewInfo: UrlInfoItem, + modifier: Modifier, + onOpenInBrowser: (() -> Unit)?, +) { + val host = previewInfo.verifiedHost ?: previewInfo.url + + Row( + modifier = modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + UrlPreviewIcon(previewInfo.iconUrlFullPath, host) + + Spacer(modifier = DoubleHorzSpacer) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = previewInfo.title.ifBlank { host }, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + if (previewInfo.description.isNotBlank()) { + Text( + text = previewInfo.description, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + // When there is no title the host already stands in for it on the first line. + if (previewInfo.title.isNotBlank()) { + Text( + text = host, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + onOpenInBrowser?.let { OpenInBrowserButton(it) } + } +} + +/** + * The site's icon in a fixed square. A generic globe sits there until the icon actually loads, + * so a site whose favicon is missing (the `/favicon.ico` guess is only a guess) or undecodable + * still gets a tidy card instead of an empty hole. + */ +@Composable +private fun UrlPreviewIcon( + iconUrl: String?, + contentDescription: String, +) { + Box( + modifier = + UrlPreviewIconModifier + .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)), + contentAlignment = Alignment.Center, + ) { + var loaded by remember(iconUrl) { mutableStateOf(false) } + + if (!loaded) { + Icon( + symbol = MaterialSymbols.Language, + contentDescription = null, + modifier = Size24Modifier, + tint = Color.Gray, + ) + } + + if (iconUrl != null) { + AsyncImage( + model = iconUrl, + contentDescription = contentDescription, + contentScale = ContentScale.Fit, + modifier = UrlPreviewIconImageModifier, + onSuccess = { loaded = true }, + ) + } + } +} + +@Composable +private fun OpenInBrowserButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon( + symbol = MaterialSymbols.AutoMirrored.OpenInNew, + contentDescription = stringRes(Res.string.url_preview_open_in_browser), + modifier = Size14Modifier, + tint = Color.Gray, + ) + } +} + +private val UrlPreviewIconModifier = Modifier.size(48.dp).clip(SmallBorder) +private val UrlPreviewIconImageModifier = Modifier.fillMaxSize().padding(6.dp) From bc1f970dd989df110f0a465eed8ef2795254dc53 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 1 Oct 2026 14:29:10 +0000 Subject: [PATCH 39/45] fix: stop clipping the follow mark off the Now Playing preview avatar The preview card wrapped the account's avatar in a circular clip. The avatar already rounds its own picture; the follow mark sits in the top-right corner of its square box (and user cards along the bottom), so the extra clip cut them off. The slot is now only sized. Adds a render test that draws a marker in the avatar's top-right corner: with the old clip only 207 of its 576 pixels reached the screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N2naGACCZy1GvqmZNCwpBu --- .../settings/NowPlayingSettingsScreen.kt | 4 ++- .../settings/NowPlayingSettingsRenderTest.kt | 36 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt index 973e5381aa..8dc824086f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsScreen.kt @@ -297,7 +297,9 @@ fun NowPlayingPreviewCard( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Box(Modifier.size(44.dp).clip(CircleShape)) { avatar() } + // Sized, not clipped: the avatar rounds itself, and the follow mark and user cards it + // draws sit in the corners of its square box, which a circular clip would cut off. + Box(Modifier.size(44.dp)) { avatar() } Column( modifier = Modifier.weight(1f).padding(start = 14.dp, end = 12.dp), diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt index 69ee28bc70..9c5b1c853a 100644 --- a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NowPlayingSettingsRenderTest.kt @@ -21,14 +21,19 @@ package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.ImageComposeScene import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingAccess @@ -125,7 +130,7 @@ class NowPlayingSettingsRenderTest { return try { val png = scene.render().encodeToData(EncodedImageFormat.PNG)!!.bytes System.getenv("NOW_PLAYING_RENDER_DIR")?.let { dir -> - val name = scenarios.entries.first { it.value === content }.key + val name = scenarios.entries.firstOrNull { it.value === content }?.key ?: return@let File(dir, "now-playing-$name-${if (dark) "dark" else "light"}.png").writeBytes(png) } ImageIO.read(ByteArrayInputStream(png)) @@ -142,6 +147,35 @@ class NowPlayingSettingsRenderTest { return seen.size } + @Test + fun theAvatarCornersAreNotClipped() { + // ClickableUserPicture draws the follow mark in the top-right corner of its square box, + // outside the round picture. A circular clip around the slot used to cut it off. + val marker = Color(0xFFFF00FF) + val image = + render(false) { + NowPlayingPreviewCard( + avatar = { + Box(Modifier.fillMaxSize()) { + Box(Modifier.size(12.dp).align(Alignment.TopEnd).background(marker)) + } + }, + name = "Vitor", + liveStatus = null, + isOn = true, + ) + } + + var markerPixels = 0 + for (x in 0 until width) { + for (y in 0 until height) { + if (image.getRGB(x, y) == marker.toArgb()) markerPixels++ + } + } + // 12dp at density 2 is a 24x24 square: all of it must reach the screen. + assertTrue(markerPixels >= 24 * 24 * 95 / 100, "only $markerPixels of ${24 * 24} corner pixels were drawn") + } + @Test fun everyStateRendersInBothThemes() { scenarios.values.forEach { content -> From da1b4d70abc1f13ed3feddd2ef9ef11b94589e90 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 1 Oct 2026 14:33:40 +0000 Subject: [PATCH 40/45] test: point the search-parity oracle at wss://search.brainstorm.world Switch fetch_fixtures.py's default relay and the parity tests' docs from search-staging to the production search relay, and re-record tools/search-parity/fixture.json from it. The new corpus keeps the same shape: 8 cases x 8 events, every event satisfies its NIP-01 filter, and 8 of 64 carry no literal occurrence of their search term, so the KDoc's numbers still hold; its quoted examples are swapped for ones in the new recording. Also fixes the script's usage line, which pointed at a stale output path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvR9ox7m91GLFUARuqj8o5 --- .../commons/search/SearchParityFixture.kt | 2 +- .../commons/search/SearchRelayParityTest.kt | 6 +- tools/search-parity/fetch_fixtures.py | 10 +- tools/search-parity/fixture.json | 1680 +++++++++-------- 4 files changed, 890 insertions(+), 808 deletions(-) diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchParityFixture.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchParityFixture.kt index 1d74d5811d..70b09d6eef 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchParityFixture.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchParityFixture.kt @@ -30,7 +30,7 @@ import kotlinx.serialization.json.jsonPrimitive import java.io.File /** - * Real answers from a live vespa-relay (`search-staging.brainstorm.world`), recorded once by + * Real answers from a live vespa-relay (`search.brainstorm.world`), recorded once by * `tools/search-parity/fetch_fixtures.py` and read from disk here. * * Recorded rather than fetched so `./gradlew test` stays offline and deterministic — a suite that diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchRelayParityTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchRelayParityTest.kt index 83631e1c32..a3bcff9bc3 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchRelayParityTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchRelayParityTest.kt @@ -31,7 +31,7 @@ import kotlin.test.assertTrue * * ## What this can and cannot prove * - * The oracle is `search-staging.brainstorm.world`, a vespa-relay — the same software Amethyst's + * The oracle is `search.brainstorm.world`, a vespa-relay — the same software Amethyst's * token language was ported from. It is a **valid oracle for NIP-01 filtering** and **not one for * NIP-50 text matching**, and the difference is the whole design of this file. * @@ -42,8 +42,8 @@ import kotlin.test.assertTrue * pins it. * * NIP-50 is not closed. This relay retrieves topically, not lexically: asked for `bitcoin` it - * returns a block-height summary that never says "bitcoin", and asked for `nostr` it returns "made - * my display name refer to my npub's last characters". Eight of the 64 recorded events — 12% — + * returns a liquidation-levels heatmap that only ever says "BTC", and asked for `"open source"` it + * returns "FOSS you say will save you". Eight of the 64 recorded events — 12% — * contain no literal occurrence of the term that fetched them. * * The fixture is recorded with the `include:spam` lens, which waives the web-of-trust gate. That diff --git a/tools/search-parity/fetch_fixtures.py b/tools/search-parity/fetch_fixtures.py index ad4f08a2f0..1bc2dbfd60 100755 --- a/tools/search-parity/fetch_fixtures.py +++ b/tools/search-parity/fetch_fixtures.py @@ -6,7 +6,7 @@ Run by hand when the fixture needs refreshing; the test that consumes its output touches the network, so `./gradlew test` stays hermetic and offline. ./gradlew :cli:installDist - tools/search-parity/fetch_fixtures.py > commons/src/jvmTest/resources/search-parity-fixture.json + tools/search-parity/fetch_fixtures.py > tools/search-parity/fixture.json Each case pins the filter FIELDS (kinds/tags/window) as flags rather than folding them into the search string, so the test can rebuild the identical NIP-01 Filter and assert our matcher @@ -18,7 +18,7 @@ import shlex import subprocess import sys -RELAY = os.environ.get("RELAY", "wss://search-staging.brainstorm.world") +RELAY = os.environ.get("RELAY", "wss://search.brainstorm.world") AMY = os.environ.get("AMY", os.path.join(os.getcwd(), "cli/build/install/amy/bin/amy")) # The relay gates and ranks results through the searcher's web of trust, and answers an # anonymous query with nothing at all. `include:spam` waives that gate, which is what makes @@ -26,9 +26,9 @@ AMY = os.environ.get("AMY", os.path.join(os.getcwd(), "cli/build/install/amy/bin # to one account's trust graph and re-records differently as that graph moves. # # It does NOT make the relay lexical. Measured on this corpus, results still include events -# carrying no literal occurrence of the term — asked for `nostr` it returns "made my display -# name refer to my npub's last characters". Retrieval is semantic; see the parity test for why -# that means text results are not compared. +# carrying no literal occurrence of the term — asked for `"open source"` it returns "FOSS you say +# will save you". Retrieval is semantic; see the parity test for why that means text results are +# not compared. LENS = os.environ.get("LENS", "include:spam") LIMIT = os.environ.get("LIMIT", "8") TIMEOUT = os.environ.get("TIMEOUT", "30") diff --git a/tools/search-parity/fixture.json b/tools/search-parity/fixture.json index 561c5d4776..8ca8ad56ee 100644 --- a/tools/search-parity/fixture.json +++ b/tools/search-parity/fixture.json @@ -20,28 +20,6 @@ ] ] }, - { - "content": "Very interesting! Around 32 minutes he makes a comment about basically hand encrypting. Which makes me think about an interesting case for maybe an air gapped messaging device that only shares encrypted marmot messages over QR to an Internet connected device. \n\n", - "created_at": 1771074608, - "id": "40921da81160cfef0710b57100f7d5fb4053e3395b3f28274a1aa3a1a8396c5e", - "kind": 1, - "pubkey": "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "sig": "9cfadea6054d22678904c1c7767ad2125af450bfbec1cf3de8c973408b7e20626f2e905f1735b7874b27beb05598aba226ec4497c5378eadcb2e9020f0b514f8", - "tags": [ - [ - "p", - "b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc", - "wss://relay.primal.net" - ], - [ - "e", - "d3da9e89cb7c45bf26b5b3df4b3d33375ccb406227deb4484f7869ae9040142b", - "wss://relay.primal.net", - "root", - "b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc" - ] - ] - }, { "content": "Was Bitcoin hijacked?\n\nInteresting critiques of #Bitcoin from one of the co-authors of *Hijacking Bitcoin*. Has anyone read it? I'm adding it to my 2025 reading list... #bookstr\nnostr:nevent1qqsy7580q8rwtr8ujd723vefk552ka6adflp9dncxdew8qpudn4n45qpr4mhxue69uhkummnw3ezucnfw33k76twv4ezuum0vd5kzmp0qgsz2vdh9xuuqxrxpn5t8vs2h5p78ywg2zgxv43p6qh5g7usdc05mpcrqsqqqqqpj0pnhd", "created_at": 1735176083, @@ -72,28 +50,6 @@ ] ] }, - { - "content": "Trump's self-serving moves could be the demise of Bitcoin.\n\nhttps://www.theverge.com/2024/9/16/24246805/trump-world-liberty-financial-crypto-platform-announcement", - "created_at": 1726552577, - "id": "62423119247288c27ebd49d40b125c7c8bf1ff1991530b75274107d9d8b4cd26", - "kind": 1, - "pubkey": "3d2673d46bc1757c0a266610b0e71abd726e4b66555a7d457265116ef68337ae", - "sig": "9b4c38dd5108221763db28cffa7de4e2f6bedcb2a9ad52946de80b5ece983e3ec8a13108b60a0952e35d6ffa4f2a54049627774131401273c709a42b568150eb", - "tags": [ - [ - "subject", - "Trump will be the end of #Bitcoin" - ], - [ - "t", - "bitcoin" - ], - [ - "client", - "voyage" - ] - ] - }, { "content": "Don't think it would pass at the moment, but I'd love to see a vote forced on this to better see where each Congressperson stands. Overall, bullish.\nhttps://www.coinspeaker.com/cynthia-lummis-bitcoin-strategic-reserve/\n#bitcoin ", "created_at": 1721899426, @@ -168,6 +124,32 @@ ] ] }, + { + "content": "8 hours left, how high will it go?\nhttps://scarce.city/auctions/buy-bitcoin\n https://image.nostr.build/27076e466ee4da429f8d36759a8bb3535d334970f2aadfd4f4f62c4a319a08cb.png\n\n#bitcoin #plebchain", + "created_at": 1713970324, + "id": "b1c98f0c793453776c0d9e6846161599a6c92e8170b6668200907c60a4b2bb99", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "ce9359cc91b271ab0baeccb62a7736c69996da491538cb82b285edf9c091209197e163e0a82a5fdacda818d78ad5100376da98149bc4321a8b558ee6afd655f3", + "tags": [ + [ + "t", + "bitcoin" + ], + [ + "t", + "plebchain" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "Buy bitcoin sign auction" + ] + ] + }, { "content": "If you are looking for a personal reserve currency, #Bitcoin is simply unbeatable. All of the arguments against it don't matter, if you hardly ever interact with it and it merely sits in cold storage to prove your solvency.\n\nIf you just want to buy a cup of coffee, pay your taxes, or order a flight online, you can use a second-rail of Bitcoin to do that, like Lightning.\n\nOr use dollars or poker chips or frequent flyer miles or a pack of cigarettes or a silver coin or wash the dishes... or whatever they'll accept in exchange.\nNone of these things negate the importance of Bitcoin, nor are they generally more-efficient or effective than something like Lightning.", "created_at": 1704962711, @@ -189,6 +171,56 @@ "Bitcoin is digital gold" ] ] + }, + { + "content": "BTC \u2022 Liquidation Levels Heatmap 03/ENE/2024 06:10\nhttps://image.nostr.build/eb619c86c281a455c0627b88a153bfa9a341579e4446b8bcfed0cbfc2dcc54e7.jpg\nhttps://image.nostr.build/35a8ef90f3ce9db9a435c9720a1d472e8220c27a77604f1df584e0ffb321a85b.jpg\nhttps://image.nostr.build/e857466246849ac273d91c99feb50b27d94db95540c50d64994e7657701207ac.jpg\nhttps://image.nostr.build/18738e3619ffff3b3546aa18690c526648cffabb4caeb2eaece6b8f9619c7157.jpg", + "created_at": 1704268836, + "id": "9c8ff40fbcf6be612ebcd5e831a08cde2b7f9e2fff9be8805ce46ca4dfd42252", + "kind": 1, + "pubkey": "c40cb64de1a1f6ec0d6c6d98ea665a42a09d37868f2919ae5703b324baa7a1e9", + "sig": "fa9f37fd6b20a8c19ec488e855a3a772f75b7adf31ce3b5903940a3a5a4e0a6fb490ae6f4c9c02d2de0c125914125b8a85565c671826ebbe4251620557d7e50f", + "tags": [ + [ + "imeta", + "url https://image.nostr.build/eb619c86c281a455c0627b88a153bfa9a341579e4446b8bcfed0cbfc2dcc54e7.jpg", + "ox eb619c86c281a455c0627b88a153bfa9a341579e4446b8bcfed0cbfc2dcc54e7 https://nostr.build", + "x c9f05304a8006a4395ebe8b6585042c9356b4689a35ad7bf17b91ccff2022d63", + "m image/jpeg", + "dim 1386x840", + "bh LoHoa.sW?aozxak9ofkA~poeV[oL", + "blurhash LoHoa.sW?aozxak9ofkA~poeV[oL" + ], + [ + "imeta", + "url https://image.nostr.build/35a8ef90f3ce9db9a435c9720a1d472e8220c27a77604f1df584e0ffb321a85b.jpg", + "ox 35a8ef90f3ce9db9a435c9720a1d472e8220c27a77604f1df584e0ffb321a85b https://nostr.build", + "x ed4f7b7ad0a35f367606c076c6d52919e75e59cbd0740999d5271bcec59e8d8c", + "m image/jpeg", + "dim 1386x840", + "bh LoH.Z]nl?aozxakAoej@~poLWBn+", + "blurhash LoH.Z]nl?aozxakAoej@~poLWBn+" + ], + [ + "imeta", + "url https://image.nostr.build/e857466246849ac273d91c99feb50b27d94db95540c50d64994e7657701207ac.jpg", + "ox e857466246849ac273d91c99feb50b27d94db95540c50d64994e7657701207ac https://nostr.build", + "x 28db9107cdb7d148617034fbff4a73db1393f6c4cfe578f33e4ae22fd5bcf199", + "m image/jpeg", + "dim 1386x840", + "bh LJRo]W%N_MxF-?o|x@Vt.k%1Q.Ny", + "blurhash LJRo]W%N_MxF-?o|x@Vt.k%1Q.Ny" + ], + [ + "imeta", + "url https://image.nostr.build/18738e3619ffff3b3546aa18690c526648cffabb4caeb2eaece6b8f9619c7157.jpg", + "ox 18738e3619ffff3b3546aa18690c526648cffabb4caeb2eaece6b8f9619c7157 https://nostr.build", + "x c87414fbd60abc358799c98dc754cc31ab07d506f858a872cbf4134f0fb24fa4", + "m image/jpeg", + "dim 1386x798", + "bh LkPQmAE0%3-;%LaykBWB~E%MInNF", + "blurhash LkPQmAE0%3-;%LaykBWB~E%MInNF" + ] + ] } ], "flags": [ @@ -256,6 +288,41 @@ ] ] }, + { + "content": "nostr:naddr1qqnhgmpdwp5kutfjv4nxzcfhxy6j6vn9veskzde3x5kky6t5vdhkjm3dwejkuer0wgq3samnwvaz7tmnwpshg6tp94shycmpdesjucm0d5hsygpwl2n3twa5dh2mu6ma4rthqqnx6yt8fwgnhqtc4hd4ctnrmxrnxypsgqqqw5cqqp2llq", + "created_at": 1780004572, + "id": "0f07af38491d37cdfb55eea1896abd83388519ec8fde7f652c8da2dc2cce1e0e", + "kind": 1, + "pubkey": "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331", + "sig": "2eb289b08ca3c3f79c7f221dc13e8c3395179d538467783eec72b5dee50a634f2fe033b3d26a55400e6d2a8c8f591f7cd8219079ffb6d144691fba1709dc71d0", + "tags": [ + [ + "alt", + "A short note: nostr:naddr1qqnhgmpdwp5kutfjv4nxzcfhxy6j6vn9veskzd..." + ], + [ + "e", + "a4ac96cd4bd4f9abe16f13c26816da93af7f53fcc919636c8627cfc03c516624", + "wss://relay.primal.net/", + "root", + "174ed916a8520e623293cad8419a176edbcc8c1a1e5f5271e36c922730f9470f" + ], + [ + "p", + "174ed916a8520e623293cad8419a176edbcc8c1a1e5f5271e36c922730f9470f", + "wss://profiles.nostr1.com/" + ], + [ + "p", + "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331", + "wss://spatia-arcana.com/" + ], + [ + "client", + "Amethyst" + ] + ] + }, { "content": "It\u2019s insane. \n\nBugging Francis to put nwc lol ", "created_at": 1780003520, @@ -435,129 +502,45 @@ ] }, { - "content": "nostr:nprofile1qyd8wumn8ghj7ctjw35kxmr9wvhxcctev4erxtnwv4mhxqpqmvlm9xukx5pek66dzt2muwkefhx3mzc4thlzt756n9h8dplrtk3qmxle2g\nBITCOIN LIGHTNING PAYMENT ZAPS ACROSS SATELLITE IN HISTORIC FIRST\n\nhttps://s.w.org/images/core/emoji/16.0.1/72x72/26a1.png\n--\n\u270d\ufe0f A Bitcoin Lightning payment request was relayed through a geostationary satellite and then paid, marking the first public demonstration of a Lightning invoice transmitted through actual space.\n--\n\ud83d\udc49 First public demonstration of a Lightning invoice transmitted through actual space\n\ud83d\udc49 Lightning payment request relayed through a geostationary satellite\n\ud83d\udc49 Decoupling Bitcoin from single points of terrestrial failure\n\ud83d\udc49 Potential for satellite infrastructure to harden Bitcoin's communications layer\n\n--\n#Printer #Europe #Africa #MiddleEast #Asia #technology\n--\nnostr:nevent1qvzqqqqqqypzpkelk2devdgrnd456yk4hcadjnwdrk932h07yhaf4xtww6r7xhdzqyd8wumn8ghj7ctjw35kxmr9wvhxcctev4erxtnwv4mhxqpqyd70x9wr4mt7vp2ukvklwa2tr4pugaww4s83g4dzda24urverpzs3am40v \n ", - "created_at": 1757588519, - "id": "94bdc9e1d70588ac5516a29bc35647db7e721e63b6be731ae07d4ebd7f5284e8", + "content": "\nnostr:nprofile1qy3hwumn8ghj7un9d3shjtt5v4ehgmn9wshxkwrn9ekxz7t9wgejumn9waesqgrjxj24tzky80sws3pf636u0rwueruy07ttwhd3jng95xdm03masuq2e9z6\n\u270d\ufe0f Block Inc. announces the launch of its Bitcoin Business Stack, a comprehensive platform for merchants to accept and manage Bitcoin payments, and makes a historic push for Lightning payments at the Bitcoin 2025 Conference.\n\ud83d\udc49 Block Inc. launches the Bitcoin Business Stack, a comprehensive platform for merchants to accept and manage Bitcoin payments\n\ud83d\udc49 Merchants can now accept Bitcoin directly in-store using Square POS\n\ud83d\udc49 Block Inc. is rolling out Bitcoin payment capability for merchants using Square POS\n\ud83d\udc49 Block Inc. is committed to making Bitcoin an everyday currency for everyone\n\ud83d\udc49 Block Inc. is focused on making Bitcoin more accessible, secure, and usable\n\n#MilesSuter #Bitcoin2025Conference #technology\nnostr:nevent1qvzqqqqqqypzqu35j42c43pmur5yg2w5whrcmhxglprlj6m4mvv56pdpnwmuwlv8qy3hwumn8ghj7un9d3shjtt5v4ehgmn9wshxkwrn9ekxz7t9wgejumn9waesqgxcr3vrdp2kws3gqywadc8xx0deudaezl9kpl9qgm7fchuum8gfyucdrx7r \n", + "created_at": 1748460431, + "id": "664a7133a599f3494362f2f7df474645878e0a213f199ed7a67125038f67b069", "kind": 1, - "pubkey": "de1abdd3127788ab39fb6f558233067d7cf07e8165598738c9a46a9cff3979a0", - "sig": "08d5eb18ea7ff848f41d3f09688ffb1914761b38a33eb663a97452c9e50fd9984add5b93129789be5bee8f6de30010fc20c22288c9afe625053c1f28b92302fb", + "pubkey": "2902af7ca5cf9039dd5846469404be1010a5460af4e52b3a857a6db84dad3caa", + "sig": "4823794334a7d9c9198932a6e7498dac7a3ee96c82dc537f985e09c36917498d2d3059a20294be1216cd4dd2bb116a52284e55558804a49ff89bd13fe7050fc8", "tags": [ [ "r", - "https://bitcoinist.com/bitcoin-lightning-payment-satellite-historic-first/" + "https://bitcoinmagazine.com/news/block-announces-bitcoin-business-stack-makes-historic-lightning-payments-push-at-bitcoin-2025" ], [ "subject", - "Bitcoin Lightning Payment Zaps Across Satellite In Historic First" + "Block Announces Bitcoin Business Stack, Makes Historic Lightning Payments Push at Bitcoin 2025" ], [ "published_at", - "1757588406" + "1748460178" ], [ "image", - "https://s.w.org/images/core/emoji/16.0.1/72x72/26a1.png" + "https://bitcoinmagazine.com/wp-content/uploads/2025/05/Miles-Suter-BTC.jpg" ], [ "p", - "de1abdd3127788ab39fb6f558233067d7cf07e8165598738c9a46a9cff3979a0", - "wss://articles.layer3.news" - ], - [ - "p", - "db3fb29b9635039b6b4d12d5be3ad94dcd1d8b155dfe25fa9a996e7687e35da2", - "wss://articles.layer3.news" + "2902af7ca5cf9039dd5846469404be1010a5460af4e52b3a857a6db84dad3caa", + "wss://relay-testnet.k8s.layer3.news" ], [ "imeta", - "url https://s.w.org/images/core/emoji/16.0.1/72x72/26a1.png" + "url https://bitcoinmagazine.com/wp-content/uploads/2025/05/Miles-Suter-BTC.jpg" ], [ "t", - "Printer" + "Miles Suter" ], [ "t", - "Europe" - ], - [ - "t", - "Africa" - ], - [ - "t", - "Middle East" - ], - [ - "t", - "Asia" - ], - [ - "t", - "technology" - ], - [ - "t", - "Technology:perspective" - ], - [ - "summary", - "The experiment, conducted by user 'Printer', uplinked a Lightning invoice as an image to the QO-100 amateur radio transponder and downlinked it back to Earth, where it was decoded and settled over the Lightning Network. This demonstration showcases the potential for satellite infrastructure to harden Bitcoin's communications layer against last-mile failures, censorship, and disaster scenarios." - ] - ] - }, - { - "content": "\nnostr:nprofile1qyd8wumn8ghj7ctjw35kxmr9wvhxcctev4erxtnwv4mhxqpqmvlm9xukx5pek66dzt2muwkefhx3mzc4thlzt756n9h8dplrtk3qmxle2g\n\u270d\ufe0f A Bitcoin Lightning payment request was relayed through a geostationary satellite and then paid, marking the first public demonstration of a Lightning invoice transmitted through actual space.\n\ud83d\udc49 First public demonstration of a Lightning invoice transmitted through actual space\n\ud83d\udc49 Lightning payment request relayed through a geostationary satellite\n\ud83d\udc49 Decoupling Bitcoin from single points of terrestrial failure\n\ud83d\udc49 Potential for satellite infrastructure to harden Bitcoin's communications layer\n\n#Printer #Europe #Africa #MiddleEast #Asia #technology\nnostr:nevent1qvzqqqqqqypzpkelk2devdgrnd456yk4hcadjnwdrk932h07yhaf4xtww6r7xhdzqyd8wumn8ghj7ctjw35kxmr9wvhxcctev4erxtnwv4mhxqpqyd70x9wr4mt7vp2ukvklwa2tr4pugaww4s83g4dzda24urverpzs3am40v \n", - "created_at": 1757588519, - "id": "f54988f41ad840926c56362ff6a7a3194b6fdcd7cf82c11544c808b9bf1d9bdb", - "kind": 1, - "pubkey": "7455e74032c0aa128d6d3458612647578d3823526dd019e7f1a33f0f4b0fdb6c", - "sig": "0c08c3fcd70e2e4ab389bb30c235f815cabd9c8fe790211beac35e4563231a8f2639c2e12a00749fcfdee076f7123b00e889529761fdd8f35f4ba076a00905ad", - "tags": [ - [ - "r", - "https://bitcoinist.com/bitcoin-lightning-payment-satellite-historic-first/" - ], - [ - "subject", - "Bitcoin Lightning Payment Zaps Across Satellite In Historic First" - ], - [ - "published_at", - "1757588406" - ], - [ - "image", - "https://s.w.org/images/core/emoji/16.0.1/72x72/26a1.png" - ], - [ - "p", - "7455e74032c0aa128d6d3458612647578d3823526dd019e7f1a33f0f4b0fdb6c", - "wss://articles.layer3.news" - ], - [ - "imeta", - "url https://s.w.org/images/core/emoji/16.0.1/72x72/26a1.png" - ], - [ - "t", - "Printer" - ], - [ - "t", - "Europe" - ], - [ - "t", - "Africa" - ], - [ - "t", - "Middle East" - ], - [ - "t", - "Asia" + "Bitcoin 2025 Conference" ], [ "t", @@ -565,19 +548,19 @@ ], [ "p", - "db3fb29b9635039b6b4d12d5be3ad94dcd1d8b155dfe25fa9a996e7687e35da2", - "wss://articles.layer3.news" + "723495558ac43be0e84429d475c78ddcc8f847f96b75db194d05a19bb7c77d87", + "wss://relay-testnet.k8s.layer3.news" ], [ "e", - "237cf315c3aed7e6055cb32df7754b1d43c475ceac0f1455a26f555e0d991845", - "wss://articles.layer3.news", + "d81c5836855674228011dd6e0e633db9e37b917cb60fca046fc9c5f9cd9d0927", + "wss://relay-testnet.k8s.layer3.news", "root" ], [ "q", - "237cf315c3aed7e6055cb32df7754b1d43c475ceac0f1455a26f555e0d991845", - "wss://articles.layer3.news" + "d81c5836855674228011dd6e0e633db9e37b917cb60fca046fc9c5f9cd9d0927", + "wss://relay-testnet.k8s.layer3.news" ] ] } @@ -593,41 +576,62 @@ { "events": [ { - "content": "**Nostr Nedir?**\n\nNostr, d\u00fc\u015f\u00fcn\u00fclenin aksine bir uygulama de\u011fil, bir sosyal a\u011f protokol\u00fcd\u00fcr. Pop\u00fcler bir araca benzetmek gerekirse Twitter diyebiliriz ancak Nostr'\u0131n belli bir patronu ya da merkezi bir sahibi yoktur. Sizin payla\u015ft\u0131\u011f\u0131n\u0131z g\u00f6nderileri silmek ya da belli bir kategoriye koymakla g\u00f6revlendirilmi\u015f tek bir kurum ya da ki\u015fi bulunmaz. Nostr, herkesin \u00f6zg\u00fcrce fikirlerini beyan edebildi\u011fi merkeziyetsiz bir sosyal a\u011fd\u0131r.\n\n\n**Nostr hesab\u0131m\u0131 nas\u0131l kurabilirim?**\n\nNostr anahtarlar\u0131 iki b\u00f6l\u00fcme ayr\u0131l\u0131r: nsec ve npub. nsec sizin gizli parolan\u0131z olmakla beraber Nostr hesab\u0131n\u0131z\u0131n kalbidir. Bu parolay\u0131 kaybetti\u011finiz anda hesab\u0131n\u0131za eri\u015fiminiz tamamen kapan\u0131r. nsec anahtar\u0131n\u0131z\u0131 kimseyle payla\u015fmad\u0131\u011f\u0131n\u0131z s\u00fcrece hesab\u0131n\u0131z ba\u015fkalar\u0131 taraf\u0131ndan ele ge\u00e7irilemez. npub ise sizin kullan\u0131c\u0131 ad\u0131n\u0131z (herkese a\u00e7\u0131k adresiniz) gibi d\u00fc\u015f\u00fcn\u00fclebilir. Nostr hesab\u0131 olu\u015fturmak i\u00e7in do\u011frudan Primal veya Amethyst gibi bir Nostr uygulamas\u0131n\u0131 indirip saniyeler i\u00e7inde anahtarlar\u0131n\u0131z\u0131 \u00fcretebilirsiniz.\n\n\n**Nostr ad\u0131 alt\u0131nda bir uygulama bulam\u0131yorum?**\n\nEvet, bu \u00e7ok normal; \u00e7\u00fcnk\u00fc yaz\u0131n\u0131n ba\u015f\u0131nda da belirtti\u011fim gibi Nostr bir uygulama de\u011fil, bir internet protokol\u00fcd\u00fcr. A\u015fa\u011f\u0131da bulabilece\u011finiz uygulamalar\u0131 kullanarak al\u0131\u015f\u0131lagelen kullan\u0131m\u0131n\u0131za devam edebilirsiniz:\nPrimal: Ak\u0131\u015f\u0131 takip etmek i\u00e7in (Twitter benzeri aray\u00fcz)\nAmethyst: Android i\u00e7in pop\u00fcler bir Nostr istemcisi\nYakihonne: Uzun makale ve blog yazmak isteyenler i\u00e7in\n\n\n**Ak\u0131\u015f\u0131m nas\u0131l takip ediliyor?**\n\nInstagram ve Twitter benzeri uygulamalar\u0131n aksine Nostr size ba\u011f\u0131ml\u0131l\u0131k yapacak yapay zek\u00e2 algoritmalar\u0131yla bir ak\u0131\u015f olu\u015fturmaz. Sadece takip etti\u011finiz ki\u015filerin ve kanallar\u0131n payla\u015ft\u0131klar\u0131n\u0131 g\u00f6r\u00fcrs\u00fcn\u00fcz. Bu da saatlerce i\u00e7erik kayd\u0131rmak gibi vakit kay\u0131plar\u0131n\u0131 minimuma d\u00fc\u015f\u00fcr\u00fcr.\n\n\n**Zap / Bitcoin**\n\nNostr, \"Zap\" destekli (mikro bah\u015fi\u015fler) bir platformdur. \u0130nsanlar payla\u015ft\u0131\u011f\u0131n\u0131z g\u00f6nderilerin alt\u0131na Zap g\u00f6ndererek size Bitcoin \u00fczerinden do\u011frudan ba\u011f\u0131\u015fta bulunabilirler.\n\n\n**Zap hesab\u0131 ve kurulumu**\n\n\u00d6ncelikle Nostr hesab\u0131n\u0131za eri\u015fim sa\u011flay\u0131n. Daha sonras\u0131nda Wallet of Satoshi veya benzeri bir Bitcoin Lightning c\u00fczdan\u0131 \u00fczerinden bir Lightning adresi olu\u015fturun. Kulland\u0131\u011f\u0131n\u0131z uygulama (Primal, Amethyst vb.) profil ayarlar\u0131ndan Lightning adresinizi ba\u011flay\u0131n ve bitti. Art\u0131k di\u011fer insanlardan Zap alabilirsiniz.\n\n\n**Genel \u00d6zet**\n\nNostr; beynimizi \u00e7\u00fcr\u00fctmeyen, zaman\u0131m\u0131z\u0131 bo\u015fa harcatmayan bir platform olarak g\u00fcny\u00fcz\u00fcne \u00e7\u0131kmaya ba\u015flad\u0131. Belli bir ki\u015finin kurdu\u011fu uygulamalarda saatlerce vakit ge\u00e7irip hesab\u0131n\u0131z\u0131n nedensizce kapat\u0131lmas\u0131 ya da sans\u00fcrlenmesi riskine gerek yok. Daha \u00f6zg\u00fcrl\u00fck\u00e7\u00fc olan bu a\u011f hepimize iyi gelecek. Sa\u011fl\u0131cakla kal\u0131n.\n\ntopluluk@nostr.org.tr", - "created_at": 1788483939, - "id": "a699579a051fa603b4ea0d7cd4a4e354c345e7b8959df02e87aa57242cdc8ec8", + "content": "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via relays. it grew out of a small community of bitcoin/lightning devs, specifically [fiatjaf](nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6). it was a reaction to the counter-party risks in using centralized social networks (twitter, facebook, etc) and the failings of other, semi-decentralized approaches (mastodon).\n\nbitcoin familiarity helps when groking nostr. the identity portion of nostr is private/public key pair. similar to a bitcoin wallet where one uses one\u2019s private key to sign a transaction and publish it into the network, one uses a nostr private key to sign an event and publish it to relays. there are various defined [kinds](https://github.com/nostr-protocol/nips#event-kinds) of events, from profile information (name, about, url, avatar url, etc), to contact lists (follows), to notes (a short plain text message). the bitcoin network is peer to peer (distributed), where nodes will gossip transactions around to all the other nodes, but this is not the case with nostr. events stay where you publish them, relays only talk to clients, not other relays. this makes nostr decentralized, but not distributed. since it\u2019s not peer-to-peer, one client doesn\u2019t need to see the entire traffic of the network, or to query some distributed hash table a la IPFS, to find content across the network. this middle ground between centralized and distributed allows snappy response times yet prevents the domination of network lock-in.", + "created_at": 1782172014, + "id": "4c3ffd171a535f9bac1afc47526b565833c726f82e59b3bf287db3e39cafc48e", "kind": 30023, - "pubkey": "628b2c9aedcf9da47f42128402f3fd82787dac75c3595db1bccf3b7baab0452f", - "sig": "cf0cdbf17d750c38a0fc0e168d68338761e84f9eb174e09806e656002de23d4503f5c07a3872228058bfe5154db5d5eaf01a734d063f8e361aef4c42941961ff", + "pubkey": "27c66d241898300a254f0853dfbfcf735a627dc2790c45a03de04704dd54a3b4", + "sig": "cc646b6cd05fe3e1fbcc365e9a042ca0ecd0b04523d3c228ce1504dc77703cea150ebdab0e4704fe3fb9bc5d6a08acf3ca32367386cdbab4a778e18213f85ab2", "tags": [ - [ - "client", - "YakiHonne", - "31990:20986fb83e775d96d188ca5c9df10ce6d613e0eb7e5768a0f0b12b37cdac21b3:1700732875747" - ], - [ - "d", - "579d7bcc2e06132c" - ], - [ - "image", - "" - ], [ "title", - "Nostr Nedir?" + "nostr" ], [ "summary", - "" + "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via" ], [ "published_at", - "1788483851" + "1676764800" ], [ - "t", - "#nostr" + "d", + "d4b8f38a8c6e1e9a9ecbffecb23fd7dfe7b68b18d08584cb7b09d7e858f5ebf6" + ], + [ + "r", + "https://plantimals.org/nostr/" + ] + ] + }, + { + "content": "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via relays. it grew out of a small community of bitcoin/lightning devs, specifically [fiatjaf](nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6). it was a reaction to the counter-party risks in using centralized social networks (twitter, facebook, etc) and the failings of other, semi-decentralized approaches (mastodon).\n\nbitcoin familiarity helps when groking nostr. the identity portion of nostr is private/public key pair. similar to a bitcoin wallet where one uses one\u2019s private key to sign a transaction and publish it into the network, one uses a nostr private key to sign an event and publish it to relays. there are various defined [kinds](https://github.com/nostr-protocol/nips#event-kinds) of events, from profile information (name, about, url, avatar url, etc), to contact lists (follows), to notes (a short plain text message). the bitcoin network is peer to peer (distributed), where nodes will gossip transactions around to all the other nodes, but this is not the case with nostr. events stay where you publish them, relays only talk to clients, not other relays. this makes nostr decentralized, but not distributed. since it\u2019s not peer-to-peer, one client doesn\u2019t need to see the entire traffic of the network, or to query some distributed hash table a la IPFS, to find content across the network. this middle ground between centralized and distributed allows snappy response times yet prevents the domination of network lock-in.", + "created_at": 1782142849, + "id": "d957e3daa1b7c6275cb2335edbe67bef9e21218aaa32731f7362f2b17367e297", + "kind": 30023, + "pubkey": "8d5b9142b2021c38ecbd9fe6bead324836dad84379643507eb8a4ce58dd31feb", + "sig": "f93128dc50737a678b41d8d57c051281839325e7de6692605902d3bb718ec5a450bde6e28a4715dac852797c161ba54f96d69183a4826506ca1b7232817b07cf", + "tags": [ + [ + "title", + "nostr" + ], + [ + "summary", + "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via" + ], + [ + "published_at", + "1676764800" + ], + [ + "d", + "d4b8f38a8c6e1e9a9ecbffecb23fd7dfe7b68b18d08584cb7b09d7e858f5ebf6" + ], + [ + "r", + "https://plantimals.org/nostr/" ] ] }, @@ -700,74 +704,6 @@ ] ] }, - { - "content": "nostr.wine nostr relay.", - "created_at": 1771436582, - "id": "85fe5aa85429bb367ce0cf8f03f71f74ba83dd02f1b42f666bfb921bb07c7c86", - "kind": 30023, - "pubkey": "e938eb5077f30bd8fd0f9a0681514e52706fb231f0ac7516e3194530618addfa", - "sig": "40c7e26e24663feb4a22906d22bb424740ddf826d6c71a2a4839e8adf533f11692ad1b19c9566414849e2519b508398b7d848e63b2b9ef6631567b3242bd0460", - "tags": [ - [ - "d", - "nostr_wine" - ], - [ - "title", - "nostr.wine Nostr Relay" - ], - [ - "image", - "" - ], - [ - "published_at", - "2026-02-18 17:43:02 +0000 UTC" - ], - [ - "t", - "nostr" - ], - [ - "t", - "relay" - ] - ] - }, - { - "content": "nostr.mom nostr relay.", - "created_at": 1771436581, - "id": "d6f0e0db71f5b276e008d7285b896e069b78f0eb5da3b34fdd87c97eac66d01a", - "kind": 30023, - "pubkey": "e938eb5077f30bd8fd0f9a0681514e52706fb231f0ac7516e3194530618addfa", - "sig": "6936e469dd04081e2fd9eb79800793258d7cbae1a0f178a05d9a2480037f48ec78b9e9e3d3486ccdaa3922f29eae73a7b7734222f7a76070d3cdbc9260fe487a", - "tags": [ - [ - "d", - "nostr_mom" - ], - [ - "title", - "nostr.mom Nostr Relay" - ], - [ - "image", - "" - ], - [ - "published_at", - "2026-02-18 17:43:01 +0000 UTC" - ], - [ - "t", - "nostr" - ], - [ - "t", - "relay" - ] - ] - }, { "content": "Nostr (Notes and Other Stuff Transmitted by Relays) is a decentralized, censorship-resistant social protocol designed to give users control over their online identities and interactions.", "created_at": 1771436580, @@ -874,6 +810,60 @@ "This is a long form article, you can read it in https://habla.news/franzap/respect-the-a-context" ] ] + }, + { + "content": "\n# Blogging Nostr!\n\n\n![avatar](https://paynym.is/PM8TJXVwNbJb8MGphGbijWucP3fw9mkDZ9BQT3pGkCsdFK21cR6v5xKrart2oBsqDMkxtqMGgkoTgn1NAczLznUeTwa4BG2rs1zZDfEtKin7WuBcdvcK/avatar)\n\n------------\n\n\n\n\nHabla is a new nostr native blogging platform. It is the second one I have found. I'm, writing this as a test, but so fair I have noticed my markdown syntax is not being rendered yet. On the other hand, I also seem to be able to see my blogstack post on Habla as well. This is why it's a test. I'm still trying to wrap my head around how this all works.\n\n\n\nI've been writing a blog about how I use bitcoin since April on a platform namedtw Ghost under a pseudonym. I wrote on that platform for the following reasons:\n\n* I want to be a writer\n\n* I spend most of my waking hours thinking about bitcoin.\n\n* Marty Bent uses Ghost and had this paywall I thought was really cool at the time.\n\nMarty's paywall is actually kind a neat, Instead of asking his readers to pay an annual subscription fee like the New York Times, You could just pay 50 cents per article. This is much better than the New York Times Paywall. When I first heard of it, I thought writers could only get paid on the Internet with a paywall or ads. The mechanics of the system did not leave many options. Lightning addresses worked, but most people never heard of them and even most Bitcoiners didn't use them. Yalls had a paywall, but the UX wasn't great. I first thought of starting a blog because of yalls. I thought I could do magic tricks over video for tips, like buskers in Vegas, but on the Internet. Then I thought, I'm not that good at magic and I don't feel comfortable throwing my face out there like a whore on the Internet (No disrespect to sex workers, but **I doubt anyone would pay for my sex services** and **my wife would not approve**.) I also have a degree in creative writing, but I'm a bit rusty. The lightning network got me interested in writing again.\n\nIt's what prompted me to run my own lightning node in the first place. I think this was back in 2018 or 2019. At the time, I tried running [Pierre Rochard's lightning node](https://github.com/lightning-power-users/node-launcher), but didn't have the technical chops to pull it off on my old Windows laptop. It might have worked if I knew that I needed to plug the damn thing into an Ethernet cable to download the bitcoin blockchain.(We didn't use the term time chain in those days).\n\n----\n\nHis paywall was an order of magnitude better than running my own node to accept tips on yalls.org, so I started my blog on ghost with the intention of using his paywall software.\n\nI spun up a virtual private server on Bitlaunch.\n\nI installed Yunohost because it was easy to set up Ghost. \n\nNow Marty finally launched his service. If you like this model, you can use it for charges 10 bucks a month.\n\nFor the record, **I really like Marty**. I listen to Rabbit Hole Recap every week. I'm not trying to ruffle any feathers, or shit on his side project, but [@dergigi](https://dergigi.com/2021/12/30/the-freedom-of-value/) convinced that the paywall model will soon go the way of the doorstep delivered newspaper. If you don't have time to read or listen to that entire article, please listen to this 30 second clip of the Bitcoin.Review podcast created by [Oscar Merry](https://fountain.fm/clip/VF7nRhSOGejzI4KbfsWG).\n\n----\n\n## Blogs Can Be Nostr Native\n\nThere's no reason to spin up a server on a VPS to start a blog anymore. No reason to host it on a Raspberry Pi. You don't need to buy a shared hosting account. You don't need to install ruby, gem or jekyll to host it on Github. You don't even need to buy a domain, although you might want to buy one for a [DIY Nip 05 verification](https://nvk.org/n00b-nip5). Hopefully, in the not too distant future, you won't even need Github. Habla.news is a Medium alternative, but there's no reason why we couldn't have a Github alternative. We're just waiting on someone to build it. \n\nThe reason I find this value 4 value model built on Nostr so exciting now is because *it is now trivial for those who produce value to receive value* and *trivial for those who cherish valuable content to send value to those who produce it*. \n\n**Why spend the time setting up ghost or pay a fee to accept lightning payments or a ghost account?** --*Especially if you are writing a blog about bitcoin.\n\nI like the idea of charging for services, but paywalls just don't jive with the old walled garden model anymore. \n\nNostr is censorship resistant. Anyone can create a nostr key.\n \n[Some freaks say it's not normie ready](https://iris.to/post/note1mn98w4szpnkdfc53sqmgwme0akrt7wz5vzvvwwm840qv0020zu4s2fe62f), but it is much easier to create a private key than create a blog using the aforementioned options. I look forward to the day when we see more no-coiners witnessing how trivial it is to send value over the Internet. [@TheGuySwann](https://iris.to/post/note1c39e8k2c0uj7l77q0p7md5a9rahumv8dr57crd3r4xq7tegecn9sx7tckq) wrote a great post about how Facebook spent years developing their shitcoin, only to get shut down by congress.\n\n----\n### Paywalls Not Required\n\n**You couldn't accept native bitcoin on any other platforms until now**. Now **plebs are zapping each other left and right**.\n\nIn the value 4 value model, the zapper sets the value on the article. The paywall sets a price on reading the full article.\n\n\n\n* We **don't need paywalls anymore.**\n\n* **I don't need to send you spam** .\n\n* If you want to read my blog, just **follow me on Nostr.**\n\n* We **don't need to rely on digital overlords** to create content. \n\n* We **don't need ads **to monetize content. \n\n**Ads suck.**\n\nThis is **pure vida.**\n\n----\n\n**Nostr is the first amendment translated into code** like Satoshi translated this math:\n\n![bitcoin supply math](https://blog.lopp.net/content/images/2022/01/subsidy_equation.jpg)\n\n\n----\n\n\nInto this code:\n\n----\n\n![bitcoin monetary supply ](https://blog.lopp.net/content/images/2022/01/Screenshot-from-2022-01-10-18-07-46.png)\n\n----\nWe can write without **undue influence** from ads, corporations, or even governments.\n\n\n\n**We can create a better Internet.**\n\n\n\n**Code is speech.\nCypherpunks write code.**\n\n\n**Nostr makes speech unstoppable**.\n\nThat's a big fucking deal.\n\n\n\n**Blogging Bitcoin**\n\n![block](https://nostr.build/i/nostr.build_62b2b95fdf1b8df642842d7a7d27c3dee975e32ed60db4ab9abf3e9f19885526.png)\n\n------------\n\n\n[![Value 4 Value](https://nostr.build/i/nostr.build_571397a69b34a41f499ad9b057a49c65531e104785428e5a00a56a500017e60f.png)](https://bloggingbitcoin.store/apps/cd4EUVkfhWbTg5UZsbCJPi5ghxZ/pos)\n", + "created_at": 1677183856, + "id": "d1d627b2e4120e4362c206a49c39d2cd7abc4bf49b83e282cb9b58cb71b2af2f", + "kind": 30023, + "pubkey": "75bf23531ae9f98c62995ba07191e488ead475975371d63d7dfd46bde1bfa895", + "sig": "a76f74be0cf073d1d197d20675092dc4e6d03a850db7ce50622e180f1a7d12ad2c1a689e8d85a35aea308293f73a1acc6d375c77df404bb3bd7b08037dbba95e", + "tags": [ + [ + "client", + "blogstack.io" + ], + [ + "title", + "Nostr" + ], + [ + "image", + "https://nostr.build/i/karnage/nostr.build_ac9b396c77ef9ef096ff7c24c8403b6653df51b56c76d65e6db17540ff7d6e3c.png" + ], + [ + "summary", + "Blogging on Nostr instead of Ghost using the value 4 value model instead of paywalls." + ], + [ + "t", + "nostr" + ], + [ + "t", + "blog" + ], + [ + "t", + "value4value" + ], + [ + "t", + "bitcoin" + ], + [ + "t", + "freespeech" + ], + [ + "d", + "3f9775d8" + ], + [ + "published_at", + "1677183856" + ] + ] } ], "flags": [ @@ -934,28 +924,6 @@ ] ] }, - { - "content": "Trump's self-serving moves could be the demise of Bitcoin.\n\nhttps://www.theverge.com/2024/9/16/24246805/trump-world-liberty-financial-crypto-platform-announcement", - "created_at": 1726552577, - "id": "62423119247288c27ebd49d40b125c7c8bf1ff1991530b75274107d9d8b4cd26", - "kind": 1, - "pubkey": "3d2673d46bc1757c0a266610b0e71abd726e4b66555a7d457265116ef68337ae", - "sig": "9b4c38dd5108221763db28cffa7de4e2f6bedcb2a9ad52946de80b5ece983e3ec8a13108b60a0952e35d6ffa4f2a54049627774131401273c709a42b568150eb", - "tags": [ - [ - "subject", - "Trump will be the end of #Bitcoin" - ], - [ - "t", - "bitcoin" - ], - [ - "client", - "voyage" - ] - ] - }, { "content": "Don't think it would pass at the moment, but I'd love to see a vote forced on this to better see where each Congressperson stands. Overall, bullish.\nhttps://www.coinspeaker.com/cynthia-lummis-bitcoin-strategic-reserve/\n#bitcoin ", "created_at": 1721899426, @@ -1030,6 +998,32 @@ ] ] }, + { + "content": "8 hours left, how high will it go?\nhttps://scarce.city/auctions/buy-bitcoin\n https://image.nostr.build/27076e466ee4da429f8d36759a8bb3535d334970f2aadfd4f4f62c4a319a08cb.png\n\n#bitcoin #plebchain", + "created_at": 1713970324, + "id": "b1c98f0c793453776c0d9e6846161599a6c92e8170b6668200907c60a4b2bb99", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "ce9359cc91b271ab0baeccb62a7736c69996da491538cb82b285edf9c091209197e163e0a82a5fdacda818d78ad5100376da98149bc4321a8b558ee6afd655f3", + "tags": [ + [ + "t", + "bitcoin" + ], + [ + "t", + "plebchain" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "Buy bitcoin sign auction" + ] + ] + }, { "content": "If you are looking for a personal reserve currency, #Bitcoin is simply unbeatable. All of the arguments against it don't matter, if you hardly ever interact with it and it merely sits in cold storage to prove your solvency.\n\nIf you just want to buy a cup of coffee, pay your taxes, or order a flight online, you can use a second-rail of Bitcoin to do that, like Lightning.\n\nOr use dollars or poker chips or frequent flyer miles or a pack of cigarettes or a silver coin or wash the dishes... or whatever they'll accept in exchange.\nNone of these things negate the importance of Bitcoin, nor are they generally more-efficient or effective than something like Lightning.", "created_at": 1704962711, @@ -1053,24 +1047,24 @@ ] }, { - "content": "CyberHornet of Bitcoin\n\nhttps://cdn.satellite.earth/4da669a843213de2f1c4a28ae28e10edcbe646134a19c7eaf183c22a21889b7b.jpg\n\nIt is imperative of the pleb to combat attacks on #Bitcoin, verbal or otherwise.", - "created_at": 1698786952, - "id": "030f7946eb8b5a6d8327fdbad510fcb45bab9730133660a17f9faaf189604a37", + "content": "Today's Bitcoin News! (10 min. video) \"Bitcoin ETF Race is ON, $1.2 Million per Bitcoin?, #Bitcoin vs Gold, OpenSats onboards Tobin Harding #Nostr #BTC\"\n\nhttps://youtu.be/8OvAm_xPM1I\n\nYouTube \u2014> https://youtu.be/8OvAm_xPM1I\n\nToday we discuss the the race for the first Bitcoin spot ETF in the US, significant leadership changes at Grayscale Investments, Cathie Wood's bold Bitcoin prediction, the ongoing debate between Bitcoin and Gold, and the latest developments from OpenSats. Join us as we explore these pivotal moments in the cryptocurrency world and what they mean for investors and enthusiasts alike. Here are all the topics and link: \n\nBitcoin Archive account on Nostr \u2014 https://nostr.com/npub15h5n4muwsgxtc74hkcs9lp2ts7hdfdyvta4np7a7hfwfneqdeuls5qtl4k\n\nThe Bitcoin spot ETF race: \n\t\u201cSEC tells spot Bitcoin ETF applicants to update submissions by December 29 if they want to be approved in the first wave\u201d - Reuters. \n\tFIDELITY BITCOIN ETF \u2014 A spot Bitcoin ETF will give 124m Americans immediate and direct access to Bitcoin through their brokerage accounts. Fidelity has 43 million brokerage accounts for customers. Fidelity also has a Bitcoin ETF application. \n\tCNN says \"The spot Bitcoin ETF could quickly reach your 401(k) retirement plan...It\u2019s a big step toward mainstream adoption of bitcoin.\" \ud83d\ude80\n\nBarry Silbert has resigned as the chairman of Grayscale Investments. He will be replaced by Mark Shifke, the current Chief Financial Officer of Digital Currency Group (DCG), effective January 1. This change was reported in a recent SEC filing by Grayscale. The company is currently awaiting a decision from the Securities and Exchange Commission regarding the transformation of its Bitcoin Trust into a U.S. spot exchange-traded fund (ETF). Additionally, Mark Murphy, DCG's president, also resigned from the board. This move comes amid ongoing legal challenges faced by DCG, including a lawsuit from the New York Attorney General for alleged investor fraud. For more details, you can read the full article on CoinDesk's website \u2014 https://www.coindesk.com/business/2023/12/26/barry-silbert-resigns-as-grayscale-chairman-to-be-replaced-by-mark-shifke/\n\nCathie Wood Bitcoin Prediction: \"People Have NO IDEA What's Coming\u2026\" \u2014 $700 Billion Bitcoin market cap right now. Prediction: $20 trillion by 2030, which is about 30x from today. That would make 1 BTC = around $1.2 Million dollars. VIDEO \u2014 https://youtu.be/f7IB6V4UiA4\n\nAn amazing visual comparison between the evolution of the price of Gold and Bitcoin since 2014. #Bitcoin or Gold? --- https://twitter.com/TheBTCTherapist/status/1739668557389070654\n\nFrustrating facts \u2014 Lopsided inflation where regular people always lose (Opt out with #Bitcoin) --- https://twitter.com/_sriramk_/status/1739692573998875114\n\nThe Nostr Report: By plebs, for plebs. A synopsis of the best content of the day. Don\u2019t miss a beat. \u2014 https://Nostr.Report\n\nOpenSats onboards Tobin Harding --- https://primal.net/e/note1c2fnn5mvmtwngxz23auq8hmemwhlsms9ae6k4stel9utn4qjxgjqtd9h4h \u2014 OpenSats is a non-profit organization dedicated to supporting open-source Bitcoin development and other related projects. Its primary mission is to provide financial assistance and resources to developers and teams working on open-source projects that contribute to the Bitcoin ecosystem. The organization operates on a transparent funding model, allowing donors to see exactly where their contributions are going. OpenSats focuses on funding initiatives that enhance the security, scalability, and usability of Bitcoin, with an emphasis on projects that remain open and accessible to the public. By supporting these endeavors, OpenSats aims to strengthen the Bitcoin network and ensure its continuous improvement, while fostering a community of developers committed to the principles of open-source software.", + "created_at": 1703627501, + "id": "35ff530dc21630e85b61ee817bb9d48de07638c36d961d628974d0508d7f83ed", "kind": 1, - "pubkey": "50c5c98ccc31ca9f1ef56a547afc4cb48195fe5603d4f7874a221db965867c8e", - "sig": "8c2450128c786b30beb83b1bfaa288e8de5a553e325541a9271741e8b01b9bd122b3ebf4bc56237ebd1d6aaa44ededb043c6db19dde18ed40af2453b0edeca4b", + "pubkey": "90aaac10e033b4d1e607d426fc0eaabb9caa86720421dad8ae30182dd3efdb01", + "sig": "f874fd91c0ce8ee8ef19d06573fb2ac2b873ff36924e52c1b302297ff606c3d85dc167a4bc76f25012a214bdc4e8f02f247db9fe9ff5de022b60ce826ff14e57", "tags": [ [ "a", - "34550:f4db5270bd991b17bea1e6d035f45dee392919c29474bbac10342d223c74e0d0:Art" + "34550:5c50661b092cc786b55cb5d69149b129bdcc8e05d7238cc653de9f465ed69f91:fractalbitcoin" ], [ "r", - "https://cdn.satellite.earth/4da669a843213de2f1c4a28ae28e10edcbe646134a19c7eaf183c22a21889b7b.jpg" + "https://youtu.be/8OvAm_xPM1I" ], [ "subject", - "CyberHornet of Bitcoin" + "Today's Bitcoin News! (10 min. video) \"Bitcoin ETF Race is ON, $1.2 Million per Bitcoin?, #Bitcoin vs Gold, OpenSats onboards Tobin Harding #Nostr #BTC\"" ], [ "t", @@ -1079,6 +1073,22 @@ [ "t", "bitcoin" + ], + [ + "t", + "Nostr" + ], + [ + "t", + "nostr" + ], + [ + "t", + "BTC" + ], + [ + "t", + "btc" ] ] } @@ -1096,407 +1106,71 @@ { "events": [ { - "content": "i did the opposite. made my display name refer to my npub's last characters lol", - "created_at": 1780026498, - "id": "00005c760c62babe02476b1823cb8b313dc05fdb76216a80ab0bcbc3a869aa95", + "content": "Nostr Wiki\n\nEstive sumido do #Nostr por um tempo, mas durante este tempo tamb\u00e9m estivo o mais pr\u00f3ximo do protocolo que algu\u00e9m pode estar... at\u00e9 domingo publico que o projeto que estou finalizando para o Nostr, e sem sombra de d\u00favidas est\u00e1 espetacular, estou muito animado.\n\nBom dia meus caros!!", + "created_at": 1758286903, + "id": "ae298ef12b30dcf70f57655eb27f3e44dae390f75756aa63302e675a0d113c15", "kind": 1, - "pubkey": "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331", - "sig": "2d5021debe899883103afc527b79917f10ce7051dca9f3a99794417aaa5a2846f347afc52e1bad5d7a0ab1950bdf2fd74a01894a1c97d7eebd51a4f0441e6f0e", + "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", + "sig": "c76edad7378dc377c95378e2b73d38478ce1ed78605ae9f8945578da43b2bab45349f5d9a947aea8050c6b030d3438d18e7ab65e6e3c5464457399dd4de94c32", "tags": [ [ - "e", - "e8dd3873f90a5dfc6a1e7ce06bb404040ab2e415b32994d9b80028d523b08ba7", - "wss://relay.nostr.band", - "root" + "a", + "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" ], [ - "p", - "c3c7122c63af5281fee3b9338d10ae65b166ffe6f73f1c272c5c1120607731d7" + "subject", + "Nostr Wiki" ], [ - "client", - "Wisp" + "t", + "Nostr" ], [ - "nonce", - "47903", - "16" + "t", + "nostr" ] ] }, { - "content": "Added also the nostr:nprofile1qqs9wj8mumkqgslplpdkvdgla8xzw9cpfnun3txfdrnmyrysnxqzg5cpzdmhxue69uhhyetvv9ujuerp0yhxzee03paeqn header to the latest banners. #grownostr #nostr \n\nhttps://www.bitpopart.com/banners\nhttps://blossom.primal.net/5058d980419c8fc4554360ae1c4f6df63868be8458f32dd7e5a8548909c122f6.png\nnostr:nevent1qqs2pv36kdpnmx5whwc86z0mavenycc935ufaekrdzxq9a500rntnss24qc9d", - "created_at": 1779970864, - "id": "2e7e2865e5ccfc9a4b38296ae59d2aba97bc634ea457b1258506b3f6d981f296", + "content": "To add to what nostr:nprofile1qqs06gywary09qmcp2249ztwfq3ue8wxhl2yyp3c39thzp55plvj0sgprdmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdakj7qguwaehxw309a6xsetrd96xzer9dshxummnw3erztnrdakj7qgmwaehxw309amksetpwshxsctswpuhgctkv4exutnrduhsw0qlr4 said, Aedile is intended to be a fast, efficient, and stable set of code that other developers can use to bootstrap Nostr apps.\n\nThere are a few things every Nostr clients needs to do, like connect to relays, handle user login, sign events, publish them, and search. We aim to make Aedile the best at all of those so it can be a firm foundation for future Nostr apps.", + "created_at": 1735859178, + "id": "45da80e4cdd89c184f76b3f54452a98bc605adbabf48915a8a8b2eb9888f0546", "kind": 1, - "pubkey": "43baaf0c28e6cfb195b17ee083e19eb3a4afdfac54d9b6baf170270ed193e34c", - "sig": "e301be97f1a8d7e0ab27b25b1190cb012a71f5ad8c364adff5c0c0b492d4343a41ef4c26fb7973543aeceaf607ec6955e4d05967b79850f321eac347c50f7788", + "pubkey": "70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9", + "sig": "f9a86c7dee312817f1d39e3b166e792585e6b736f1712ad00fa1da6eb4798a54131358b4483f5893130081bb7f15e4e7dfa06c5f15c800e246492da453cdc7f1", "tags": [ [ "e", - "a0b23ab3433d9a8ebbb07d09fbeb333263058d389ee6c3688c02f68f78e6b9c2", + "dff2cb2931ea98da9dac8ac5441c84294ba667dd106fa48e65321cfaad9f0c52", + "", + "root" + ], + [ + "e", + "ff776994c5d217ea538c6752e64cd71d1721c459b5c5a2bc86036861511cad8b" + ], + [ + "e", + "f785909a94e788b03d74801c96b319849124cda6929c77f5149466b74c38b5cb", + "", + "reply" + ], + [ + "p", + "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", "", "mention" ], [ "p", - "5748fbe6ec0443e1f85b66351fe9cc2717014cf938acc968e7b20c9099802453", - "wss://relay.day.ag/", - "mention" - ], - [ - "t", - "grownostr" - ], - [ - "t", - "nostr" - ], - [ - "imeta", - "url https://blossom.primal.net/5058d980419c8fc4554360ae1c4f6df63868be8458f32dd7e5a8548909c122f6.png", - "m png", - "dim 1758.0x1560.0" - ] - ] - }, - { - "content": "GM\nnostr:nevent1qqs2tn7tyvfap8xh8g2m05n4x36rqxfk6905gdxyf9y3dn6yxphld3spzamhxue69uhhyetvv9ujuurjd9kkzmpwdejhgtczyq3azthcw509acn8ld35r47yrw2rfgdenp57qgfwkdx4d2akkyhg5qcyqqqqqqggk8acz", - "created_at": 1778568024, - "id": "605afbb176da20c96970b3802a4675850374ed7c399a9da76918ee81f786c99d", - "kind": 1, - "pubkey": "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "sig": "de85b213cbf6835fedb30ed2652609a8adc77d0de22cb72e1829ef2ed278902272f7850c8591271d662f6f809075f7f3c4cc58c03ac9f24225f9eab9bc6f6c74", - "tags": [ - [ - "alt", - "A short note: GM\nnostr:nevent1qqs2tn7tyvfap8xh8g2m05n4x36rqxfk69..." + "5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735" ], [ "p", - "23d12ef8751e5ee267fb6341d7c41b9434a1b99869e0212eb34d56abb6b12e8a", - "wss://relay.damus.io/" - ], - [ - "zap", - "23d12ef8751e5ee267fb6341d7c41b9434a1b99869e0212eb34d56abb6b12e8a", - "wss://relay.damus.io/", - "0.9" - ], - [ - "zap", - "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "wss://nos.lol/", - "0.1" - ], - [ - "client", - "Amethyst" + "75d12141a391994c4062501f155c2e34e41a8c3cf9bef9e4d7d298253458d1e2" ] ] }, - { - "content": "I just hosted a website over bluetooth...\n\nUsing nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qpqy0gja7r4re0wyelmvdqa03qmjs62rwvcd8szzt4nf4t2hd43969qw7vhfa at #soveng\n\nCC: nostr:nprofile1qyt8wumn8ghj7etyv4hzumn0wd68ytnvv9hxgtcpzemhxue69uhks6tnwshxummnw3ezumrpdejz7qpq2rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sugh36r wen bitchat over fips...?\n https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png \n\n", - "created_at": 1775842974, - "id": "53f61389e137c717f84fc7fc8e98ed5c2d9b2ef89a9a74310cdb09d902974baa", - "kind": 1, - "pubkey": "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "sig": "b501f64195814b04d132bfbbf48f3a963b0a3aa747e6f2940a8ad36671eb5b9e58221769976245f383e73d96a1c8ccb4a43510268d2651c3b22bf53213f4de50", - "tags": [ - [ - "t", - "soveng" - ], - [ - "p", - "23d12ef8751e5ee267fb6341d7c41b9434a1b99869e0212eb34d56abb6b12e8a", - "wss://nos.lol/", - "mention" - ], - [ - "p", - "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", - "wss://eden.nostr.land/", - "mention" - ], - [ - "r", - "wss://nos.lol/" - ], - [ - "r", - "wss://relay.primal.net/" - ], - [ - "r", - "wss://relay.damus.io/", - "write" - ], - [ - "r", - "wss://nostr.wine/", - "write" - ], - [ - "r", - "wss://npub1spxdug4m3y24hpx5crm0el4zhkk0wafs8kp6m0xu0wecygqej2xqq8gyhx.fips.network/" - ], - [ - "imeta", - "url https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png", - "m image/png", - "dim 1634x1098", - "service nip96" - ], - [ - "client", - "Primal Web" - ] - ] - }, - { - "content": "Nostr Wiki\n\nEstive sumido do #Nostr por um tempo, mas durante este tempo tamb\u00e9m estivo o mais pr\u00f3ximo do protocolo que algu\u00e9m pode estar... at\u00e9 domingo publico que o projeto que estou finalizando para o Nostr, e sem sombra de d\u00favidas est\u00e1 espetacular, estou muito animado.\n\nBom dia meus caros!!", - "created_at": 1758286903, - "id": "ae298ef12b30dcf70f57655eb27f3e44dae390f75756aa63302e675a0d113c15", - "kind": 1, - "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", - "sig": "c76edad7378dc377c95378e2b73d38478ce1ed78605ae9f8945578da43b2bab45349f5d9a947aea8050c6b030d3438d18e7ab65e6e3c5464457399dd4de94c32", - "tags": [ - [ - "a", - "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" - ], - [ - "subject", - "Nostr Wiki" - ], - [ - "t", - "Nostr" - ], - [ - "t", - "nostr" - ] - ] - }, - { - "content": "Acabei de finalizar a publica\u00e7\u00e3o da api para a search engine de buscas no #nostr. Voc\u00ea pode buscar relays, usu\u00e1rios, pegar um usu\u00e1rio espec\u00edfico, pesqusiar no ecopo de amigos de um usu\u00e1rio(follows) e por ai vai, teste os endpoints.\n\nSe voc\u00ea tem um cliente nostr ou est\u00e1 desenvolvendo um, teste e user como desejar, est\u00e1 100% aberta: \n\nhttps://find.nosbook.org/\n\nLembrando que pubkey sempre \u00e9 tratada no formato hexadecimal, que \u00e9 como os clientes de fato utilizam por baixo dos panos, ent\u00e3o onde recebe o par\u00e2metro pubkey, \u00e9 o hexadecimal da chave p\u00fablica, acessando o link voc\u00ea pode testar direto na p\u00e1gina web da documenta\u00e7\u00e3o! \n\nDivirta-se!!!", - "created_at": 1728191395, - "id": "7cd323a4f0e21d215b6448e2d0700219fdb1466e2239d87618daf381885d4d93", - "kind": 1, - "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", - "sig": "0a909b420af53e20c700384cc62294199006ca30e5a3e556d5f6359e2e789aa5ac3a1e2c6b6f0649f4e9164161ee5b9cd54b5187557077ad097de51f0384577a", - "tags": [ - [ - "t", - "nostr" - ], - [ - "a", - "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" - ], - [ - "subject", - "API Search Engine Nostr" - ], - [ - "client", - "noStrudel", - "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546" - ] - ] - }, - { - "content": "Still want to see a good pinterest, instagram and tiktok competitor on nostr. There's so much value in visual content and we haven't even bothered scratching that surface. \n\nImagine the interoperability of a curation platform with a shopping platform, both contributing to the creator's sovereign identity vs fighting against one another. \n\nMassive potential. ", - "created_at": 1724727720, - "id": "a42cc0bb299bbce89e5ea77b797a2126a2f149f8a22ab665d14dc9187c1e3cfc", - "kind": 1, - "pubkey": "1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411", - "sig": "051be0cfd325158e5fbae60d7a1f4deadc409677f507166774130fea51acd952e115a8a889b2b7ed13b9ef9d6587428b0b8fc76b6aa09db3085f133e1e84acc1", - "tags": [] - }, - { - "content": "MORE CLIENTS SHOULD SUPPORT DISPOSABLE NPUBS. AMETHYST IS LEADING THE WAY HERE. WOULD BE THE SINGLE BIGGEST PRIVACY WIN FOR ONLINE SPEECH ACROSS THE BOARD.\n\nNORMALIZE SINGLE USE DISPOSABLE IDENTITIES. AS THE REST OF THE INTERNET MOVES TOWARD LINKING EVERY ACTION TO LEGAL IDENTITIES, NOSTR REPRESENTS A BRIGHTER PATH. ", - "created_at": 1723216387, - "id": "529a990178f65ec0669132589d6cf1f586db6c4e133ea94d664a6a719f215e68", - "kind": 1, - "pubkey": "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", - "sig": "a0cd66e5873e57ae11889d097a64e1b3e24bc8af7858bffd48b26e5a128d4707ae8c2247358c0af4daaa42a655851b82198855d2fdef6ce1cd1b8e72e85d9124", - "tags": [] - } - ], - "flags": [ - "--kind", - "1", - "--since", - "1700000000" - ], - "name": "window_since", - "search": "nostr include:spam", - "terms": "nostr" - }, - { - "events": [ - { - "content": "i did the opposite. made my display name refer to my npub's last characters lol", - "created_at": 1780026498, - "id": "00005c760c62babe02476b1823cb8b313dc05fdb76216a80ab0bcbc3a869aa95", - "kind": 1, - "pubkey": "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331", - "sig": "2d5021debe899883103afc527b79917f10ce7051dca9f3a99794417aaa5a2846f347afc52e1bad5d7a0ab1950bdf2fd74a01894a1c97d7eebd51a4f0441e6f0e", - "tags": [ - [ - "e", - "e8dd3873f90a5dfc6a1e7ce06bb404040ab2e415b32994d9b80028d523b08ba7", - "wss://relay.nostr.band", - "root" - ], - [ - "p", - "c3c7122c63af5281fee3b9338d10ae65b166ffe6f73f1c272c5c1120607731d7" - ], - [ - "client", - "Wisp" - ], - [ - "nonce", - "47903", - "16" - ] - ] - }, - { - "content": "I just hosted a website over bluetooth...\n\nUsing nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qpqy0gja7r4re0wyelmvdqa03qmjs62rwvcd8szzt4nf4t2hd43969qw7vhfa at #soveng\n\nCC: nostr:nprofile1qyt8wumn8ghj7etyv4hzumn0wd68ytnvv9hxgtcpzemhxue69uhks6tnwshxummnw3ezumrpdejz7qpq2rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sugh36r wen bitchat over fips...?\n https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png \n\n", - "created_at": 1775842974, - "id": "53f61389e137c717f84fc7fc8e98ed5c2d9b2ef89a9a74310cdb09d902974baa", - "kind": 1, - "pubkey": "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "sig": "b501f64195814b04d132bfbbf48f3a963b0a3aa747e6f2940a8ad36671eb5b9e58221769976245f383e73d96a1c8ccb4a43510268d2651c3b22bf53213f4de50", - "tags": [ - [ - "t", - "soveng" - ], - [ - "p", - "23d12ef8751e5ee267fb6341d7c41b9434a1b99869e0212eb34d56abb6b12e8a", - "wss://nos.lol/", - "mention" - ], - [ - "p", - "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", - "wss://eden.nostr.land/", - "mention" - ], - [ - "r", - "wss://nos.lol/" - ], - [ - "r", - "wss://relay.primal.net/" - ], - [ - "r", - "wss://relay.damus.io/", - "write" - ], - [ - "r", - "wss://nostr.wine/", - "write" - ], - [ - "r", - "wss://npub1spxdug4m3y24hpx5crm0el4zhkk0wafs8kp6m0xu0wecygqej2xqq8gyhx.fips.network/" - ], - [ - "imeta", - "url https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png", - "m image/png", - "dim 1634x1098", - "service nip96" - ], - [ - "client", - "Primal Web" - ] - ] - }, - { - "content": "Nostr Wiki\n\nEstive sumido do #Nostr por um tempo, mas durante este tempo tamb\u00e9m estivo o mais pr\u00f3ximo do protocolo que algu\u00e9m pode estar... at\u00e9 domingo publico que o projeto que estou finalizando para o Nostr, e sem sombra de d\u00favidas est\u00e1 espetacular, estou muito animado.\n\nBom dia meus caros!!", - "created_at": 1758286903, - "id": "ae298ef12b30dcf70f57655eb27f3e44dae390f75756aa63302e675a0d113c15", - "kind": 1, - "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", - "sig": "c76edad7378dc377c95378e2b73d38478ce1ed78605ae9f8945578da43b2bab45349f5d9a947aea8050c6b030d3438d18e7ab65e6e3c5464457399dd4de94c32", - "tags": [ - [ - "a", - "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" - ], - [ - "subject", - "Nostr Wiki" - ], - [ - "t", - "Nostr" - ], - [ - "t", - "nostr" - ] - ] - }, - { - "content": "Acabei de finalizar a publica\u00e7\u00e3o da api para a search engine de buscas no #nostr. Voc\u00ea pode buscar relays, usu\u00e1rios, pegar um usu\u00e1rio espec\u00edfico, pesqusiar no ecopo de amigos de um usu\u00e1rio(follows) e por ai vai, teste os endpoints.\n\nSe voc\u00ea tem um cliente nostr ou est\u00e1 desenvolvendo um, teste e user como desejar, est\u00e1 100% aberta: \n\nhttps://find.nosbook.org/\n\nLembrando que pubkey sempre \u00e9 tratada no formato hexadecimal, que \u00e9 como os clientes de fato utilizam por baixo dos panos, ent\u00e3o onde recebe o par\u00e2metro pubkey, \u00e9 o hexadecimal da chave p\u00fablica, acessando o link voc\u00ea pode testar direto na p\u00e1gina web da documenta\u00e7\u00e3o! \n\nDivirta-se!!!", - "created_at": 1728191395, - "id": "7cd323a4f0e21d215b6448e2d0700219fdb1466e2239d87618daf381885d4d93", - "kind": 1, - "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", - "sig": "0a909b420af53e20c700384cc62294199006ca30e5a3e556d5f6359e2e789aa5ac3a1e2c6b6f0649f4e9164161ee5b9cd54b5187557077ad097de51f0384577a", - "tags": [ - [ - "t", - "nostr" - ], - [ - "a", - "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" - ], - [ - "subject", - "API Search Engine Nostr" - ], - [ - "client", - "noStrudel", - "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546" - ] - ] - }, - { - "content": "Still want to see a good pinterest, instagram and tiktok competitor on nostr. There's so much value in visual content and we haven't even bothered scratching that surface. \n\nImagine the interoperability of a curation platform with a shopping platform, both contributing to the creator's sovereign identity vs fighting against one another. \n\nMassive potential. ", - "created_at": 1724727720, - "id": "a42cc0bb299bbce89e5ea77b797a2126a2f149f8a22ab665d14dc9187c1e3cfc", - "kind": 1, - "pubkey": "1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411", - "sig": "051be0cfd325158e5fbae60d7a1f4deadc409677f507166774130fea51acd952e115a8a889b2b7ed13b9ef9d6587428b0b8fc76b6aa09db3085f133e1e84acc1", - "tags": [] - }, - { - "content": "MORE CLIENTS SHOULD SUPPORT DISPOSABLE NPUBS. AMETHYST IS LEADING THE WAY HERE. WOULD BE THE SINGLE BIGGEST PRIVACY WIN FOR ONLINE SPEECH ACROSS THE BOARD.\n\nNORMALIZE SINGLE USE DISPOSABLE IDENTITIES. AS THE REST OF THE INTERNET MOVES TOWARD LINKING EVERY ACTION TO LEGAL IDENTITIES, NOSTR REPRESENTS A BRIGHTER PATH. ", - "created_at": 1723216387, - "id": "529a990178f65ec0669132589d6cf1f586db6c4e133ea94d664a6a719f215e68", - "kind": 1, - "pubkey": "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", - "sig": "a0cd66e5873e57ae11889d097a64e1b3e24bc8af7858bffd48b26e5a128d4707ae8c2247358c0af4daaa42a655851b82198855d2fdef6ce1cd1b8e72e85d9124", - "tags": [] - }, { "content": "May 6, 2023 I started on Nostr. I kinda figured it might just be something and took a moment to blog it with a timestamp: https://hive.blog/bitcoin/@crrdlx/running-nostr\n\nSo, a couple of ways to look at today: \n1. Today is my first Nostr birthday or \"hatchday\" - I knew nothing of Nostr then, but have learned a ton since.\n2. Today starts year number two - Still learning every day and looking forward to what might come next.\n\n https://image.nostr.build/38697a68d75aebf0429085e3b771934e7250b4f976ac5112a22bb592375458d8.gif\n#nostr #plebchain", "created_at": 1714996203, @@ -1552,6 +1226,397 @@ "nostr" ] ] + }, + { + "content": "The Nostr Wishlist is growing. What else?\nhttp://nostrwiki.crrdlx.infinityfreeapp.com/doku.php?id=wiki:nostr-wishlist\n https://image.nostr.build/db84fca117575ae9b4062450cd69b05c1a80348de61e317d2256f000d2998305.png\n#nostr #grownostr", + "created_at": 1714651719, + "id": "f969e3f189cc5c6b1b64fbdbbb2cb65a07137d147601caf7bfa915961ae13e40", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "db81b8fa7d8a10d8e0ff7cec82ebbc31a0e9523ee7afa4dccc466359c35a5ced748f8fcac529105db0829540536cbdc09e2296a5f588d3559e52f29a15b12f72", + "tags": [ + [ + "t", + "nostr" + ], + [ + "t", + "grownostr" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "Nostr Wishlist" + ] + ] + }, + { + "content": "Putting this out again...don't know why, but somehow I think it's important. Who can figure this out?\n----\nI don't know if it's possible, but it certainly _feels_ possible. Can there be NFTs on Nostr?\n\nhttps://files.peakd.com/file/peakd-hive/crrdlx/23wXQUJXU4KEFX3rg8VusMYEhYF8NYKcY991K1zzqDyq6oTgBDPrs83m48DNn9rzp57c3.png\n\nI've written and pondered on NFTs a few times and come up with a definition that I think works in laying out exactly what an NFT actually is. I defined NFTs as Verified Unique Ownership (VUO for short). (See NFTs defined in three words | PeakD and ponderings at PeakD)\n\nThe upshot of VUO is this:\n\nV = verified = The NFT (which is nothing more than metadata such as a custom JSON) is openly visible to all to verify and cannot be altered.\nU = unique = The NFT/metadata is unique. Although a monkey jpeg may be the same for all 1,000 mints, the owner of mint #1 will definitely not trade with the owner of mint #859.\nO = ownership = The NFT can be transferred to another person and that transfer can also be verified.\n\nWith regards to Nostr, the V, the verified, seems to be the challenge. It seems that the hang-up would be persistence (or lack of persistence) in data. That is, having the metadata persistently stored on Nostr may be a high hurdle. It may be impossible. For instance, if I minted an NFT and the metadata got stored on several Nostr relays, it seems that would work. The data can be verified by others. It's persistently verifiable until...those relays go down or close up shop or purge that metadata note. Then, that NFT is gone. This, of course, differs from a blockchain.\n\nRegarding the O of ownership, I'm really not sure how that would transpire on Nostr. But, I think clever people could figure that out. If the persistent V of verifiability were solved, then a persistent alteration should also be possible to track on the \"Nostr blockchain\", the \"nostrchain.\"\n\nIf this were possible...what would be the use cases? I really don't know. I do think they would emerge though. Being around Nostr suggests to me that the fun-factor would be off-the-charts though. There are some clever and comical people in this space. Fun, is an important use-case in my view, by the way!\n\nWen NFTs on #Nostr?\n#plebchain", + "created_at": 1714425742, + "id": "dab6aebcf29ed7c6dbcfd539e53d6eb7ce6b93a83542ebfb629804f1f834f136", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "c0727a79b33c64111e5525ae72843077154c0d20d0057b58ee0feeef7c9b55b7c8a09591eee11866c3ced4ce0334ae621c56567b59d7a891a53756c2f8d0b5a5", + "tags": [ + [ + "t", + "1" + ], + [ + "t", + "859" + ], + [ + "t", + "nostr" + ], + [ + "t", + "plebchain" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "NFTs on Nostr" + ] + ] + }, + { + "content": "First Meme of NOSTR\n\nhttps://image.nostr.build/f7366c61d378ec12040f52f4382f48a2aa3d1dd2bf59c1b803c20b1a8d3eddbf.jpg\n\n#btc #plebchain #cryptoterrestrial #nostr #grownostr", + "created_at": 1712797491, + "id": "a8b24e884774cda42c0f01d0a3b52012aa00bd3d58479f4c6983bcca76b71f93", + "kind": 1, + "pubkey": "2cd0d8fd7003efdb16aa1a82ce25881b20de1f99b99a688cf03db34c85567da7", + "sig": "73ad64f13bd9aca844a0b93e37cfa477cf39042d1c75e345587a9c6b422c7ed65f514c1ebc8c13a002f48a2c815236d0cfc8c76a26878fae720f266ea633103e", + "tags": [ + [ + "a", + "34550:2cd0d8fd7003efdb16aa1a82ce25881b20de1f99b99a688cf03db34c85567da7:digitalarts" + ], + [ + "r", + "https://image.nostr.build/f7366c61d378ec12040f52f4382f48a2aa3d1dd2bf59c1b803c20b1a8d3eddbf.jpg" + ], + [ + "subject", + "First Meme of NOSTR" + ], + [ + "t", + "btc" + ], + [ + "t", + "plebchain" + ], + [ + "t", + "cryptoterrestrial" + ], + [ + "t", + "nostr" + ], + [ + "t", + "grownostr" + ] + ] + }, + { + "content": "Discovering #nostr feels amazing. From the surface it seems easy but with what you can build upon and using this protocol..well seems a bit overwhelming. Understanding how it works and learning about it is fascinating to say the least.\nDecentralized access to information that is censorship resistant..feed curation, lists of people you want to interact with. This is next level and with integration of #bitcoin Lightning payments..future is bright. Glad to be here in such an early stage. \n\nthanks nostr:npub1f6ugxyxkknket3kkdgu4k0fu74vmshawermkj8d06sz6jts9t4kslazcka for guidance and patience.\ud83e\udd0c", + "created_at": 1711409590, + "id": "9442ca1e321f583c3ed1eace48de854c1b55828257d2e2e1a08ffea95dd5159a", + "kind": 1, + "pubkey": "18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef", + "sig": "21bdd10997cf217004e1532a77e6471e207246fc18d192d36253627c6935cdd618404a89714144f4edd2f52a1ef752b9d935033a481c72bda46deb33f0338a0d", + "tags": [ + [ + "subject", + "Discovering #NOSTR" + ], + [ + "p", + "4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d" + ], + [ + "t", + "nostr" + ], + [ + "t", + "bitcoin" + ] + ] + } + ], + "flags": [ + "--kind", + "1", + "--since", + "1700000000" + ], + "name": "window_since", + "search": "nostr include:spam", + "terms": "nostr" + }, + { + "events": [ + { + "content": "Nostr Wiki\n\nEstive sumido do #Nostr por um tempo, mas durante este tempo tamb\u00e9m estivo o mais pr\u00f3ximo do protocolo que algu\u00e9m pode estar... at\u00e9 domingo publico que o projeto que estou finalizando para o Nostr, e sem sombra de d\u00favidas est\u00e1 espetacular, estou muito animado.\n\nBom dia meus caros!!", + "created_at": 1758286903, + "id": "ae298ef12b30dcf70f57655eb27f3e44dae390f75756aa63302e675a0d113c15", + "kind": 1, + "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", + "sig": "c76edad7378dc377c95378e2b73d38478ce1ed78605ae9f8945578da43b2bab45349f5d9a947aea8050c6b030d3438d18e7ab65e6e3c5464457399dd4de94c32", + "tags": [ + [ + "a", + "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" + ], + [ + "subject", + "Nostr Wiki" + ], + [ + "t", + "Nostr" + ], + [ + "t", + "nostr" + ] + ] + }, + { + "content": "To add to what nostr:nprofile1qqs06gywary09qmcp2249ztwfq3ue8wxhl2yyp3c39thzp55plvj0sgprdmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdakj7qguwaehxw309a6xsetrd96xzer9dshxummnw3erztnrdakj7qgmwaehxw309amksetpwshxsctswpuhgctkv4exutnrduhsw0qlr4 said, Aedile is intended to be a fast, efficient, and stable set of code that other developers can use to bootstrap Nostr apps.\n\nThere are a few things every Nostr clients needs to do, like connect to relays, handle user login, sign events, publish them, and search. We aim to make Aedile the best at all of those so it can be a firm foundation for future Nostr apps.", + "created_at": 1735859178, + "id": "45da80e4cdd89c184f76b3f54452a98bc605adbabf48915a8a8b2eb9888f0546", + "kind": 1, + "pubkey": "70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9", + "sig": "f9a86c7dee312817f1d39e3b166e792585e6b736f1712ad00fa1da6eb4798a54131358b4483f5893130081bb7f15e4e7dfa06c5f15c800e246492da453cdc7f1", + "tags": [ + [ + "e", + "dff2cb2931ea98da9dac8ac5441c84294ba667dd106fa48e65321cfaad9f0c52", + "", + "root" + ], + [ + "e", + "ff776994c5d217ea538c6752e64cd71d1721c459b5c5a2bc86036861511cad8b" + ], + [ + "e", + "f785909a94e788b03d74801c96b319849124cda6929c77f5149466b74c38b5cb", + "", + "reply" + ], + [ + "p", + "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", + "", + "mention" + ], + [ + "p", + "5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735" + ], + [ + "p", + "75d12141a391994c4062501f155c2e34e41a8c3cf9bef9e4d7d298253458d1e2" + ] + ] + }, + { + "content": "May 6, 2023 I started on Nostr. I kinda figured it might just be something and took a moment to blog it with a timestamp: https://hive.blog/bitcoin/@crrdlx/running-nostr\n\nSo, a couple of ways to look at today: \n1. Today is my first Nostr birthday or \"hatchday\" - I knew nothing of Nostr then, but have learned a ton since.\n2. Today starts year number two - Still learning every day and looking forward to what might come next.\n\n https://image.nostr.build/38697a68d75aebf0429085e3b771934e7250b4f976ac5112a22bb592375458d8.gif\n#nostr #plebchain", + "created_at": 1714996203, + "id": "d31162a39b411614b9093e060d7cb4f4366cd38b2c9ca1727cdc02151446c814", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "893d797f5d52f1b92f4a4413c130cd36d976ee5c6a4eed1f90853fc4367f0590053070c1bfed703ac387f7c8c758ef40d643aa89b8fa0ce656d51da1194fc8d9", + "tags": [ + [ + "t", + "nostr" + ], + [ + "t", + "plebchain" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "A year on Nostr" + ] + ] + }, + { + "content": "#Nostr Fan Art\n\nhttps://blogclif.wordpress.com/2024/05/02/nostr-as-a-better-place-to-grow/\n\nNostr is a better place to grow", + "created_at": 1714721223, + "id": "13ead8b7c5f4aca351783cb85ec4d65670d8455d9c576ee8ca57b3cf9d7a7a01", + "kind": 1, + "pubkey": "f8f6b6f741bd422346579304550de64a6445fd332c50389e9a1f4d8294a101e0", + "sig": "a8b93de6c931c97265b3cb452a6bc2463e576bed4310a4fbf19185e4845732941d654c187a48d497d014f981092c3e80a9a4ea2ce0069649fde1be33bcbee727", + "tags": [ + [ + "a", + "34550:f8f6b6f741bd422346579304550de64a6445fd332c50389e9a1f4d8294a101e0:fanart" + ], + [ + "r", + "https://blogclif.wordpress.com/2024/05/02/nostr-as-a-better-place-to-grow/" + ], + [ + "subject", + "#Nostr Fan Art" + ], + [ + "t", + "Nostr" + ], + [ + "t", + "nostr" + ] + ] + }, + { + "content": "The Nostr Wishlist is growing. What else?\nhttp://nostrwiki.crrdlx.infinityfreeapp.com/doku.php?id=wiki:nostr-wishlist\n https://image.nostr.build/db84fca117575ae9b4062450cd69b05c1a80348de61e317d2256f000d2998305.png\n#nostr #grownostr", + "created_at": 1714651719, + "id": "f969e3f189cc5c6b1b64fbdbbb2cb65a07137d147601caf7bfa915961ae13e40", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "db81b8fa7d8a10d8e0ff7cec82ebbc31a0e9523ee7afa4dccc466359c35a5ced748f8fcac529105db0829540536cbdc09e2296a5f588d3559e52f29a15b12f72", + "tags": [ + [ + "t", + "nostr" + ], + [ + "t", + "grownostr" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "Nostr Wishlist" + ] + ] + }, + { + "content": "Putting this out again...don't know why, but somehow I think it's important. Who can figure this out?\n----\nI don't know if it's possible, but it certainly _feels_ possible. Can there be NFTs on Nostr?\n\nhttps://files.peakd.com/file/peakd-hive/crrdlx/23wXQUJXU4KEFX3rg8VusMYEhYF8NYKcY991K1zzqDyq6oTgBDPrs83m48DNn9rzp57c3.png\n\nI've written and pondered on NFTs a few times and come up with a definition that I think works in laying out exactly what an NFT actually is. I defined NFTs as Verified Unique Ownership (VUO for short). (See NFTs defined in three words | PeakD and ponderings at PeakD)\n\nThe upshot of VUO is this:\n\nV = verified = The NFT (which is nothing more than metadata such as a custom JSON) is openly visible to all to verify and cannot be altered.\nU = unique = The NFT/metadata is unique. Although a monkey jpeg may be the same for all 1,000 mints, the owner of mint #1 will definitely not trade with the owner of mint #859.\nO = ownership = The NFT can be transferred to another person and that transfer can also be verified.\n\nWith regards to Nostr, the V, the verified, seems to be the challenge. It seems that the hang-up would be persistence (or lack of persistence) in data. That is, having the metadata persistently stored on Nostr may be a high hurdle. It may be impossible. For instance, if I minted an NFT and the metadata got stored on several Nostr relays, it seems that would work. The data can be verified by others. It's persistently verifiable until...those relays go down or close up shop or purge that metadata note. Then, that NFT is gone. This, of course, differs from a blockchain.\n\nRegarding the O of ownership, I'm really not sure how that would transpire on Nostr. But, I think clever people could figure that out. If the persistent V of verifiability were solved, then a persistent alteration should also be possible to track on the \"Nostr blockchain\", the \"nostrchain.\"\n\nIf this were possible...what would be the use cases? I really don't know. I do think they would emerge though. Being around Nostr suggests to me that the fun-factor would be off-the-charts though. There are some clever and comical people in this space. Fun, is an important use-case in my view, by the way!\n\nWen NFTs on #Nostr?\n#plebchain", + "created_at": 1714425742, + "id": "dab6aebcf29ed7c6dbcfd539e53d6eb7ce6b93a83542ebfb629804f1f834f136", + "kind": 1, + "pubkey": "005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2", + "sig": "c0727a79b33c64111e5525ae72843077154c0d20d0057b58ee0feeef7c9b55b7c8a09591eee11866c3ced4ce0334ae621c56567b59d7a891a53756c2f8d0b5a5", + "tags": [ + [ + "t", + "1" + ], + [ + "t", + "859" + ], + [ + "t", + "nostr" + ], + [ + "t", + "plebchain" + ], + [ + "a", + "34550:005bc4de41cfcb580f71cad6ae8909a976568633a4f6a93c6b7fd5bfef11e1a2:General" + ], + [ + "subject", + "NFTs on Nostr" + ] + ] + }, + { + "content": "First Meme of NOSTR\n\nhttps://image.nostr.build/f7366c61d378ec12040f52f4382f48a2aa3d1dd2bf59c1b803c20b1a8d3eddbf.jpg\n\n#btc #plebchain #cryptoterrestrial #nostr #grownostr", + "created_at": 1712797491, + "id": "a8b24e884774cda42c0f01d0a3b52012aa00bd3d58479f4c6983bcca76b71f93", + "kind": 1, + "pubkey": "2cd0d8fd7003efdb16aa1a82ce25881b20de1f99b99a688cf03db34c85567da7", + "sig": "73ad64f13bd9aca844a0b93e37cfa477cf39042d1c75e345587a9c6b422c7ed65f514c1ebc8c13a002f48a2c815236d0cfc8c76a26878fae720f266ea633103e", + "tags": [ + [ + "a", + "34550:2cd0d8fd7003efdb16aa1a82ce25881b20de1f99b99a688cf03db34c85567da7:digitalarts" + ], + [ + "r", + "https://image.nostr.build/f7366c61d378ec12040f52f4382f48a2aa3d1dd2bf59c1b803c20b1a8d3eddbf.jpg" + ], + [ + "subject", + "First Meme of NOSTR" + ], + [ + "t", + "btc" + ], + [ + "t", + "plebchain" + ], + [ + "t", + "cryptoterrestrial" + ], + [ + "t", + "nostr" + ], + [ + "t", + "grownostr" + ] + ] + }, + { + "content": "Discovering #nostr feels amazing. From the surface it seems easy but with what you can build upon and using this protocol..well seems a bit overwhelming. Understanding how it works and learning about it is fascinating to say the least.\nDecentralized access to information that is censorship resistant..feed curation, lists of people you want to interact with. This is next level and with integration of #bitcoin Lightning payments..future is bright. Glad to be here in such an early stage. \n\nthanks nostr:npub1f6ugxyxkknket3kkdgu4k0fu74vmshawermkj8d06sz6jts9t4kslazcka for guidance and patience.\ud83e\udd0c", + "created_at": 1711409590, + "id": "9442ca1e321f583c3ed1eace48de854c1b55828257d2e2e1a08ffea95dd5159a", + "kind": 1, + "pubkey": "18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef", + "sig": "21bdd10997cf217004e1532a77e6471e207246fc18d192d36253627c6935cdd618404a89714144f4edd2f52a1ef752b9d935033a481c72bda46deb33f0338a0d", + "tags": [ + [ + "subject", + "Discovering #NOSTR" + ], + [ + "p", + "4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d" + ], + [ + "t", + "nostr" + ], + [ + "t", + "bitcoin" + ] + ] } ], "flags": [ @@ -1567,61 +1632,101 @@ { "events": [ { - "content": "I just hosted a website over bluetooth...\n\nUsing nostr:nprofile1qy88wumn8ghj7mn0wvhxcmmv9uq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qpqy0gja7r4re0wyelmvdqa03qmjs62rwvcd8szzt4nf4t2hd43969qw7vhfa at #soveng\n\nCC: nostr:nprofile1qyt8wumn8ghj7etyv4hzumn0wd68ytnvv9hxgtcpzemhxue69uhks6tnwshxummnw3ezumrpdejz7qpq2rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sugh36r wen bitchat over fips...?\n https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png \n\n", - "created_at": 1775842974, - "id": "53f61389e137c717f84fc7fc8e98ed5c2d9b2ef89a9a74310cdb09d902974baa", - "kind": 1, - "pubkey": "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", - "sig": "b501f64195814b04d132bfbbf48f3a963b0a3aa747e6f2940a8ad36671eb5b9e58221769976245f383e73d96a1c8ccb4a43510268d2651c3b22bf53213f4de50", + "content": "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via relays. it grew out of a small community of bitcoin/lightning devs, specifically [fiatjaf](nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6). it was a reaction to the counter-party risks in using centralized social networks (twitter, facebook, etc) and the failings of other, semi-decentralized approaches (mastodon).\n\nbitcoin familiarity helps when groking nostr. the identity portion of nostr is private/public key pair. similar to a bitcoin wallet where one uses one\u2019s private key to sign a transaction and publish it into the network, one uses a nostr private key to sign an event and publish it to relays. there are various defined [kinds](https://github.com/nostr-protocol/nips#event-kinds) of events, from profile information (name, about, url, avatar url, etc), to contact lists (follows), to notes (a short plain text message). the bitcoin network is peer to peer (distributed), where nodes will gossip transactions around to all the other nodes, but this is not the case with nostr. events stay where you publish them, relays only talk to clients, not other relays. this makes nostr decentralized, but not distributed. since it\u2019s not peer-to-peer, one client doesn\u2019t need to see the entire traffic of the network, or to query some distributed hash table a la IPFS, to find content across the network. this middle ground between centralized and distributed allows snappy response times yet prevents the domination of network lock-in.", + "created_at": 1782172014, + "id": "4c3ffd171a535f9bac1afc47526b565833c726f82e59b3bf287db3e39cafc48e", + "kind": 30023, + "pubkey": "27c66d241898300a254f0853dfbfcf735a627dc2790c45a03de04704dd54a3b4", + "sig": "cc646b6cd05fe3e1fbcc365e9a042ca0ecd0b04523d3c228ce1504dc77703cea150ebdab0e4704fe3fb9bc5d6a08acf3ca32367386cdbab4a778e18213f85ab2", "tags": [ [ - "t", - "soveng" + "title", + "nostr" ], [ - "p", - "23d12ef8751e5ee267fb6341d7c41b9434a1b99869e0212eb34d56abb6b12e8a", - "wss://nos.lol/", - "mention" + "summary", + "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via" ], [ - "p", - "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", - "wss://eden.nostr.land/", - "mention" + "published_at", + "1676764800" + ], + [ + "d", + "d4b8f38a8c6e1e9a9ecbffecb23fd7dfe7b68b18d08584cb7b09d7e858f5ebf6" ], [ "r", - "wss://nos.lol/" + "https://plantimals.org/nostr/" + ] + ] + }, + { + "content": "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via relays. it grew out of a small community of bitcoin/lightning devs, specifically [fiatjaf](nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6). it was a reaction to the counter-party risks in using centralized social networks (twitter, facebook, etc) and the failings of other, semi-decentralized approaches (mastodon).\n\nbitcoin familiarity helps when groking nostr. the identity portion of nostr is private/public key pair. similar to a bitcoin wallet where one uses one\u2019s private key to sign a transaction and publish it into the network, one uses a nostr private key to sign an event and publish it to relays. there are various defined [kinds](https://github.com/nostr-protocol/nips#event-kinds) of events, from profile information (name, about, url, avatar url, etc), to contact lists (follows), to notes (a short plain text message). the bitcoin network is peer to peer (distributed), where nodes will gossip transactions around to all the other nodes, but this is not the case with nostr. events stay where you publish them, relays only talk to clients, not other relays. this makes nostr decentralized, but not distributed. since it\u2019s not peer-to-peer, one client doesn\u2019t need to see the entire traffic of the network, or to query some distributed hash table a la IPFS, to find content across the network. this middle ground between centralized and distributed allows snappy response times yet prevents the domination of network lock-in.", + "created_at": 1782142849, + "id": "d957e3daa1b7c6275cb2335edbe67bef9e21218aaa32731f7362f2b17367e297", + "kind": 30023, + "pubkey": "8d5b9142b2021c38ecbd9fe6bead324836dad84379643507eb8a4ce58dd31feb", + "sig": "f93128dc50737a678b41d8d57c051281839325e7de6692605902d3bb718ec5a450bde6e28a4715dac852797c161ba54f96d69183a4826506ca1b7232817b07cf", + "tags": [ + [ + "title", + "nostr" + ], + [ + "summary", + "## the protocol\n\n[nostr](https://github.com/nostr-protocol/nostr) is a protocol for establishing pseudonymous identities and using them to cryptographically sign messages and pass them around via" + ], + [ + "published_at", + "1676764800" + ], + [ + "d", + "d4b8f38a8c6e1e9a9ecbffecb23fd7dfe7b68b18d08584cb7b09d7e858f5ebf6" ], [ "r", - "wss://relay.primal.net/" + "https://plantimals.org/nostr/" + ] + ] + }, + { + "content": "Join me today for Episode 758 of Bitcoin And . . . Topics for today: - nostr nostr nostr - Dito to Bring Mastodon to nostr - RFK Jr. and Sound Money - Peeps Madz at Gensler - Block Sues Visa/MC - Altszn is starting. Just Don't - Miners Selling BTC #Bitcoin #BitcoinAnd #BTC Circle P: npub: npub1ugnq57hn8va6xqr5zywy2eunem6c624583vkt0dmv40ep7tnnxkqrr898l (faster) email: shishi21m@protonmail.com (slower) Product: Comfrey One full root for 20$ Or root cutting for 1$ each Buyer pays shipping Articles: https://cointelegraph.com/news/bitcoin-miners-selling-btc-as-hedge-bitfinex https://www.coindesk.com/policy/2023/07/19/us-presidential-candidate-rfk-jr-vows-to-exempt-crypto-from-capital-gains-tax-if-elected/ https://bitcoinmagazine.com/business/blackrock-etf-would-supercharge-bitcoin-mining https://cointelegraph.com/news/lawmakers-propose-sec-chair-consider-legislation-not-enforcement-approach-crypto https://www.finextra.com/newsarticle/42653/block-sues-visa-and-mastercard-over-interchange-fees https://www.nobsbitcoin.com/simple-bitcoin-wallet-to-drop-lightning/ - https://www.cnbc.com/futures-and-commodities/ - https://bitinfocharts.com/ - https://bitcoin.clarkmoody.com/dashboard/ - https://mempool.space/ - https://fountain.fm/charts https://decrypt.co/149120/genesis-trading-saw-uptick-in-altcoin-activity-after-ripple-sec-decision https://cointelegraph.com/news/bitcoin-mining-becoming-more-environmentally-friendly https://primal.net/e/note1dnps5ceywjsx3c4dgyef660ylymudkrxc29vlxpacx5m9zghncusfjhpu3 https://www.nobsbitcoin.com/dito-bringing-best-parts-of-mastodon-to-nostr/ https://primal.net/e/note1sml79x4y6hpuqy8xr2squdztjzxa59lkmv2560vv9aqne6z3hkwq3pn7vl https://www.nobsbitcoin.com/dito-bringing-best-parts-of-mastodon-to-nostr/ https://decrypt.co/149106/openai-donates-5-million-to-the-american-journalism-project https://primal.net/p/npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk Find me on nostr npub1vwymuey3u7mf860ndrkw3r7dz30s0srg6tqmhtjzg7umtm6rn5eq2qzugd (npub) 6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32 (Hex) StackerNews: stacker.news/NunyaBidness Podcasting 2.0: fountain.fm/show/eK5XaSb3UaLRavU3lYrI Apple Podcasts: tinyurl.com/unm35bjh Instagram: instagram.com/bitcoin\\_and Mastodon: noagendasocial.com/@NunyaBidness Support Bitcoin And . . . on Patreon: patreon.com/BitcoinAndPodcast Find Lightning Network Channel partners here: https://t.me/+bj-7w\\_ePsANlOGEx (Nodestrich) https://t.me/plebnet (Plebnet) Music by: Flutey Funk Kevin MacLeod (incompetech.com) Licensed under Creative Commons: By Attribution 3.0 License creativecommons.org/licenses/by/3.0/", + "created_at": 1781766026, + "id": "aef3fd33ea4d2f7bd07e796c173a42a293ad95df140439e074af7a7f672ba030", + "kind": 30023, + "pubkey": "1ee4d19877f0f101d65c53a302126edd7cbf051eca4ef9adc47010a84931ce0c", + "sig": "a357c61d5ccefb2e8fe222abb95d27ae6a4f1265ab303cb5e58136a1fad3a52a615ab7239100a260b05baec6fe78c97cd51b24c5e72386a6c9fd47b29f0e26dd", + "tags": [ + [ + "title", + "nostr nostr nostr Ep758" + ], + [ + "summary", + "Join me today for Episode 758 of Bitcoin And . . . Topics for today: - nostr nostr nostr - Dito to Bring Mastodon to nostr - RFK Jr. and Sound Money - Peeps Madz at Gensler - Block Sues Visa/MC -" + ], + [ + "published_at", + "1689793696" + ], + [ + "d", + "nostr-nostr-nostr-ep758" ], [ "r", - "wss://relay.damus.io/", - "write" + "https://soundcloud.com/bitcoin-and/nostr-nostr-nostr-ep758" ], [ - "r", - "wss://nostr.wine/", - "write" - ], - [ - "r", - "wss://npub1spxdug4m3y24hpx5crm0el4zhkk0wafs8kp6m0xu0wecygqej2xqq8gyhx.fips.network/" + "image", + "https://assets.podhome.fm/98d1b634-3ead-490c-ade3-08dc4d9e1446/6384706134504864440x3000.jpg" ], [ "imeta", - "url https://blossom.primal.net/45d7ca76a456c8dbe32b13223340f91c65c391d08dde40ed930127acf5aa0e2d.png", - "m image/png", - "dim 1634x1098", - "service nip96" - ], - [ - "client", - "Primal Web" + "m audio/mpeg", + "url https://op3.dev/e/dts.podtrac.com/redirect.mp3/pdcn.co/e/pscrb.fm/rss/p/serve.podhome.fm/episode/98d1b634-3ead-490c-ade3-08dc4d9e1446/638470613459883050f568cac8-cc4f-4c3c-a83f-764b84cce760.mp3" ] ] }, @@ -1655,74 +1760,6 @@ ] ] }, - { - "content": "nostr.wine nostr relay.", - "created_at": 1771436582, - "id": "85fe5aa85429bb367ce0cf8f03f71f74ba83dd02f1b42f666bfb921bb07c7c86", - "kind": 30023, - "pubkey": "e938eb5077f30bd8fd0f9a0681514e52706fb231f0ac7516e3194530618addfa", - "sig": "40c7e26e24663feb4a22906d22bb424740ddf826d6c71a2a4839e8adf533f11692ad1b19c9566414849e2519b508398b7d848e63b2b9ef6631567b3242bd0460", - "tags": [ - [ - "d", - "nostr_wine" - ], - [ - "title", - "nostr.wine Nostr Relay" - ], - [ - "image", - "" - ], - [ - "published_at", - "2026-02-18 17:43:02 +0000 UTC" - ], - [ - "t", - "nostr" - ], - [ - "t", - "relay" - ] - ] - }, - { - "content": "nostr.mom nostr relay.", - "created_at": 1771436581, - "id": "d6f0e0db71f5b276e008d7285b896e069b78f0eb5da3b34fdd87c97eac66d01a", - "kind": 30023, - "pubkey": "e938eb5077f30bd8fd0f9a0681514e52706fb231f0ac7516e3194530618addfa", - "sig": "6936e469dd04081e2fd9eb79800793258d7cbae1a0f178a05d9a2480037f48ec78b9e9e3d3486ccdaa3922f29eae73a7b7734222f7a76070d3cdbc9260fe487a", - "tags": [ - [ - "d", - "nostr_mom" - ], - [ - "title", - "nostr.mom Nostr Relay" - ], - [ - "image", - "" - ], - [ - "published_at", - "2026-02-18 17:43:01 +0000 UTC" - ], - [ - "t", - "nostr" - ], - [ - "t", - "relay" - ] - ] - }, { "content": "Nostr (Notes and Other Stuff Transmitted by Relays) is a decentralized, censorship-resistant social protocol designed to give users control over their online identities and interactions.", "created_at": 1771436580, @@ -1793,39 +1830,82 @@ ] }, { - "content": "Nostr Wiki\n\nEstive sumido do #Nostr por um tempo, mas durante este tempo tamb\u00e9m estivo o mais pr\u00f3ximo do protocolo que algu\u00e9m pode estar... at\u00e9 domingo publico que o projeto que estou finalizando para o Nostr, e sem sombra de d\u00favidas est\u00e1 espetacular, estou muito animado.\n\nBom dia meus caros!!", - "created_at": 1758286903, - "id": "ae298ef12b30dcf70f57655eb27f3e44dae390f75756aa63302e675a0d113c15", + "content": "To add to what nostr:nprofile1qqs06gywary09qmcp2249ztwfq3ue8wxhl2yyp3c39thzp55plvj0sgprdmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdakj7qguwaehxw309a6xsetrd96xzer9dshxummnw3erztnrdakj7qgmwaehxw309amksetpwshxsctswpuhgctkv4exutnrduhsw0qlr4 said, Aedile is intended to be a fast, efficient, and stable set of code that other developers can use to bootstrap Nostr apps.\n\nThere are a few things every Nostr clients needs to do, like connect to relays, handle user login, sign events, publish them, and search. We aim to make Aedile the best at all of those so it can be a firm foundation for future Nostr apps.", + "created_at": 1735859178, + "id": "45da80e4cdd89c184f76b3f54452a98bc605adbabf48915a8a8b2eb9888f0546", "kind": 1, - "pubkey": "55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27", - "sig": "c76edad7378dc377c95378e2b73d38478ce1ed78605ae9f8945578da43b2bab45349f5d9a947aea8050c6b030d3438d18e7ab65e6e3c5464457399dd4de94c32", + "pubkey": "70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9", + "sig": "f9a86c7dee312817f1d39e3b166e792585e6b736f1712ad00fa1da6eb4798a54131358b4483f5893130081bb7f15e4e7dfa06c5f15c800e246492da453cdc7f1", "tags": [ [ - "a", - "34550:55472e9c01f37a35f6032b9b78dade386e6e4c57d80fd1d0646abb39280e5e27:ndevs-br" + "e", + "dff2cb2931ea98da9dac8ac5441c84294ba667dd106fa48e65321cfaad9f0c52", + "", + "root" ], [ - "subject", - "Nostr Wiki" + "e", + "ff776994c5d217ea538c6752e64cd71d1721c459b5c5a2bc86036861511cad8b" ], [ - "t", - "Nostr" + "e", + "f785909a94e788b03d74801c96b319849124cda6929c77f5149466b74c38b5cb", + "", + "reply" ], [ - "t", - "nostr" + "p", + "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1", + "", + "mention" + ], + [ + "p", + "5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735" + ], + [ + "p", + "75d12141a391994c4062501f155c2e34e41a8c3cf9bef9e4d7d298253458d1e2" ] ] }, { - "content": "Still want to see a good pinterest, instagram and tiktok competitor on nostr. There's so much value in visual content and we haven't even bothered scratching that surface. \n\nImagine the interoperability of a curation platform with a shopping platform, both contributing to the creator's sovereign identity vs fighting against one another. \n\nMassive potential. ", - "created_at": 1724727720, - "id": "a42cc0bb299bbce89e5ea77b797a2126a2f149f8a22ab665d14dc9187c1e3cfc", - "kind": 1, - "pubkey": "1bc70a0148b3f316da33fe3c89f23e3e71ac4ff998027ec712b905cd24f6a411", - "sig": "051be0cfd325158e5fbae60d7a1f4deadc409677f507166774130fea51acd952e115a8a889b2b7ed13b9ef9d6587428b0b8fc76b6aa09db3085f133e1e84acc1", - "tags": [] + "content": "", + "created_at": 1720461688, + "id": "cb5892408f969b7002118b108c95ebccfa094087fc4946b7f630a4da61c6848e", + "kind": 30023, + "pubkey": "726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11", + "sig": "dfb9200e48bd1b808cf61415a24b7e90ead2ea8b20f676cc131976d71125e8b2c54246a83436bfd3914153b6867d0a6ef54cbc920a340fa5fd7341b3a707cfc4", + "tags": [ + [ + "d", + "respect-the-a-context" + ], + [ + "title", + "Nostr" + ], + [ + "summary", + "Nostr" + ], + [ + "published_at", + "1696882326" + ], + [ + "t", + "nostr" + ], + [ + "published_at", + "1696882326" + ], + [ + "alt", + "This is a long form article, you can read it in https://habla.news/franzap/respect-the-a-context" + ] + ] } ], "flags": [ @@ -1867,39 +1947,6 @@ ] ] }, - { - "content": "Grok Build is Now Open Source\nExplore the harness behind our coding agent and TUI.\n\n\nnostr:naddr1qvzqqqr4gupzqfrzd2rja8n2v66f6g6g46gzske3l0fhhnpxa50h2vw6lt4xjp4gqqjxzv3sxasnjv3394jxxde3956rvvr994sn2vnr956nvdejvs6nqdmrvs6kgw83x37 ", - "created_at": 1784198593, - "id": "40d9f5c7ee4a67c5dd64d7542d8be05fcf7ddb56a1114646a7eee9f516d48fb4", - "kind": 1, - "pubkey": "24626a872e9e6a66b49d2348ae90285b31fbd37bcc26ed1f7531dafaea6906a8", - "sig": "f51de79bcfc00cab5fd98640a1fc3a9877dcb8b47c0a129ed43b91b7962e384a974842ee5031156cc7d082576c24dee6c8db6598e308f75b254d1463e99c7581", - "tags": [ - [ - "subject", - "Grok Build is Now Open Source" - ], - [ - "image", - "https://x.ai/images/news/open-source-cover.png" - ], - [ - "a", - "30023:24626a872e9e6a66b49d2348ae90285b31fbd37bcc26ed1f7531dafaea6906a8:a207a921-dc71-460e-a52c-5672d507cd5d", - "", - "mention" - ], - [ - "client", - "" - ], - [ - "p", - "24626a872e9e6a66b49d2348ae90285b31fbd37bcc26ed1f7531dafaea6906a8", - "" - ] - ] - }, { "content": "Top 10: Open Source AI Platforms\nFrom powerful frameworks to cutting-edge models, discover the top 10 open-source AI platforms that are giving developers ultimate control\n\n\nnostr:naddr1qvzqqqr4gupzq7yp3uw8pk4kj3u7mx690fenmw7wn7a3unf6l76yn2lnug4sk3xeqqjrjvf3xcenze3h94jxgdph956rqwfh95urywfc956rvvmxx4jxvc3kx3snj97cg2r ", "created_at": 1781196258, @@ -2074,6 +2121,41 @@ "Amethyst" ] ] + }, + { + "content": "He's going so hardcore cloning the repo and building from source lol", + "created_at": 1779978067, + "id": "0000dc5ab01b9a8831edd854e85de91b13fae0bde77fc64706655de3e2b38f13", + "kind": 1, + "pubkey": "e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb", + "sig": "eee7678cd75cc28b468777683d0800bdd29f4d9ace6a9b18aeed0614dc34a743edb79c1ac4fe60369becc47a3dd8585e35f2abba7384d9a5f07c689fd70cf851", + "tags": [ + [ + "e", + "00004d1e487177d4f2e18a71d8db28b8009402438c0d52789eb2132ef1e035c4", + "wss://nos.lol", + "root" + ], + [ + "e", + "00002db9957e51979b7d2450e194de9f9d515a8cefa3dd0574d1b39e311fc6e5", + "wss://relay.primal.net", + "reply" + ], + [ + "p", + "1e067bfb58820576df3daf7cb051d4411b80a0b8fa12fc253cd0ab41cf1a2069" + ], + [ + "client", + "Wisp" + ], + [ + "nonce", + "78063", + "16" + ] + ] } ], "flags": [ @@ -2086,5 +2168,5 @@ } ], "lens": "include:spam", - "relay": "wss://search-staging.brainstorm.world" + "relay": "wss://search.brainstorm.world" } From ff15246c7b4a378b7af894039bb146ea5dedc676 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 1 Oct 2026 14:37:31 +0000 Subject: [PATCH 41/45] fix: harden the title/favicon link preview fallback Audit fixes for the compact link preview: - Stop reading <title> and icon <link>s once <body> starts. </head> is optional, and without it an inline SVG's <title> ("Close", "Menu") was taken as the page title. - Never cut a long <title> inside a surrogate pair. - Match the HTML mime type case-insensitively: UrlPreview stores the server's spelling, so "Text/HTML" pages lost their icon. - Honour data: icons. An inline data:image icon (up to 64 KB) is used as is; `href="data:,"` means "no favicon", so /favicon.ico is no longer requested for those sites. - Remember icon URLs the server refused (HTTP errors only, not network errors) so a missing /favicon.ico is not re-requested every time its card scrolls back into view. - Keep the open-in-browser callback stable across recompositions. - Benchmark rows for the title/icon parse path: no measurable cost on the heavy head, ~1.5 us on a small one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdPp7UiETpzv8jQ5qfAFqp --- .../commons/preview/MetaTagsParser.kt | 26 ++++++++++++-- .../amethyst/commons/preview/UrlInfoItem.kt | 16 ++++++++- .../preview/TitleAndIconFallbackTest.kt | 34 +++++++++++++++++++ .../preview/UrlInfoItemTextPreviewTest.kt | 22 ++++++++++++ .../prodbench/MetaTagsParserBenchmark.kt | 13 +++++++ .../commons/ui/components/UrlPreviewCard.kt | 24 +++++++++++-- 6 files changed, 129 insertions(+), 6 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt index 6fefdf4dc2..5f4bd4cd4b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/MetaTagsParser.kt @@ -58,6 +58,7 @@ object MetaTagsParser { private const val META = "meta" private const val LINK = "link" private const val HEAD = "head" + private const val BODY = "body" private const val ICON = "icon" private const val REL = "rel" private const val CONTENT = "content" @@ -88,6 +89,12 @@ object MetaTagsParser { /** The `</head>` that ends the interesting part of the document. */ HEAD_END, + /** + * A `<body>` start tag. `</head>` is optional, so on a page that omits it this is the only + * sign the head is over. + */ + BODY_START, + /** A closed `<title>`: its text is [TagScanner.textStart]..<[TagScanner.textEnd]. */ TITLE, @@ -111,26 +118,34 @@ object MetaTagsParser { ): Sequence<MetaTag> = sequence { val s = TagScanner(input) + // Meta tags past an implicit end of head are still read, as they always were. The + // title and icons are not: past `<body>`, a `<title>` is an inline SVG's tooltip + // ("Close", "Menu"), not the page's name. + var inBody = false while (!s.exhausted()) { when (s.nextTag()) { TagKind.HEAD_END -> { break } + TagKind.BODY_START -> { + inBody = true + } + TagKind.META -> { val attrs = parseAttrs(input, s.attrsStart, s.attrsEnd) ?: continue yield(MetaTag(attrs)) } TagKind.TITLE -> { - if (includeTitleAndIcons) { + if (includeTitleAndIcons && !inBody) { val text = titleText(input, s.textStart, s.textEnd) if (text.isNotEmpty()) yield(MetaTag(mapOf(CONTENT to text), HeadElement.TITLE)) } } TagKind.LINK -> { - if (includeTitleAndIcons) { + if (includeTitleAndIcons && !inBody) { val attrs = parseAttrs(input, s.attrsStart, s.attrsEnd) ?: continue if (isIconRel(attrs[REL])) yield(MetaTag(attrs, HeadElement.LINK)) } @@ -160,7 +175,11 @@ object MetaTagsParser { ): String { val raw = input.substring(from, to) val decoded = if (raw.indexOf('&') < 0) raw else raw.replace(Attrs.RE_CHAR_REF, Attrs.Companion::replaceCharRefs) - return decoded.replace(WHITESPACE_RUN, " ").trim().take(MAX_TITLE_LENGTH) + val title = decoded.replace(WHITESPACE_RUN, " ").trim() + if (title.length <= MAX_TITLE_LENGTH) return title + // Never end on half of a surrogate pair: a lone high surrogate renders as tofu. + val cut = if (title[MAX_TITLE_LENGTH - 1].isHighSurrogate()) MAX_TITLE_LENGTH - 1 else MAX_TITLE_LENGTH + return title.substring(0, cut) } private class TagScanner( @@ -314,6 +333,7 @@ object MetaTagsParser { when (nameEnd - nameStart) { META.length -> { if (nameIs(nameStart, nameEnd, META)) return TagKind.META + if (nameIs(nameStart, nameEnd, BODY)) return TagKind.BODY_START // Most `<link>`s are stylesheets and preloads; checking the raw span for `icon` // keeps their attributes from ever being parsed into a map. if (nameIs(nameStart, nameEnd, LINK) && spanContains(attrsStart, attrsEnd, ICON)) return TagKind.LINK diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt index 00b24466cf..4977a6cd67 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt @@ -71,10 +71,20 @@ class UrlInfoItem( /** * The page's icon, absolute: the one it declared, else `/favicon.ico` at its origin -- the * same guess every browser makes, since most sites that declare nothing still serve one. + * + * A `data:` icon is used as-is when it is an image (the image loader decodes those inline), + * and taken at its word when it is not: `href="data:,"` is how a site tells browsers it has no + * favicon, so guessing `/favicon.ico` there is a request that is known to miss. + * * Null for a non-HTML URL (an image or a video is its own preview) or an unresolvable one. */ val iconUrlFullPath: String? = - if (mimeType.startsWith("text/html")) resolve(icon) ?: resolve(DEFAULT_FAVICON) else null + when { + !mimeType.startsWith("text/html", ignoreCase = true) -> null + icon.startsWith("data:image/", ignoreCase = true) -> if (icon.length <= MAX_INLINE_ICON_LENGTH) icon else resolve(DEFAULT_FAVICON) + icon.startsWith("data:", ignoreCase = true) -> null + else -> resolve(icon) ?: resolve(DEFAULT_FAVICON) + } /** * The declared `og:video`, absolute, but only when the declaration holds up: a [videoType] the @@ -149,5 +159,9 @@ class UrlInfoItem( companion object { private const val DEFAULT_FAVICON = "/favicon.ico" + + // Previews are cached, so an inline icon is held for as long as its card is. Real inline + // favicons are a few KB; anything far past that is not worth keeping in memory. + private const val MAX_INLINE_ICON_LENGTH = 64 * 1024 } } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt index d0f9db83cc..4822fceaa4 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/preview/TitleAndIconFallbackTest.kt @@ -176,4 +176,38 @@ class TitleAndIconFallbackTest { assertEquals("", info.icon) } + + @Test + fun aBodyTitleIsIgnoredWhenTheHeadIsNeverClosed() { + // `</head>` is optional: `<body>` closes the head implicitly. An inline SVG's <title> + // ("Close", "Menu") is the classic thing to then mistake for the page title. + val info = extract("""<head><meta name="description" content="D"><body><svg><title>Close""") + + assertEquals("", info.title) + assertEquals("D", info.description) + } + + @Test + fun aBodyIconLinkIsIgnoredWhenTheHeadIsNeverClosed() { + val info = extract("""T""") + + assertEquals("T", info.title) + assertEquals("", info.icon) + } + + @Test + fun aTitleWithAttributesIsRead() { + val info = extract("""Helmet""") + + assertEquals("Helmet", info.title) + } + + @Test + fun aLongTitleIsNotCutInsideASurrogatePair() { + // 299 ASCII chars then an emoji: a plain take(300) keeps only its high surrogate. + val info = extract("" + "a".repeat(299) + "\uD83D\uDE00 tail") + + assertTrue(info.title.isNotEmpty()) + assertTrue(!info.title.last().isHighSurrogate(), "title ends in a dangling high surrogate") + } } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt index eaeeeb96a4..5998927fbe 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItemTextPreviewTest.kt @@ -84,4 +84,26 @@ class UrlInfoItemTextPreviewTest { fun nonHtmlUrlsHaveNoIcon() { assertNull(page(url = "https://example.com/a.png", image = "https://example.com/a.png", mimeType = "image/png").iconUrlFullPath) } + + @Test + fun anUppercaseHtmlMimeStillGetsAnIcon() { + // UrlPreview stores MediaType.toString(), which keeps the server's spelling. + assertEquals( + "https://example.nsite.lol/favicon.ico", + page(mimeType = "Text/HTML; charset=UTF-8").iconUrlFullPath, + ) + } + + @Test + fun anEmptyDataIconMeansTheSiteHasNoIcon() { + // `` is the common way to tell browsers not to request + // /favicon.ico. Requesting it anyway is a guaranteed miss. + assertNull(page(icon = "data:,").iconUrlFullPath) + } + + @Test + fun anInlineImageIconIsUsedAsIs() { + val inline = "data:image/png;base64,iVBORw0KGgo=" + assertEquals(inline, page(icon = inline).iconUrlFullPath) + } } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/MetaTagsParserBenchmark.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/MetaTagsParserBenchmark.kt index 228498fdf1..2a2d0f70db 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/MetaTagsParserBenchmark.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/prodbench/MetaTagsParserBenchmark.kt @@ -160,6 +160,19 @@ class MetaTagsParserBenchmark { } bench("no , whole doc", open, 2_000) { MetaTagsParser.parse(it).count() } + // What production runs (HtmlParser asks for the and icon <link>s too). Should sit + // on top of the rows above: the 60 preload links never reach the attribute parser. + bench("spa head, +title/icons", spa, 50_000) { MetaTagsParser.parse(it, includeTitleAndIcons = true).count() } + bench("spa head, og: + fallbacks", spa, 50_000) { + OpenGraphParser().extractUrlInfo(MetaTagsParser.parse(it, includeTitleAndIcons = true)).title.length + } + bench("heavy head, +title/icons", heavy, 2_000) { MetaTagsParser.parse(it, includeTitleAndIcons = true).count() } + bench("no </head>, +title/icons", open, 2_000) { MetaTagsParser.parse(it, includeTitleAndIcons = true).count() } + + val spaFallbacks = OpenGraphParser().extractUrlInfo(MetaTagsParser.parse(spa, includeTitleAndIcons = true)) + assertEquals("Example — Your Network. Your Rules.", spaFallbacks.title) + assertEquals("/favicon.svg", spaFallbacks.icon) + assertTrue(MetaTagsParser.parse(open).count() >= 64) } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt index 59265eb13d..66853c6bac 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.ui.components +import androidx.collection.LruCache import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -49,6 +50,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import coil3.network.HttpException import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem @@ -130,7 +132,15 @@ fun UrlPreviewCard( // Only meaningful when the card's own tap does something else (e.g. opening the comment // thread); otherwise it would duplicate the card's open-in-browser tap. - val onOpenInBrowser: (() -> Unit)? = onCardClick?.let { { runCatching { uri.openUri(url) } } } + val hasCardClick = onCardClick != null + val onOpenInBrowser: (() -> Unit)? = + remember(hasCardClick, url, uri) { + if (hasCardClick) { + { runCatching { uri.openUri(url) } } + } else { + null + } + } // A page with no picture to lead with -- most often one with no OpenGraph at all, previewed // from its `<title>`, meta description and favicon -- gets the short, wide card. Painting the @@ -251,6 +261,13 @@ private fun CompactUrlPreviewCard( } } +/** + * Icon URLs the server refused (404 and the like) in this process. The image loader caches + * successes but not failures, so without this a site with no `/favicon.ico` would be asked for it + * again -- and 404 again -- every time its card scrolled back into view. + */ +private val failedIconUrls = LruCache<String, Unit>(200) + /** * The site's icon in a fixed square. A generic globe sits there until the icon actually loads, * so a site whose favicon is missing (the `/favicon.ico` guess is only a guess) or undecodable @@ -278,13 +295,16 @@ private fun UrlPreviewIcon( ) } - if (iconUrl != null) { + if (iconUrl != null && failedIconUrls[iconUrl] == null) { AsyncImage( model = iconUrl, contentDescription = contentDescription, contentScale = ContentScale.Fit, modifier = UrlPreviewIconImageModifier, onSuccess = { loaded = true }, + // Only a server's answer is remembered. A network error (offline, a timeout) says + // nothing about the icon and must be retried once the connection is back. + onError = { if (it.result.throwable is HttpException) failedIconUrls.put(iconUrl, Unit) }, ) } } From 5b7cffc72d0d4bc40696256612e5a2afd87f3438 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Thu, 1 Oct 2026 10:38:28 -0400 Subject: [PATCH 42/45] fix(desktop): read now-playing command output while the command runs runCommand waited for the process to exit before reading its stdout. On a busy Mac `ps -A -o comm=` prints ~100 KB, past the pipe buffer, so ps blocked on the write, waitFor hit the 5 s timeout and the macOS reader never saw Spotify or Music (and every poll stalled 5 s). Read stdout to EOF while a watchdog thread kills a command that outlives the timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../desktop/nowPlaying/OsNowPlayingReader.kt | 24 +++++++++++++------ .../nowPlaying/OsNowPlayingReaderTest.kt | 22 +++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt index 7a8cb58c6d..f8aad95b00 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReader.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread /** Reads what the OS says another app is playing right now. */ interface OsNowPlayingReader { @@ -63,13 +64,22 @@ internal suspend fun runCommand( .start() process = started started.outputStream.close() - // Wait before reading so a hung command cannot block the read forever. The outputs - // here are a few KB, well under the pipe buffer, so the command never blocks on it. - if (!started.waitFor(timeoutSeconds, TimeUnit.SECONDS) || started.exitValue() != 0) { - null - } else { - started.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } - } + // Read while the command runs: `ps -A -o comm=` prints ~100 KB on a busy Mac, past the + // pipe buffer, so waiting for the exit before reading deadlocked until the timeout and + // the reader never saw a player. The watchdog still kills a command that hangs, which + // also ends the read. + val watchdog = + thread(isDaemon = true, name = "now-playing-command-timeout") { + try { + if (!started.waitFor(timeoutSeconds, TimeUnit.SECONDS)) started.destroyForcibly() + } catch (_: InterruptedException) { + // The command finished first. + } + } + val output = started.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } + val exited = started.waitFor(timeoutSeconds, TimeUnit.SECONDS) + watchdog.interrupt() + if (!exited || started.exitValue() != 0) null else output } catch (e: InterruptedException) { throw e } catch (e: Exception) { diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt index 02e0bc97c2..36b29a93cc 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nowPlaying/OsNowPlayingReaderTest.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.desktop.nowPlaying import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.nowPlaying.NowPlayingSource import kotlinx.coroutines.test.runTest +import java.io.File import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull +import kotlin.test.assertTrue class OsNowPlayingReaderTest { private val listNames = @@ -215,4 +217,24 @@ class OsNowPlayingReaderTest { assertEquals("ZuneMusic", WindowsNowPlayingReader.appLabel("Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic")) assertEquals("308046B0AF4A39CB", WindowsNowPlayingReader.appLabel("308046B0AF4A39CB")) } + + private val hasShell = File("/bin/sh").canExecute() + + /** `ps -A -o comm=` prints ~100 KB on a busy Mac; reading only after the exit deadlocked on the pipe. */ + @Test + fun runCommandReturnsOutputLargerThanThePipeBuffer() = + runTest { + if (!hasShell) return@runTest + val output = runCommand(listOf("/bin/sh", "-c", "i=0; while [ \$i -lt 4000 ]; do echo 0123456789012345678901234567890123456789012345678; i=\$((i+1)); done")) + assertEquals(4000 * 50, output?.length) + } + + @Test + fun runCommandGivesUpOnAHungCommand() = + runTest { + if (!hasShell) return@runTest + val started = System.currentTimeMillis() + assertNull(runCommand(listOf("/bin/sh", "-c", "sleep 30"), timeoutSeconds = 1)) + assertTrue(System.currentTimeMillis() - started < 10_000, "the timeout must end the call") + } } From 3d9c5bf0c95b049c9b66d2031868351ba8e25fa2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Thu, 1 Oct 2026 11:08:42 -0400 Subject: [PATCH 43/45] fix: resolve relative URLs against a pathless base on Android Android's java.net.URI resolves a relative reference against a base with an empty path by gluing it onto the host: `y18.svg` against `https://news.ycombinator.com` became `https://news.ycombinator.comy18.svg`, so Hacker News' compact link preview never loaded its icon (seen on an SM-T220). The JDK follows RFC 3986 and treats the empty path as `/`, so normalize the base to that before resolving. Rebuilt from the raw parts so an encoded query is not encoded twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../util/HttpUrlResolution.jvmAndroid.kt | 22 +++++++- .../commons/util/HttpUrlResolutionTest.kt | 56 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolutionTest.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.jvmAndroid.kt index bda5b0c2a9..b27eb59efe 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.jvmAndroid.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.jvmAndroid.kt @@ -29,7 +29,7 @@ actual fun resolveHttpUrl( reference: String, ): String? = runCatching { - val baseUri = base?.let { runCatching { URI(it).toURL().toURI() }.getOrNull() } + val baseUri = base?.let { runCatching { URI(it).toURL().toURI().withRootPath() }.getOrNull() } val resolved = if (baseUri != null) baseUri.resolve(reference) else URI(reference) if (!resolved.scheme.equals("http", ignoreCase = true) && !resolved.scheme.equals("https", ignoreCase = true) @@ -39,3 +39,23 @@ actual fun resolveHttpUrl( resolved.toURL().toString() } }.getOrNull() + +/** + * `https://host` as `https://host/`. RFC 3986 resolves a relative reference against an empty base path + * as if the path were `/`, and the JDK does, but Android's `java.net.URI` does not: it glued + * `y18.svg` onto `https://news.ycombinator.com` as `https://news.ycombinator.comy18.svg`, so the + * page's icon (or a relative og:image) pointed at a host that does not exist. + */ +private fun URI.withRootPath(): URI = + if (isOpaque || !rawPath.isNullOrEmpty() || rawAuthority == null) { + this + } else { + // From the raw (still-encoded) parts: the multi-argument constructor would encode them again. + URI( + buildString { + append(scheme).append("://").append(rawAuthority).append('/') + rawQuery?.let { append('?').append(it) } + rawFragment?.let { append('#').append(it) } + }, + ) + } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolutionTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolutionTest.kt new file mode 100644 index 0000000000..353aacd5d1 --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolutionTest.kt @@ -0,0 +1,56 @@ +/* + * 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.commons.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +/** + * A relative reference against a base with no path resolves as if the path were `/` (RFC 3986). + * Android's `java.net.URI` got this wrong — `y18.svg` against `https://news.ycombinator.com` became + * `https://news.ycombinator.comy18.svg` — so the base is normalized before resolving. + */ +class HttpUrlResolutionTest { + @Test + fun relativeReferenceAgainstAPathlessBase() { + assertEquals("https://news.ycombinator.com/y18.svg", resolveHttpUrl("https://news.ycombinator.com", "y18.svg")) + assertEquals("https://news.ycombinator.com/favicon.ico", resolveHttpUrl("https://news.ycombinator.com", "/favicon.ico")) + } + + @Test + fun pathlessBaseKeepsItsQueryAndEncoding() { + assertEquals("https://a.example/icon.png", resolveHttpUrl("https://a.example?q=a%20b", "icon.png")) + // Encoded characters are carried over as they are, not encoded a second time. + assertEquals("https://a.example/icons/a%20b.png", resolveHttpUrl("https://a.example?q=a%20b", "icons/a%20b.png")) + } + + @Test + fun basesWithAPathAreUnchanged() { + assertEquals("https://a.example/dir/icon.png", resolveHttpUrl("https://a.example/dir/page.html", "icon.png")) + assertEquals("https://cdn.example/x.png", resolveHttpUrl("https://a.example/dir/", "//cdn.example/x.png")) + } + + @Test + fun nonHttpStaysRefused() { + assertNull(resolveHttpUrl("https://a.example", "file:///etc/passwd")) + } +} From f12cf180f66c25b86d209232cde42ff04b8401b6 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Thu, 1 Oct 2026 15:25:53 +0000 Subject: [PATCH 44/45] fix: use KMP BigDecimal conversions in shared zap and search code commonMain's BigDecimal has no toLong()/toDouble() (only the JVM typealias does), so :commonsUI:compileCommonMainKotlinMetadata failed in CI's lint job. Use quartz's toLongValue()/toDoubleValue(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P --- .../ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt | 3 ++- .../ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt | 3 ++- .../commons/ui/screen/loggedIn/search/SearchBarViewModel.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt index 919f592563..0219d847ce 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.utils.toLongValue private const val BIG_ZAP_THRESHOLD_SATS = 50_000L @@ -76,7 +77,7 @@ fun RenderChatZap( val isBigZap = remember(zapEvent) { - (zapEvent.amount?.toLong() ?: 0L) >= BIG_ZAP_THRESHOLD_SATS + (zapEvent.amount?.toLongValue() ?: 0L) >= BIG_ZAP_THRESHOLD_SATS } val accentAlpha = if (isBigZap) 0.18f else 0.08f diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index 2884af5d8e..84c25cd819 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.utils.BigDecimal +import com.vitorpamplona.quartz.utils.toLongValue private val Gold = Color(0xFFFFC300) private val Silver = Color(0xFFB0B7C0) @@ -88,7 +89,7 @@ fun PodcastTopSupporters( note.zaps.mapNotNull { (_, receiptNote) -> val receipt = receiptNote?.event as? ZapReceiptEvent ?: return@mapNotNull null val request = receipt.zapRequest ?: return@mapNotNull null - val sats = receipt.amount()?.toLong() ?: return@mapNotNull null + val sats = receipt.amount()?.toLongValue() ?: return@mapNotNull null // Anon/private zaps carry an `anon` tag; collapse them into the shared bucket. val isAnon = request.tags.any { it.isNotEmpty() && it[0] == "anon" } ZapContribution(receiptNote.idHex, request.pubKey, isAnon, sats) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt index 9d6b69413e..b6e66d4ec9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Rfc3986 import com.vitorpamplona.quartz.utils.startsWithAny +import com.vitorpamplona.quartz.utils.toDoubleValue import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -467,7 +468,7 @@ class SearchBarViewModel( query = parsed, order = order, event = { it.event }, - zapTotal = { it.zapsAmount.toDouble() }, + zapTotal = { it.zapsAmount.toDoubleValue() }, ) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) From c6d20b7b13d9513e4d290e1cdc670a87344c31cc Mon Sep 17 00:00:00 2001 From: vitorpamplona <532031+vitorpamplona@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:37:12 +0000 Subject: [PATCH 45/45] chore: sync Crowdin translations and seed translator npub placeholders --- .../values-hu-rHU/strings.xml | 43 +++++++++++++++---- docs/changelog/translators.json | 1 + 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index 82f9a3ce58..cce0cc7504 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -580,8 +580,8 @@ <string name="concord_invite_revoking">Visszavonás…</string> <string name="concord_name_budget">%1$d / %2$d bájt</string> <string name="concord_invite_revoke_privatize_warning">Ez a közösség utolsó élő meghívási hivatkozása. A visszavonása priváttá teszi a közösséget, és lecseréli a kulcsait, így csak a jelenlegi tagok tartják meg a hozzáférést.</string> - <string name="concord_invite_revoked_privatized">A hivatkozás visszavonva. A közösség most privát, és a kulcsai le lettek cserélve.</string> - <string name="concord_invite_revoked_privatize_pending">A hivatkozás visszavonva. A közösség most privát, de a kulcsai nem lettek lecserélve: egy olyan adminisztrátornak kell lecserélnie őket, aki kitilthat tagokat.</string> + <string name="concord_invite_revoked_privatized">A hivatkozás vissza lett vonva. A közösség most privát, és a kulcsai le lettek cserélve.</string> + <string name="concord_invite_revoked_privatize_pending">A hivatkozás vissza lett vonva. A közösség most privát, de a kulcsai nem lettek lecserélve: egy olyan adminisztrátornak kell lecserélnie őket, aki kitilthat tagokat.</string> <string name="concord_mode_private">Privát</string> <plurals name="concord_mode_public"> <item quantity="one">Nyilvános · %1$d élő meghívási hivatkozás</item> @@ -3638,7 +3638,7 @@ <string name="concord_channel_make_public_message">A közösség minden tagja olvashatja mostantól a(z) #%1$s csatornát. A privát állapotban közzétett üzenetek csak azoknak a tagoknak maradnak olvashatók, akik birtokolták a kulcsát.</string> <string name="concord_channel_name_label">Csatorna neve</string> <string name="concord_channel_no_messages">Még nincsenek üzenetek</string> - <string name="concord_channel_private_hint">Csak azok a tagok olvashatják, akik birtokolják a hozzáférési szerepkörét. Adja meg a szerepkört, hogy beengedje őket; a visszavonása lecseréli a csatornakulcsot.</string> + <string name="concord_channel_private_hint">Csak azok a tagok olvashatják, akik rendelkeznek az ehhez tartozó hozzáférési szerepkörrel. A szerepkör megadása lehetővé teszi számukra a belépést; a visszavonása a csatorna kulcsának frissítését eredményezi.</string> <string name="concord_channel_private_toggle">Privát csatorna</string> <string name="concord_channel_rename">Csatorna átnevezése</string> <string name="concord_channel_rename_save">Átnevezés</string> @@ -5396,6 +5396,7 @@ <string name="marmot_invite_device_rejected">Egyetlen átjátszó sem fogadta el a KeyPackage-et. Ellenőrizze az átjátszó beállításait, és próbálja újra.</string> <string name="chat_delivery_sending">Küldés\u2026</string> <string name="chat_delivery_failed">Nem sikerült elküldeni. Koppintson az újrapróbálkozáshoz.</string> + <string name="chat_delivery_rejected">Az átjátszó elutasította ezt az üzenetet</string> <plurals name="marmot_unread_messages"> <item quantity="one">%1$d új üzenet</item> <item quantity="other">%1$d új üzenet</item> @@ -5464,13 +5465,13 @@ <string name="cordn_preview_video">Videó</string> <string name="cordn_preview_file">Fájl</string> <string name="new_conversation_cordn_title">Cordn csoport</string> - <string name="new_conversation_cordn_tagline">Titkosított csoportos csevegés, amelyet az Ön által választott koordinátor rendez.</string> + <string name="new_conversation_cordn_tagline">Titkosított csoportok egy Ön által választott koordinátoron</string> <string name="new_conversation_cordn_chip">Koordinált</string> - <string name="new_conversation_cordn_best">Azoknak a csapatoknak a legjobb, amelyek a beszélgetés egyetlen megbízható sorrendjét szeretnék.</string> + <string name="new_conversation_cordn_best">Azon csapatok számára, amelyek a beszélgetés egy megbízható sorrendjét szeretnék.</string> <string name="new_conversation_cordn_cta">Cordn csoport létrehozása</string> - <string name="new_conversation_cordn_pro_1">Végponttól végpontig titkosított MLS-sel – a koordinátor soha nem olvashat el egyetlen üzenetet sem.</string> - <string name="new_conversation_cordn_pro_2">Egy mindenki által elfogadott sorrend, így az előzményeket egy hibás óra sem keverheti össze.</string> - <string name="new_conversation_cordn_con_1">A koordinátor tudja, ki van a csoportban és mikor beszélnek, még ha nem is tudja elolvasni, mit mondanak.</string> + <string name="new_conversation_cordn_pro_1">A koordinátor nem tudja elolvasni az üzeneteket</string> + <string name="new_conversation_cordn_pro_2">Egy mindenki által elfogadott üzenetrendezési sorrend</string> + <string name="new_conversation_cordn_con_1">A koordinátor tudja, ki van a csoportban és mikor</string> <string name="cordn_invitations_entry_action">Meghívók áttekintése</string> <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say @@ -6276,4 +6277,30 @@ <string name="browser_pill_download_risky">Ez a fájltípus képes kódot futtatni. Ellenőrizze hogy a név megegyezik-e azzal, amit le akart tölteni.</string> <string name="browser_pill_download_save">Mentés</string> <string name="browser_pill_download_from">Innen: %1$s</string> + <string name="prediction_market">Fogadási piac</string> + <string name="prediction_market_status_open">Nyitva</string> + <string name="prediction_market_status_closed">Zárva</string> + <string name="prediction_market_status_resolved">Megoldva</string> + <string name="prediction_market_status_cancelled">Megszakítva</string> + <string name="prediction_market_demo">Bemutató · játékpénz</string> + <string name="prediction_market_closes_in">Bezár ekkor: %1$s</string> + <string name="prediction_market_closed_on">Bezárt ekkor: %1$s</string> + <string name="prediction_market_bet_range">Fogadások: %1$s-%2$s satoshi</string> + <string name="prediction_market_min_bet">Legkisebb tét: %1$s satoshi</string> + <string name="prediction_market_max_bet">Legnagyobb tét: %1$s satoshi</string> + <string name="prediction_market_fee">Díj: %1$s</string> + <string name="prediction_market_cancel_reason">Megszakítva: %1$s</string> + <string name="prediction_market_winning_outcome">Nyertes eredmény</string> + <plurals name="prediction_market_more_outcomes"> + <item quantity="one">+%1$d további</item> + <item quantity="other">+%1$d további</item> + </plurals> + <string name="ballot">Szavazólap</string> + <string name="ballot_election">Választás: %1$s</string> + <string name="ballot_vote">Szavazás</string> + <string name="ballot_proof_hash">Bizonyíték: %1$s</string> + <plurals name="ballot_more_answers"> + <item quantity="one">+%1$d további válasz</item> + <item quantity="other">+%1$d további válasz</item> + </plurals> </resources> diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 4551942b1c..735dd34daa 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -81,6 +81,7 @@ { "user": "vitorpamplona", "languages": [ + "Arabic", "Arabic, Saudi Arabia", "Bengali", "Chinese Simplified",