diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/FlavorTranslationPlatform.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/FlavorTranslationPlatform.kt new file mode 100644 index 0000000000..badf92628b --- /dev/null +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/FlavorTranslationPlatform.kt @@ -0,0 +1,26 @@ +/* + * 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.components + +import com.vitorpamplona.amethyst.commons.ui.components.TranslationPlatform + +/** No translation service in this flavor, so the content is always its own "translation". */ +val FlavorTranslationPlatform: TranslationPlatform = TranslationPlatform.None diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt index f0d241c911..305f026951 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt @@ -30,13 +30,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.commons.ui.theme.Size75dp @@ -50,11 +50,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -@Immutable -class WaveformData( - val wave: List, -) - @Composable fun VideoView( videoUri: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 3d0e5bced5..a3bc505dea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex 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 472d541b02..18348acf29 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 @@ -73,6 +73,7 @@ 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.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.cast.CastRequest import com.vitorpamplona.amethyst.service.cast.CastSessionState import com.vitorpamplona.amethyst.service.cast.resolveCastLiveness @@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog import com.vitorpamplona.amethyst.ui.cast.rememberCastWithLocalNetworkPermission import com.vitorpamplona.amethyst.ui.components.ShareMediaAction import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.saveMediaToGallery import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt index e777945a9a..785480c2d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/GetMediaItem.kt @@ -25,9 +25,9 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt index a140f0b37e..7ff0500dae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemData.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem import androidx.compose.runtime.Immutable import androidx.media3.common.MediaItem +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/AudioPlayingAnimation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/AudioPlayingAnimation.kt index 05da0bf1c8..631c17d06f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/AudioPlayingAnimation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/AudioPlayingAnimation.kt @@ -44,9 +44,9 @@ import androidx.media3.common.Tracks import com.vitorpamplona.amethyst.commons.audio.AudioVisualizer import com.vitorpamplona.amethyst.commons.audio.Spectrum import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.audio.delayedByFrames import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.playerPool.PcmTapRegistry import kotlinx.coroutines.flow.emptyFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt index a9f2a26726..c4fcbd672c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/SyntheticWaveform.kt @@ -20,12 +20,13 @@ */ package com.vitorpamplona.amethyst.service.playback.composable.wavefront -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.amethyst.commons.audio.WaveformData +import kotlin.math.PI import kotlin.math.sin import kotlin.random.Random private const val SYNTHETIC_WAVEFORM_SAMPLES = 96 -private const val TWO_PI = (Math.PI * 2).toFloat() +private const val TWO_PI = (PI * 2).toFloat() /** * Builds a stable, decorative waveform from a [seed] (usually an event id). ExoPlayer drives @@ -57,7 +58,7 @@ fun syntheticWaveformFor(seed: String): WaveformData { List(SYNTHETIC_WAVEFORM_SAMPLES) { index -> val phase = index.toFloat() / SYNTHETIC_WAVEFORM_SAMPLES // Optional gentle fade so some taper at the ends, others stay even. - val envelope = 1f - envelopeStrength * (1f - sin(phase * Math.PI).toFloat()) + val envelope = 1f - envelopeStrength * (1f - sin(phase * PI).toFloat()) val carrier = sin(phase * TWO_PI * carrierCycles + phaseOffset) * carrierWeight val noise = (rng.nextFloat() - 0.5f) * 2f * noiseStrength ((baseline + carrier + noise) * envelope).coerceIn(0.05f, 1.0f) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index e4bc115142..bec6b628bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.linc.audiowaveform.infiniteLinearGradient +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData @Composable fun Waveform( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt index cb2248312e..53f1ddd1b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/IntentExtras.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.pip import android.graphics.Rect import android.os.Bundle -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.quartz.utils.ensure diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt index 2e544edff9..032a7d2f93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt @@ -40,10 +40,10 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.state.rememberMuteButtonState import androidx.media3.ui.compose.state.rememberPlayPauseButtonState +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.controls.PIP_PRESHRINK_MAX_SHORT_SIDE_PX import com.vitorpamplona.amethyst.service.playback.composable.controls.constrainVideoQualityToViewport import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt index 0f4ac5d7fb..0c6f233a37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt @@ -273,4 +273,14 @@ object AndroidRichTextPlatform : RichTextPlatform { accountViewModel: AccountViewModel, nav: INav, ) = RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav) + + @Composable + override fun SecretMessage( + content: RichTextViewerState, + callbackUri: String?, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = CoreSecretMessage(content, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt index 230631cdf1..81cb0d0993 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt @@ -22,11 +22,10 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary -import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent +import com.vitorpamplona.amethyst.commons.ui.components.rememberBlossomUriOpener import org.jetbrains.compose.resources.StringResource import com.vitorpamplona.amethyst.commons.ui.components.ClickableUrl as SharedClickableUrl @@ -44,13 +43,13 @@ fun ClickableUrl( onError: (StringResource, StringResource) -> Unit = { _, _ -> }, ) { if (url.startsWith("blossom:")) { - val context = LocalContext.current + val blossomOpener = rememberBlossomUriOpener() ClickableTextPrimary( text = urlText, style = style, maxLines = 1, overflow = TextOverflow.MiddleEllipsis, - onClick = { openBlossomUriAsIntent(context, url, onError) }, + onClick = { blossomOpener.open(url, onError) }, ) } else { SharedClickableUrl(url = url, displayText = urlText, style = style) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 26676f1aa5..759de8cf82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components -import android.util.LruCache +import androidx.collection.LruCache import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaPlaceholders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaPlaceholders.kt new file mode 100644 index 0000000000..f42a338683 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MediaPlaceholders.kt @@ -0,0 +1,280 @@ +/* + * 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.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent +import com.vitorpamplona.amethyst.commons.service.image.placeholderModel +import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.commons.ui.components.rememberBlossomUriOpener +import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon +import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp +import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.StringResource + +@Composable +fun ImageUrlWithDownloadButton( + url: String, + showImage: MutableState, +) { + val uri = LocalUriHandler.current + + val primary = MaterialTheme.colorScheme.primary + val background = MaterialTheme.colorScheme.onBackground + + val regularText = remember { SpanStyle(color = background) } + val clickableTextStyle = remember { SpanStyle(color = primary) } + + val annotatedTermsString = + remember { + buildAnnotatedString { + withStyle(clickableTextStyle) { + pushStringAnnotation("routeToImage", "") + append("$url ") + pop() + } + + withStyle(clickableTextStyle) { + pushStringAnnotation("routeToImage", "") + pop() + } + + withStyle(regularText) { append(" ") } + } + } + + val pressIndicator = + remember { + Modifier + .fillMaxWidth() + .clickable { runCatching { uri.openUri(url) } } + } + + Row( + modifier = + Modifier + .width(IntrinsicSize.Max), + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = annotatedTermsString, + modifier = + pressIndicator + .weight(1f, fill = false), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + InlineDownloadIcon(showImage) + } +} + +@Composable +private fun InlineDownloadIcon(showImage: MutableState) = + IconButton( + modifier = Modifier.size(Size20dp), + onClick = { showImage.value = true }, + ) { + DownloadForOfflineIcon(Size24dp) + } + +@Composable +fun WaitAndDisplay(content: @Composable (AnimatedVisibilityScope.() -> Unit)) { + val visible = remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(200) + visible.value = true + } + + AnimatedVisibility( + visible = visible.value, + enter = fadeIn(), + exit = fadeOut(), + content = content, + ) +} + +@Composable +fun DisplayUrlWithLoadingSymbol( + content: BaseMediaContent, + onError: (StringResource, StringResource) -> Unit = { _, _ -> }, +) { + val uri = LocalUriHandler.current + + val primary = MaterialTheme.colorScheme.primary + val background = MaterialTheme.colorScheme.onBackground + + val regularText = remember { SpanStyle(color = background) } + val clickableTextStyle = remember { SpanStyle(color = primary) } + + val blossomOpener = rememberBlossomUriOpener() + + val annotatedTermsString = + remember { + buildAnnotatedString { + if (content is MediaUrlContent) { + withStyle(clickableTextStyle) { + pushStringAnnotation("routeToImage", "") + append(content.url + " ") + pop() + } + } else { + withStyle(regularText) { append("Loading content...") } + } + + withStyle(clickableTextStyle) { + pushStringAnnotation("routeToImage", "") + pop() + } + + withStyle(regularText) { append(" ") } + } + } + + val pressIndicator = + remember { + if (content is MediaUrlContent) { + Modifier.clickable { + if (content.url.startsWith("blossom:")) { + blossomOpener.open(content.url, onError) + } else { + runCatching { uri.openUri(content.url) } + } + } + } else { + Modifier + } + } + + Row( + modifier = + Modifier + .width(IntrinsicSize.Max), + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = annotatedTermsString, + modifier = pressIndicator.weight(1f, fill = false), + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, + ) + InlineLoadingIcon() + } +} + +@Composable +fun DisplayUrlWithLoadingSymbol( + url: String, + onError: (StringResource, StringResource) -> Unit = { _, _ -> }, +) { + val uri = LocalUriHandler.current + + val primary = MaterialTheme.colorScheme.primary + val annotatedTermsString = + remember { + buildAnnotatedString { + withStyle(SpanStyle(color = primary)) { + pushStringAnnotation("routeToImage", "") + append("$url ") + pop() + } + } + } + + val blossomOpener = rememberBlossomUriOpener() + + val pressIndicator = + remember { + Modifier.clickable { + if (url.startsWith("blossom:")) { + blossomOpener.open(url, onError) + } else { + runCatching { + uri.openUri(url) + } + } + } + } + + Row( + modifier = Modifier.width(IntrinsicSize.Max), + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = annotatedTermsString, + modifier = pressIndicator.weight(1f, fill = false), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + InlineLoadingIcon() + } +} + +@Composable +private fun InlineLoadingIcon() = LoadingAnimation() + +@Composable +fun DisplayBlurHash( + blurhash: String?, + description: String?, + contentScale: ContentScale, + modifier: Modifier, + thumbhash: String? = null, +) { + val model = + placeholderModel(thumbhash, blurhash) ?: return + + AsyncImage( + model = model, + contentDescription = description, + contentScale = contentScale, + modifier = modifier, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/NonlazyGridPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/NonlazyGridPreview.kt index c9917b3936..e6d598aec3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/NonlazyGridPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/NonlazyGridPreview.kt @@ -33,10 +33,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview -import com.google.common.math.IntMath.sqrt import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp -import java.math.RoundingMode import kotlin.math.ceil +import kotlin.math.sqrt @Composable fun GridPreviewTemplate(count: Int) { @@ -93,7 +92,7 @@ fun AutoNonlazyGrid( content: @Composable (Int) -> Unit, ) { if (itemCount > 0) { - NonlazyGrid(sqrt(itemCount, RoundingMode.UP), itemCount, modifier, content) + NonlazyGrid(ceil(sqrt(itemCount.toDouble())).toInt(), itemCount, modifier, content) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextPlatformLeaves.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextPlatformLeaves.kt index 827943aedd..265da1e3bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextPlatformLeaves.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextPlatformLeaves.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -94,15 +93,15 @@ import com.vitorpamplona.amethyst.commons.ui.components.RenderRegular import com.vitorpamplona.amethyst.commons.ui.components.RenderTextParagraph import com.vitorpamplona.amethyst.commons.ui.components.TagLink import com.vitorpamplona.amethyst.commons.ui.components.measureSpaceWidth +import com.vitorpamplona.amethyst.commons.ui.components.rememberBlossomUriOpener import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.CashuCardBorders import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -444,13 +443,13 @@ fun ClickableBlossomUri( blossomUri: String, accountViewModel: AccountViewModel, ) { - val context = LocalContext.current + val blossomOpener = rememberBlossomUriOpener() ClickableTextPrimary( text = remember { BlossomUri.parse(blossomUri)?.filename() ?: blossomUri }, maxLines = 1, overflow = TextOverflow.MiddleEllipsis, - onClick = { openBlossomUriAsIntent(context, blossomUri, accountViewModel.toastManager::toast) }, + onClick = { blossomOpener.open(blossomUri) { title, message -> accountViewModel.toastManager.toast(title, message) } }, ) } diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt similarity index 66% rename from amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index def89447be..2d1e76747c 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -22,11 +22,10 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.ui.components.LocalTranslationPlatform import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthPaddingTop5dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -44,19 +43,27 @@ fun TranslatableRichTextViewer( authorPubKey: String? = null, accountViewModel: AccountViewModel, nav: INav, -) = ExpandableRichTextViewer( - content, - canPreview, - quotesLeft, - modifier, - tags, - backgroundColor, - id, - callbackUri, - authorPubKey, - accountViewModel, - nav, -) +) { + TranslatableRichTextViewer( + content = content, + id = id, + accountViewModel = accountViewModel, + ) { + ExpandableRichTextViewer( + it, + canPreview, + quotesLeft, + modifier, + tags, + backgroundColor, + id, + callbackUri, + authorPubKey, + accountViewModel, + nav, + ) + } +} @Composable fun TranslatableRichTextViewer( @@ -65,11 +72,17 @@ fun TranslatableRichTextViewer( translationMessageModifier: Modifier = MaxWidthPaddingTop5dp, accountViewModel: AccountViewModel, displayText: @Composable (String) -> Unit, -) = displayText(content) +) = LocalTranslationPlatform.current.Translatable(content, id, translationMessageModifier, accountViewModel, displayText) -/** No translation service in this flavor, so the content is always its own "translation". */ +/** + * The translation of [content] under the current language settings, or [content] unchanged when + * no translation applies. Same machinery and cache as [TranslatableRichTextViewer] but without + * the status bar, for callers that already render one and need a second string translated in + * step with it — e.g. a NIP-84 highlight, which must translate the quoted passage alongside the + * context in order to keep locating the passage inside it. + */ @Composable fun rememberTranslation( content: String, accountViewModel: AccountViewModel, -): String = content +): String = LocalTranslationPlatform.current.rememberTranslation(content, accountViewModel) 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 9fe18b7085..44476358c2 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 @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo +import com.vitorpamplona.amethyst.commons.richtext.localJavaFile import com.vitorpamplona.amethyst.commons.ui.components.SlidingCarousel import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow @@ -424,7 +425,7 @@ internal suspend fun saveMediaToGallery( }, ) } else if (content is MediaPreloadedContent) { - content.localFile?.let { + content.localJavaFile?.let { MediaSaverToDisk.save( it, content.mimeType, @@ -566,7 +567,7 @@ private fun RenderImageOrVideo( Modifier.fillMaxWidth() } - content.localFile?.let { + content.localJavaFile?.let { val ratio = content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(it.toUri().toString()) val modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index f99ab6bada..fa0fe71b73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -24,14 +24,10 @@ import android.content.Context import android.content.Intent import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size @@ -39,7 +35,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -59,15 +54,8 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.viewModelScope -import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent @@ -105,7 +93,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.isAnimatedMediaUrl -import com.vitorpamplona.amethyst.commons.service.image.placeholderModel +import com.vitorpamplona.amethyst.commons.richtext.localJavaFile import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.InformationDialog import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation @@ -116,8 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp -import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp import com.vitorpamplona.amethyst.commons.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp import com.vitorpamplona.amethyst.commons.ui.theme.Size6dp @@ -128,7 +114,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.images.BlossomFetcher import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent import com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard import com.vitorpamplona.amethyst.ui.components.pdf.PdfViewerDialog import com.vitorpamplona.amethyst.ui.note.BlankNote @@ -153,7 +138,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.coroutines.executeAsync import okio.sink -import org.jetbrains.compose.resources.StringResource import java.io.File import java.io.IOException @@ -291,7 +275,7 @@ fun ZoomableContentView( is MediaLocalImage -> { if (content.isAnimatedMedia()) { - content.localFile?.let { + content.localJavaFile?.let { GifVideoView( videoUri = it.toUri().toString(), contentDescription = content.description, @@ -319,7 +303,7 @@ fun ZoomableContentView( } is MediaLocalVideo -> { - content.localFile?.let { + content.localJavaFile?.let { Box( modifier = Modifier.fillMaxWidth().then(boundsTrackingModifier), contentAlignment = Alignment.Center, @@ -401,19 +385,19 @@ fun LocalImageView( ) } - val ratio = remember(content) { content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.localFile.toString()) } + val ratio = remember(content) { content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.localJavaFile.toString()) } val context = LocalContext.current val imageModel = if (fullResolution) { - remember(content.localFile, context) { + remember(content.localJavaFile, context) { ImageRequest .Builder(context) - .data(content.localFile) + .data(content.localJavaFile) .size(Size.ORIGINAL) .build() } } else { - content.localFile + content.localJavaFile } CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center) { imageVisible -> if (imageVisible) { @@ -467,7 +451,7 @@ fun LocalImageView( SideEffect { val drawable = (state as AsyncImagePainter.State.Success).result.image - MediaAspectRatioCache.add(content.localFile.toString(), drawable.width, drawable.height) + MediaAspectRatioCache.add(content.localJavaFile.toString(), drawable.width, drawable.height) } content.isVerified?.let { @@ -645,71 +629,6 @@ fun UrlImageView( } } -@Composable -fun ImageUrlWithDownloadButton( - url: String, - showImage: MutableState, -) { - val uri = LocalUriHandler.current - - val primary = MaterialTheme.colorScheme.primary - val background = MaterialTheme.colorScheme.onBackground - - val regularText = remember { SpanStyle(color = background) } - val clickableTextStyle = remember { SpanStyle(color = primary) } - - val annotatedTermsString = - remember { - buildAnnotatedString { - withStyle(clickableTextStyle) { - pushStringAnnotation("routeToImage", "") - append("$url ") - pop() - } - - withStyle(clickableTextStyle) { - pushStringAnnotation("routeToImage", "") - pop() - } - - withStyle(regularText) { append(" ") } - } - } - - val pressIndicator = - remember { - Modifier - .fillMaxWidth() - .clickable { runCatching { uri.openUri(url) } } - } - - Row( - modifier = - Modifier - .width(IntrinsicSize.Max), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = annotatedTermsString, - modifier = - pressIndicator - .weight(1f, fill = false), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - InlineDownloadIcon(showImage) - } -} - -@Composable -private fun InlineDownloadIcon(showImage: MutableState) = - IconButton( - modifier = Modifier.size(Size20dp), - onClick = { showImage.value = true }, - ) { - DownloadForOfflineIcon(Size24dp) - } - @Composable fun ShowHashAnimated( content: MediaUrlImage, @@ -761,161 +680,6 @@ fun BaseMediaContent.isAnimatedMedia(): Boolean = false } -@Composable -fun WaitAndDisplay(content: @Composable (AnimatedVisibilityScope.() -> Unit)) { - val visible = remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - delay(200) - visible.value = true - } - - AnimatedVisibility( - visible = visible.value, - enter = fadeIn(), - exit = fadeOut(), - content = content, - ) -} - -@Composable -fun DisplayUrlWithLoadingSymbol( - content: BaseMediaContent, - onError: (StringResource, StringResource) -> Unit = { _, _ -> }, -) { - val uri = LocalUriHandler.current - - val primary = MaterialTheme.colorScheme.primary - val background = MaterialTheme.colorScheme.onBackground - - val regularText = remember { SpanStyle(color = background) } - val clickableTextStyle = remember { SpanStyle(color = primary) } - - val context = LocalContext.current - - val annotatedTermsString = - remember { - buildAnnotatedString { - if (content is MediaUrlContent) { - withStyle(clickableTextStyle) { - pushStringAnnotation("routeToImage", "") - append(content.url + " ") - pop() - } - } else { - withStyle(regularText) { append("Loading content...") } - } - - withStyle(clickableTextStyle) { - pushStringAnnotation("routeToImage", "") - pop() - } - - withStyle(regularText) { append(" ") } - } - } - - val pressIndicator = - remember { - if (content is MediaUrlContent) { - Modifier.clickable { - if (content.url.startsWith("blossom:")) { - openBlossomUriAsIntent(context, content.url, onError) - } else { - runCatching { uri.openUri(content.url) } - } - } - } else { - Modifier - } - } - - Row( - modifier = - Modifier - .width(IntrinsicSize.Max), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = annotatedTermsString, - modifier = pressIndicator.weight(1f, fill = false), - overflow = TextOverflow.MiddleEllipsis, - maxLines = 1, - ) - InlineLoadingIcon() - } -} - -@Composable -fun DisplayUrlWithLoadingSymbol( - url: String, - onError: (StringResource, StringResource) -> Unit = { _, _ -> }, -) { - val uri = LocalUriHandler.current - - val primary = MaterialTheme.colorScheme.primary - val annotatedTermsString = - remember { - buildAnnotatedString { - withStyle(SpanStyle(color = primary)) { - pushStringAnnotation("routeToImage", "") - append("$url ") - pop() - } - } - } - - val context = LocalContext.current - - val pressIndicator = - remember { - Modifier.clickable { - if (url.startsWith("blossom:")) { - openBlossomUriAsIntent(context, url, onError) - } else { - runCatching { - uri.openUri(url) - } - } - } - } - - Row( - modifier = Modifier.width(IntrinsicSize.Max), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = annotatedTermsString, - modifier = pressIndicator.weight(1f, fill = false), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - InlineLoadingIcon() - } -} - -@Composable -private fun InlineLoadingIcon() = LoadingAnimation() - -@Composable -fun DisplayBlurHash( - blurhash: String?, - description: String?, - contentScale: ContentScale, - modifier: Modifier, - thumbhash: String? = null, -) { - val model = - placeholderModel(thumbhash, blurhash) ?: return - - AsyncImage( - model = model, - contentDescription = description, - contentScale = contentScale, - modifier = modifier, - ) -} - @Composable fun ShareMediaAction( accountViewModel: AccountViewModel, @@ -939,7 +703,7 @@ fun ShareMediaAction( } else if (content is MediaPreloadedContent) { ShareMediaAction( popupExpanded = popupExpanded, - videoUri = content.localFile?.toUri().toString(), + videoUri = content.localJavaFile?.toUri().toString(), postNostrUri = content.uri, blurhash = content.blurhash, dim = content.dim, @@ -1083,7 +847,7 @@ fun ShareMediaAction( } is MediaLocalVideo -> { - content.localFile?.let { localFile -> + content.localJavaFile?.let { localFile -> M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.share_video)) { accountViewModel.viewModelScope.launch { shareLocalVideoFile(context, localFile, mimeType) } onDismiss() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index bd5d5d9576..ba24f0c0bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.MarkdownTextStyle import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.markdownStyle import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 3e2d297165..48f01c9515 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -41,19 +41,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user_name import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error +import com.vitorpamplona.amethyst.commons.resources.ic_dm import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage 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.routeToMessage import com.vitorpamplona.amethyst.commons.ui.note.UserPicture +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier @@ -64,8 +65,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -146,7 +146,7 @@ fun ErrorRow( } ?: stringRes(Res.string.error_dialog_talk_to_user) Icon( - painter = painterRes(R.drawable.ic_dm, 2), + painter = painterRes(Res.drawable.ic_dm, 2), contentDescription = descriptor, modifier = Size20Modifier, tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index 66c57c03b7..a210327a9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext 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 af6fc99170..c2a5d2ee39 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 @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.navigation.drawer import androidx.compose.animation.animateContentSize import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -128,6 +129,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.PermanentDrawerWidth 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.painterRes import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -152,11 +154,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.drawerSpacing import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.flow.Flow @@ -297,8 +299,8 @@ fun ProfileContentTemplate( modifier = bannerModifier, ) } else { - AsyncImage( - model = R.drawable.profile_banner, + Image( + painter = painterRes(Res.drawable.profile_banner, 3), contentDescription = stringRes(Res.string.profile_banner), contentScale = ContentScale.Crop, modifier = bannerModifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index e1395798c1..cf3052f15a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt index 0fc5563865..786b6344ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt @@ -39,17 +39,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R 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.error_dialog_button_ok import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user +import com.vitorpamplona.amethyst.commons.resources.ic_dm +import com.vitorpamplona.amethyst.commons.ui.painterRes 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.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.painterRes @Composable @Preview @@ -84,7 +84,7 @@ fun ErrorMessageDialog( onClickStartMessage?.let { TextButton(onClick = onClickStartMessage) { Icon( - painter = painterRes(R.drawable.ic_dm, 3), + painter = painterRes(Res.drawable.ic_dm, 3), contentDescription = null, ) Spacer(StdHorzSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 7e5e769d5d..7d9b94b4cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -39,15 +39,16 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.notifications.MessageSetCard +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.ic_dm import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu -import com.vitorpamplona.amethyst.ui.painterRes import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -122,7 +123,7 @@ fun MessageSetCompose( fun MessageIconBox() { Box(Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp)) { Icon( - painter = painterRes(R.drawable.ic_dm, 4), + painter = painterRes(Res.drawable.ic_dm, 4), null, modifier = Modifier.size(16.dp).align(Alignment.TopEnd), tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index c0f355920e..3b04f512eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.WatchAuthorWithBlank import com.vitorpamplona.amethyst.commons.ui.note.WatchUserFollows import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer +import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange @@ -116,13 +117,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.commons.ui.theme.profile35dpModifier import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage import com.vitorpamplona.amethyst.ui.components.ExpandableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal +import com.vitorpamplona.quartz.utils.BigDecimal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -393,7 +394,7 @@ fun DisplaySecretEmojiAsReaction( ) { Surface(Modifier.padding(10.dp)) { val color = remember { mutableStateOf(Color.Transparent) } - CoreSecretMessage(localSecretContent, null, 3, color, accountViewModel, nav) + LocalRichTextPlatform.current.SecretMessage(localSecretContent, null, 3, color, accountViewModel, nav) } } } @@ -456,7 +457,7 @@ fun RenderNutzapGallery( ZapAmountCommentNotification( user = note.author, comment = event?.content?.ifBlank { null }, - amount = showAmount(java.math.BigDecimal(sats)), + amount = showAmount(BigDecimal(sats)), zapNote = note, ) }.toImmutableList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 4a214409b8..fccaa8a7fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Tunestr import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nip05_checking import com.vitorpamplona.amethyst.commons.resources.nip05_failed import com.vitorpamplona.amethyst.commons.resources.nip05_verified +import com.vitorpamplona.amethyst.commons.resources.nip_05 import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.LoadNote @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.WatchAuthor +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP @@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.ETag @@ -484,7 +484,7 @@ fun RenderNIP05VerifiedSymbol( is Nip05VerifState.Verified -> { Icon( - painter = painterRes(R.drawable.nip_05, compositionSizeReference), + painter = painterRes(Res.drawable.nip_05, compositionSizeReference), contentDescription = stringRes(id = Res.string.nip05_verified), modifier = modifier, tint = MaterialTheme.colorScheme.nip05, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 073034a51c..c0354ab756 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.NoteComposeLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel @@ -109,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.StaleRelayHint import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync @@ -465,8 +467,6 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import java.text.SimpleDateFormat -import java.util.Date @Composable fun NoteCompose( @@ -2126,7 +2126,7 @@ fun DisplayExpiration( accountViewModel.toastManager.toast( Res.string.expiration_date_label, Res.string.expiration_info_description, - SimpleDateFormat.getDateTimeInstance().format(Date(expirationDate * 1000)), + formatDateTime(expirationDate * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.MEDIUM), ) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 0b9c7c09b7..2ef0f46ce4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note -import android.content.Intent -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -55,14 +53,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup -import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -102,6 +97,8 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_unfollow import com.vitorpamplona.amethyst.commons.resources.quick_action_unmute_thread import com.vitorpamplona.amethyst.commons.resources.report_dialog_block_hide_user_btn import com.vitorpamplona.amethyst.commons.resources.report_dialog_blocking_a_user +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -114,7 +111,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.secondaryButtonBackground import com.vitorpamplona.amethyst.commons.util.njumpLink import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -122,17 +118,38 @@ import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource +import kotlin.math.abs +/** [color] with its HSL lightness raised by [amount], opaque, as `ColorUtils` computed it. */ private fun lightenColor( color: Color, amount: Float, ): Color { - var argb = color.toArgb() - val hslOut = floatArrayOf(0f, 0f, 0f) - ColorUtils.colorToHSL(argb, hslOut) - hslOut[2] += amount - argb = ColorUtils.HSLToColor(hslOut) - return Color(argb) + val r = color.red + val g = color.green + val b = color.blue + val max = maxOf(r, g, b) + val min = minOf(r, g, b) + val delta = max - min + val lightness = (max + min) / 2f + + var hue = 0f + var saturation = 0f + if (delta != 0f) { + hue = + when (max) { + r -> ((g - b) / delta).mod(6f) + g -> (b - r) / delta + 2f + else -> (r - g) / delta + 4f + } * 60f + saturation = delta / (1f - abs(2f * lightness - 1f)) + } + + return Color.hsl( + hue = hue.mod(360f), + saturation = saturation.coerceIn(0f, 1f), + lightness = (lightness + amount).coerceIn(0f, 1f), + ) } val externalLinkForUser = { user: User -> @@ -289,19 +306,15 @@ fun CardBody( ) { val quickActionShareBrowserLinkStr = stringRes(Res.string.quick_action_share_browser_link) val quickActionShareStr = stringRes(Res.string.quick_action_share) - val context = LocalContext.current + val notice = rememberShortNotice() + val sharer = rememberTextSharer() val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f) val clipboardManager = LocalClipboard.current val scope = rememberCoroutineScope() val showToast = { stringRes: StringResource -> scope.launch { - Toast - .makeText( - context, - loadStringRes(stringRes), - Toast.LENGTH_SHORT, - ).show() + notice.show(loadStringRes(stringRes)) } } @@ -492,26 +505,7 @@ fun CardBody( icon = MaterialSymbols.Share, label = stringRes(Res.string.quick_action_share), ) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - quickActionShareStr, - ) - context.startActivity(shareIntent) + sharer.share(externalLinkForNote(note), quickActionShareBrowserLinkStr, quickActionShareStr) onDismiss() } } @@ -675,35 +669,6 @@ fun QuickActionAlertDialog( ) } -@Composable -fun QuickActionAlertDialog( - title: String, - textContent: String, - buttonIconResource: Int, - buttonIconReference: Int, - buttonText: String, - buttonColors: ButtonColors = ButtonDefaults.buttonColors(), - onClickDoOnce: () -> Unit, - onClickDontShowAgain: () -> Unit, - onDismiss: () -> Unit, -) { - QuickActionAlertDialog( - title = title, - textContent = textContent, - icon = { - Icon( - painter = painterRes(buttonIconResource, buttonIconReference), - contentDescription = null, - ) - }, - buttonText = buttonText, - buttonColors = buttonColors, - onClickDoOnce = onClickDoOnce, - onClickDontShowAgain = onClickDontShowAgain, - onDismiss = onDismiss, - ) -} - @Composable fun QuickActionAlertDialog( title: String, @@ -774,33 +739,6 @@ fun QuickActionAlertDialogOneButton( ) } -@Composable -fun QuickActionAlertDialogOneButton( - title: String, - textContent: String, - buttonIconResource: Int, - buttonIconReference: Int, - buttonText: String, - buttonColors: ButtonColors = ButtonDefaults.buttonColors(), - onClickDoOnce: () -> Unit, - onDismiss: () -> Unit, -) { - QuickActionAlertDialogOneButton( - title = title, - textContent = textContent, - icon = { - Icon( - painter = painterRes(buttonIconResource, buttonIconReference), - contentDescription = null, - ) - }, - buttonText = buttonText, - buttonColors = buttonColors, - onClickDoOnce = onClickDoOnce, - onDismiss = onDismiss, - ) -} - @Composable fun QuickActionAlertDialogOneButton( title: String, 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 fd1ad4c353..d2f83d0952 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 @@ -120,8 +120,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow 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.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt index 494baa85ec..9ffd1bcf52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -110,9 +109,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder 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.mockVitorAccountViewModel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList @@ -544,7 +543,6 @@ fun ZapVote( var showErrorMessageDialog by remember { mutableStateOf(null) } - val context = LocalContext.current val scope = rememberCoroutineScope() nonClickablePrepend() @@ -603,9 +601,9 @@ fun ZapVote( onPayViaIntent = { if (it.size == 1) { val payable = it.first() - payViaIntent(payable.invoice, context, noWalletFoundStr, { }) { error -> + if (!accountViewModel.host.openLightningWallet(payable.invoice)) { zappingProgress = 0f - showErrorMessageDialog = StringToastMsg(errorDialogZapErrorStr, error) + showErrorMessageDialog = StringToastMsg(errorDialogZapErrorStr, noWalletFoundStr) } } else { val uid = Uuid.random().toString() @@ -624,7 +622,6 @@ fun ZapVote( ), ) { if (wantsToZap) { - val context = LocalContext.current FilteredZapAmountChoicePopup( baseNote, accountViewModel, @@ -644,9 +641,9 @@ fun ZapVote( onPayViaIntent = { if (it.size == 1) { val payable = it.first() - payViaIntent(payable.invoice, context, noWalletFoundStr, { }) { error -> + if (!accountViewModel.host.openLightningWallet(payable.invoice)) { zappingProgress = 0f - showErrorMessageDialog = StringToastMsg(errorDialogZapErrorStr, error) + showErrorMessageDialog = StringToastMsg(errorDialogZapErrorStr, noWalletFoundStr) } } else { val uid = Uuid.random().toString() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt index d6df75b8f6..f2eb594dcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt @@ -32,17 +32,21 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent +import com.vitorpamplona.quartz.utils.BigDecimal import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.plus +import com.vitorpamplona.quartz.utils.toDoubleValue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.math.BigDecimal -import java.math.RoundingMode +import kotlin.math.roundToLong + +private val ZERO = BigDecimal(0) @Stable data class PollOption( val option: Int, val descriptor: String, - var zappedValue: MutableState = mutableStateOf(BigDecimal.ZERO), + var zappedValue: MutableState = mutableStateOf(ZERO), var tally: MutableState = mutableFloatStateOf(0f), var consensusThreadhold: MutableState = mutableStateOf(false), var zappedByLoggedIn: MutableState = mutableStateOf(false), @@ -57,13 +61,11 @@ class PollNoteViewModel : ViewModel() { private var pollOptions: Map? = null private var valueMaximum: Long? = null private var valueMinimum: Long? = null - private var valueMaximumBD: BigDecimal? = null - private var valueMinimumBD: BigDecimal? = null private var closedAt: Long? = null - private var consensusThreshold: BigDecimal? = null + private var consensusThreshold: Double? = null - private var totalZapped: BigDecimal = BigDecimal.ZERO + private var totalZapped: BigDecimal = ZERO private var wasZappedByLoggedInAccount: Boolean = false var canZap = mutableStateOf(false) @@ -80,12 +82,10 @@ class PollNoteViewModel : ViewModel() { pollOptions = pollEvent?.pollOptions() valueMaximum = pollEvent?.maxAmount() valueMinimum = pollEvent?.minAmount() - valueMinimumBD = valueMinimum?.let { BigDecimal(it) } - valueMaximumBD = valueMaximum?.let { BigDecimal(it) } - consensusThreshold = pollEvent?.consensusThreshold()?.toBigDecimal() + consensusThreshold = pollEvent?.consensusThreshold() closedAt = pollEvent?.closedAt() - totalZapped = BigDecimal.ZERO + totalZapped = ZERO wasZappedByLoggedInAccount = false canZap.value = checkIfCanZap() @@ -108,16 +108,18 @@ class PollNoteViewModel : ViewModel() { tallies.forEach { val zappedValue = zappedPollOptionAmount(it.option) + val total = totalZapped.toDoubleValue() + // The share, rounded to two decimals. val tallyValue = - if (totalZapped > BigDecimal.ZERO) { - zappedValue.divide(totalZapped, 2, RoundingMode.HALF_UP) + if (total > 0) { + (zappedValue.toDoubleValue() / total * 100).roundToLong() / 100.0 } else { - BigDecimal.ZERO + 0.0 } it.zappedValue.value = zappedValue it.tally.value = tallyValue.toFloat() - it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!! + it.consensusThreadhold.value = consensusThreshold?.let { threshold -> tallyValue >= threshold } == true it.zappedByLoggedIn.value = account.userProfile().let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } } } @@ -157,36 +159,16 @@ class PollNoteViewModel : ViewModel() { } fun isValidInputVoteAmount(amount: BigDecimal?): Boolean { - when { - amount == null -> { - return false - } - - valueMinimum == null && valueMaximum == null -> { - if (amount > BigDecimal.ZERO) { - return true - } - } - - valueMinimum == null -> { - if (amount > BigDecimal.ZERO && amount <= valueMaximumBD!!) { - return true - } - } - - valueMaximum == null -> { - if (amount >= valueMinimumBD!!) { - return true - } - } - - else -> { - if ((valueMinimumBD!! <= amount) && (amount <= valueMaximumBD!!)) { - return true - } - } + if (amount == null) return false + val sats = amount.toDoubleValue() + val min = valueMinimum + val max = valueMaximum + return when { + min == null && max == null -> sats > 0 + min == null -> sats > 0 && sats <= max!! + max == null -> sats >= min + else -> min <= sats && sats <= max } - return false } fun isValidInputVoteAmount(amount: Long?): Boolean { @@ -243,32 +225,32 @@ class PollNoteViewModel : ViewModel() { } private fun zappedPollOptionAmount(option: Int): BigDecimal = - pollNote?.zaps?.values?.sumOf { + pollNote?.zaps?.values?.fold(ZERO) { acc, it -> val event = it?.event as? ZapReceiptEvent - val zapAmount = event?.amount ?: BigDecimal.ZERO + val zapAmount = event?.amount ?: ZERO val isValidAmount = isValidInputVoteAmount(event?.amount) if (isValidAmount && event?.zappedPollOption() == option) { - zapAmount + acc + zapAmount } else { - BigDecimal.ZERO + acc } } - ?: BigDecimal.ZERO + ?: ZERO private fun totalZapped(): BigDecimal = - pollNote?.zaps?.values?.sumOf { + pollNote?.zaps?.values?.fold(ZERO) { acc, it -> val zapEvent = (it?.event as? ZapReceiptEvent) - val zapAmount = zapEvent?.amount ?: BigDecimal.ZERO + val zapAmount = zapEvent?.amount ?: ZERO val isValidAmount = isValidInputVoteAmount(zapEvent?.amount) if (isValidAmount && zapEvent?.zappedPollOption() != null) { - zapAmount + acc + zapAmount } else { - BigDecimal.ZERO + acc } } - ?: BigDecimal.ZERO + ?: ZERO fun createZapOptionsThatMatchThePollingParameters(zapPaymentChoices: List): List { val options = 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 e3630ae4a6..705064e1bb 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 @@ -247,7 +247,7 @@ private fun MyLoadUrlPreviewDirect( ) Text( - text = state.previewInfo.verifiedUrl?.host ?: state.previewInfo.title, + text = state.previewInfo.verifiedHost ?: state.previewInfo.title, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index 43d75e7690..6113e1c9a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer 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.ui.screen.loggedIn.mockAccountViewModel +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/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 23a88306b0..dbc582a811 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserBanner @@ -37,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for import com.vitorpamplona.amethyst.commons.resources.profile_banner import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.WatchAuthor +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SimpleHeaderImage import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp @@ -44,7 +44,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.authorNotePictureForImageHeader import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.painterRes @Composable fun DefaultImageHeader( @@ -145,7 +144,7 @@ fun DefaultProfileBanner( sizeReference: Int, ) { Image( - painter = painterRes(R.drawable.profile_banner, sizeReference), + painter = painterRes(Res.drawable.profile_banner, sizeReference), contentDescription = stringRes(Res.string.profile_banner), contentScale = ContentScale.Crop, modifier = modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index fbb3eb0fdf..ffc2275f94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -28,13 +28,13 @@ import com.vitorpamplona.amethyst.commons.resources.ots_info_description import com.vitorpamplona.amethyst.commons.resources.ots_info_pending_description import com.vitorpamplona.amethyst.commons.resources.ots_info_title import com.vitorpamplona.amethyst.commons.resources.timestamp_pending_short +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.commons.ui.note.LoadOts +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import java.text.SimpleDateFormat -import java.util.Date /** * Compact pill showing the note's NIP-03 OpenTimestamps proof: how long ago @@ -60,7 +60,7 @@ fun DisplayOts( accountViewModel.toastManager.toast( Res.string.ots_info_title, Res.string.ots_info_description, - SimpleDateFormat.getDateTimeInstance().format(Date(unixtimestamp * 1000)), + formatDateTime(unixtimestamp * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.MEDIUM), ) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index d3845964de..164dcea4a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -68,9 +68,10 @@ import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange 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.quartz.utils.BigDecimal +import com.vitorpamplona.quartz.utils.parseBigDecimalOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.math.BigDecimal @Stable data class Reward( val amount: BigDecimal, @@ -150,7 +151,7 @@ class AddBountyAmountViewModel : ViewModel() { } fun sendPost() { - val newValue = nextAmount.text.trim().toBigDecimalOrNull() + val newValue = parseBigDecimalOrNull(nextAmount.text.trim()) if (newValue != null) { viewModelScope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt index 8776cce3ec..ba880726ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt @@ -74,11 +74,11 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.LightRedColor +import com.vitorpamplona.amethyst.commons.util.toPrettyJson import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialogOneButton import com.vitorpamplona.amethyst.ui.note.copyNoteTextAction import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent -import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -210,7 +210,7 @@ fun noteActionSections( val event = note.event if (event != null) { scope.launch { - val json = withContext(Dispatchers.Default) { JacksonMapper.toJsonPretty(event) } + val json = withContext(Dispatchers.Default) { event.toPrettyJson() } clipboardManager.setText(json) handlers.onDismiss() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index 37aa101854..953e38baea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav 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.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.ui.note.FirstUserInfoRow import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EmptyState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt index 7a9b54fd06..42ac8cb6d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ShareActions.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import android.content.Intent import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -34,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image import com.vitorpamplona.amethyst.commons.resources.share_as_image_url import com.vitorpamplona.amethyst.commons.resources.share_as_qr import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.note.externalLinkForNote @@ -59,21 +58,13 @@ fun ShareActionRows( ) { val quickActionShareBrowserLinkStr = stringRes(Res.string.quick_action_share_browser_link) val quickActionShareStr = stringRes(Res.string.quick_action_share) - val actContext = LocalContext.current + val sharer = rememberTextSharer() // AddressableNotes are shared by their replaceable address; everything else // by event id. The two image routes resolve the note from this same id. val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, externalLinkForNote(note)) - putExtra(Intent.EXTRA_TITLE, quickActionShareBrowserLinkStr) - } - val shareIntent = Intent.createChooser(sendIntent, quickActionShareStr) - actContext.startActivity(shareIntent) + sharer.share(externalLinkForNote(note), quickActionShareBrowserLinkStr, quickActionShareStr) onDismiss() } M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(Res.string.share_as_image)) { 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 new file mode 100644 index 0000000000..9664a765ae --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt @@ -0,0 +1,562 @@ +/* + * 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.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +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.WaveformData +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.podcasts.PodcastAudio +import kotlinx.collections.immutable.ImmutableList +import com.vitorpamplona.amethyst.service.playback.composable.VideoView as AppVideoView +import com.vitorpamplona.amethyst.ui.actions.EditPostView as AppEditPostView +import com.vitorpamplona.amethyst.ui.components.GifVideoView as AppGifVideoView +import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview as AppLoadUrlPreview +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView as AppZoomableContentView +import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog as AppZoomableImageDialog +import com.vitorpamplona.amethyst.ui.note.LikeReaction as AppLikeReaction +import com.vitorpamplona.amethyst.ui.note.ReactionsRow as AppReactionsRow +import com.vitorpamplona.amethyst.ui.note.ZapReaction as AppZapReaction +import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName as AppLoadCityName +import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap as AppLocationPreviewMap +import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer as AppPodcastEpisodeAudioPlayer +import com.vitorpamplona.amethyst.ui.note.types.RenderAudioFromIMeta as AppRenderAudioFromIMeta +import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack as AppRenderAudioTrack +import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWithWaveform as AppRenderAudioWithWaveform +import com.vitorpamplona.amethyst.ui.note.types.RenderChessGame as AppRenderChessGame +import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent as AppRenderGitIssueEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent as AppRenderGitPatchEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitPullRequestEvent as AppRenderGitPullRequestEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitPullRequestUpdateEvent as AppRenderGitPullRequestUpdateEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent as AppRenderGitRepositoryEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessChallenge as AppRenderLiveChessChallenge +import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd as AppRenderLiveChessGameEnd +import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent as AppRenderMeetingRoomEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomPresence as AppRenderMeetingRoomPresence +import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent as AppRenderMeetingSpaceEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack as AppRenderMusicTrack +import com.vitorpamplona.amethyst.ui.note.types.RenderNamedNappletEvent as AppRenderNamedNappletEvent +import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent as AppRenderNamedSiteEvent +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 + +/** Android's [NotePlatform]: the app's own media, map, audio, chess, git, napplet and zap composables. */ +object AndroidNotePlatform : NotePlatform { + @Composable + override fun ZoomableContentView( + content: BaseMediaContent, + images: ImmutableList, + roundedCorner: Boolean, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + ) = AppZoomableContentView( + content = content, + images = images, + roundedCorner = roundedCorner, + contentScale = contentScale, + accountViewModel = accountViewModel, + ) + + @Composable + override fun ZoomableImageDialog( + imageUrl: BaseMediaContent, + allImages: ImmutableList, + sourceBounds: Rect?, + onDismiss: () -> Unit, + accountViewModel: AccountViewModel, + ) = AppZoomableImageDialog( + imageUrl = imageUrl, + allImages = allImages, + sourceBounds = sourceBounds, + onDismiss = onDismiss, + accountViewModel = accountViewModel, + ) + + @Composable + override fun GifVideoView( + videoUri: String, + contentDescription: String?, + dimensions: DimensionTag?, + blurhash: String?, + roundedCorner: Boolean, + contentScale: ContentScale, + onDialog: (() -> Unit)?, + accountViewModel: AccountViewModel, + thumbhash: String?, + ) = AppGifVideoView( + videoUri = videoUri, + contentDescription = contentDescription, + dimensions = dimensions, + blurhash = blurhash, + roundedCorner = roundedCorner, + contentScale = contentScale, + onDialog = onDialog, + accountViewModel = accountViewModel, + thumbhash = thumbhash, + ) + + @Composable + override fun VideoView( + videoUri: String, + mimeType: String?, + title: String?, + artworkUri: String?, + authorName: String?, + roundedCorner: Boolean, + contentScale: ContentScale, + nostrUriCallback: String?, + isLiveStream: Boolean, + accountViewModel: AccountViewModel, + ) = AppVideoView( + videoUri = videoUri, + mimeType = mimeType, + title = title, + artworkUri = artworkUri, + authorName = authorName, + roundedCorner = roundedCorner, + contentScale = contentScale, + nostrUriCallback = nostrUriCallback, + isLiveStream = isLiveStream, + accountViewModel = accountViewModel, + ) + + @Composable + override fun UrlPreview( + url: String, + urlText: String, + callbackUri: String?, + accountViewModel: AccountViewModel, + nav: INav?, + ) = AppLoadUrlPreview( + url = url, + urlText = urlText, + callbackUri = callbackUri, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun LocationPreviewMap( + latitude: Double, + longitude: Double, + aspectRatio: Float, + pinColor: Color?, + pinEmoji: String?, + pinAlpha: Float, + ) = AppLocationPreviewMap( + latitude = latitude, + longitude = longitude, + aspectRatio = aspectRatio, + pinColor = pinColor, + pinEmoji = pinEmoji, + pinAlpha = pinAlpha, + ) + + @Composable + override fun LoadCityName( + geohashStr: String, + onLoading: (@Composable () -> Unit)?, + content: @Composable (String) -> Unit, + ) = AppLoadCityName( + geohashStr = geohashStr, + onLoading = onLoading, + content = content, + ) + + @Composable + override fun RenderAudioTrack( + note: Note, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderAudioTrack( + note = note, + contentScale = contentScale, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderMusicTrack( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderMusicTrack( + note = note, + makeItShort = makeItShort, + canPreview = canPreview, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun PodcastEpisodeAudioPlayer( + audio: PodcastAudio, + note: Note, + title: String?, + image: String?, + borderModifier: Modifier, + accountViewModel: AccountViewModel, + ) = AppPodcastEpisodeAudioPlayer( + audio = audio, + note = note, + title = title, + image = image, + borderModifier = borderModifier, + accountViewModel = accountViewModel, + ) + + @Composable + override fun RenderVoiceTrack( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderVoiceTrack( + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderAudioFromIMeta( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderAudioFromIMeta( + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderAudioWithWaveform( + mediaUrl: String, + title: String?, + mimeType: String?, + waveform: WaveformData?, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderAudioWithWaveform( + mediaUrl = mediaUrl, + title = title, + mimeType = mimeType, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderChessGame( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderChessGame( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderLiveChessChallenge( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderLiveChessChallenge( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderLiveChessGameEnd( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderLiveChessGameEnd( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderGitRepositoryEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderGitPatchEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderGitIssueEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderGitIssueEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderGitPullRequestEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderGitPullRequestEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderGitPullRequestUpdateEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderGitPullRequestUpdateEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderMeetingSpaceEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderMeetingSpaceEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderMeetingRoomEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderMeetingRoomEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderMeetingRoomPresence( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderMeetingRoomPresence( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderRootNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderRootNappletEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderNamedNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderNamedNappletEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderRootSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderRootSiteEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun RenderNamedSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppRenderNamedSiteEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun ReactionsRow( + baseNote: Note, + showReactionDetail: Boolean, + addPadding: Boolean, + editState: State>?, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppReactionsRow( + baseNote = baseNote, + showReactionDetail = showReactionDetail, + addPadding = addPadding, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, + ) + + @Composable + override fun LikeReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + nav: INav, + iconSize: Dp, + heartSizeModifier: Modifier, + iconFontSize: TextUnit, + showCounter: Boolean, + ) = AppLikeReaction( + baseNote = baseNote, + grayTint = grayTint, + accountViewModel = accountViewModel, + nav = nav, + iconSize = iconSize, + heartSizeModifier = heartSizeModifier, + iconFontSize = iconFontSize, + showCounter = showCounter, + ) + + @Composable + override fun ZapReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + iconSize: Dp, + iconSizeModifier: Modifier, + animationModifier: Modifier, + showCounter: Boolean, + nav: INav, + ) = AppZapReaction( + baseNote = baseNote, + grayTint = grayTint, + accountViewModel = accountViewModel, + iconSize = iconSize, + iconSizeModifier = iconSizeModifier, + animationModifier = animationModifier, + showCounter = showCounter, + nav = nav, + ) + + @Composable + override fun EditPostView( + onClose: () -> Unit, + edit: Note, + versionLookingAt: Note?, + accountViewModel: AccountViewModel, + nav: INav, + ) = AppEditPostView( + onClose = onClose, + edit = edit, + versionLookingAt = versionLookingAt, + accountViewModel = accountViewModel, + nav = nav, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatform.kt new file mode 100644 index 0000000000..e9202147f7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatform.kt @@ -0,0 +1,372 @@ +/* + * 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.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +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.WaveformData +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.ui.note.types.EditState +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.podcasts.PodcastAudio +import kotlinx.collections.immutable.ImmutableList + +/** + * The platform halves of the shared note renderer: what a note card needs that only the front end + * can draw or do. Media players and the zoomable viewer, maps and reverse geocoding, the note types + * built on a platform engine (audio players, chess, the git browser, meeting rooms, napplets), the + * reactions/zap row with its wallet hand-offs, and the post editor. + * + * The shared note code calls these through [LocalNotePlatform]; a front end installs its own at the + * composition root (Android does in its theme). Every member defaults to drawing nothing, so a + * preview, or a front end still wiring a piece, gets an empty slot rather than a crash. + */ +@Stable +interface NotePlatform { + // Media + + @Composable + fun ZoomableContentView( + content: BaseMediaContent, + images: ImmutableList, + roundedCorner: Boolean, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + ) {} + + @Composable + fun ZoomableImageDialog( + imageUrl: BaseMediaContent, + allImages: ImmutableList, + sourceBounds: Rect?, + onDismiss: () -> Unit, + accountViewModel: AccountViewModel, + ) {} + + @Composable + fun GifVideoView( + videoUri: String, + contentDescription: String?, + dimensions: DimensionTag?, + blurhash: String?, + roundedCorner: Boolean, + contentScale: ContentScale, + onDialog: (() -> Unit)?, + accountViewModel: AccountViewModel, + thumbhash: String?, + ) {} + + @Composable + fun VideoView( + videoUri: String, + mimeType: String?, + title: String?, + artworkUri: String?, + authorName: String?, + roundedCorner: Boolean, + contentScale: ContentScale, + nostrUriCallback: String?, + isLiveStream: Boolean, + accountViewModel: AccountViewModel, + ) {} + + /** The link card for [url]: OpenGraph preview, inline media, or a plain link. */ + @Composable + fun UrlPreview( + url: String, + urlText: String, + callbackUri: String?, + accountViewModel: AccountViewModel, + nav: INav?, + ) {} + + // Places + + @Composable + fun LocationPreviewMap( + latitude: Double, + longitude: Double, + aspectRatio: Float, + pinColor: Color?, + pinEmoji: String?, + pinAlpha: Float, + ) {} + + /** Resolves [geohashStr] to a place name for [content], showing [onLoading] meanwhile. */ + @Composable + fun LoadCityName( + geohashStr: String, + onLoading: (@Composable () -> Unit)?, + content: @Composable (String) -> Unit, + ) {} + + // Audio + + @Composable + fun RenderAudioTrack( + note: Note, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderMusicTrack( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun PodcastEpisodeAudioPlayer( + audio: PodcastAudio, + note: Note, + title: String?, + image: String?, + borderModifier: Modifier, + accountViewModel: AccountViewModel, + ) {} + + @Composable + fun RenderVoiceTrack( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderAudioFromIMeta( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderAudioWithWaveform( + mediaUrl: String, + title: String?, + mimeType: String?, + waveform: WaveformData?, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + // Chess + + @Composable + fun RenderChessGame( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderLiveChessChallenge( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderLiveChessGameEnd( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + // Git + + @Composable + fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderGitIssueEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderGitPullRequestEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderGitPullRequestUpdateEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + // Meeting rooms (audio spaces) + + @Composable + fun RenderMeetingSpaceEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderMeetingRoomEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderMeetingRoomPresence( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + // Napplets and nsites + + @Composable + fun RenderRootNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderNamedNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderRootSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun RenderNamedSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + // Reactions, zaps and editing + + @Composable + fun ReactionsRow( + baseNote: Note, + showReactionDetail: Boolean, + addPadding: Boolean, + editState: State>?, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + @Composable + fun LikeReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + nav: INav, + iconSize: Dp, + heartSizeModifier: Modifier, + iconFontSize: TextUnit, + showCounter: Boolean, + ) {} + + @Composable + fun ZapReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + iconSize: Dp, + iconSizeModifier: Modifier, + animationModifier: Modifier, + showCounter: Boolean, + nav: INav, + ) {} + + /** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */ + @Composable + fun EditPostView( + onClose: () -> Unit, + edit: Note, + versionLookingAt: Note?, + accountViewModel: AccountViewModel, + nav: INav, + ) {} + + /** Draws nothing: previews, and front ends still wiring their pieces. */ + object None : NotePlatform +} + +/** The front end's [NotePlatform]. Static: it is set once at the root and never changes. */ +val LocalNotePlatform = staticCompositionLocalOf { NotePlatform.None } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatformCalls.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatformCalls.kt new file mode 100644 index 0000000000..5d0b6b65ec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/NotePlatformCalls.kt @@ -0,0 +1,538 @@ +/* + * 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.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +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.WaveformData +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP +import com.vitorpamplona.amethyst.commons.ui.theme.Size14Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size18dp +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.amethyst.ui.note.types.EditState +import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.podcasts.PodcastAudio +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +// 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. + +@Composable +fun ZoomableContentView( + content: BaseMediaContent, + images: ImmutableList = remember(content) { persistentListOf(content) }, + roundedCorner: Boolean, + contentScale: ContentScale, + accountViewModel: AccountViewModel, +) = LocalNotePlatform.current.ZoomableContentView( + content = content, + images = images, + roundedCorner = roundedCorner, + contentScale = contentScale, + accountViewModel = accountViewModel, +) + +@Composable +fun ZoomableImageDialog( + imageUrl: BaseMediaContent, + allImages: ImmutableList = remember(imageUrl) { persistentListOf(imageUrl) }, + sourceBounds: Rect? = null, + onDismiss: () -> Unit, + accountViewModel: AccountViewModel, +) = LocalNotePlatform.current.ZoomableImageDialog( + imageUrl = imageUrl, + allImages = allImages, + sourceBounds = sourceBounds, + onDismiss = onDismiss, + accountViewModel = accountViewModel, +) + +@Composable +fun GifVideoView( + videoUri: String, + contentDescription: String?, + dimensions: DimensionTag?, + blurhash: String?, + roundedCorner: Boolean, + contentScale: ContentScale, + onDialog: (() -> Unit)? = null, + accountViewModel: AccountViewModel, + thumbhash: String? = null, +) = LocalNotePlatform.current.GifVideoView( + videoUri = videoUri, + contentDescription = contentDescription, + dimensions = dimensions, + blurhash = blurhash, + roundedCorner = roundedCorner, + contentScale = contentScale, + onDialog = onDialog, + accountViewModel = accountViewModel, + thumbhash = thumbhash, +) + +@Composable +fun VideoView( + videoUri: String, + mimeType: String?, + title: String? = null, + artworkUri: String? = null, + authorName: String? = null, + roundedCorner: Boolean, + contentScale: ContentScale, + nostrUriCallback: String? = null, + isLiveStream: Boolean = false, + accountViewModel: AccountViewModel, +) = LocalNotePlatform.current.VideoView( + videoUri = videoUri, + mimeType = mimeType, + title = title, + artworkUri = artworkUri, + authorName = authorName, + roundedCorner = roundedCorner, + contentScale = contentScale, + nostrUriCallback = nostrUriCallback, + isLiveStream = isLiveStream, + accountViewModel = accountViewModel, +) + +@Composable +fun LoadUrlPreview( + url: String, + urlText: String, + callbackUri: String? = null, + accountViewModel: AccountViewModel, + nav: INav? = null, +) = LocalNotePlatform.current.UrlPreview( + url = url, + urlText = urlText, + callbackUri = callbackUri, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun LocationPreviewMap( + latitude: Double, + longitude: Double, + aspectRatio: Float = 1f, + pinColor: Color? = null, + pinEmoji: String? = null, + pinAlpha: Float = 1f, +) = LocalNotePlatform.current.LocationPreviewMap( + latitude = latitude, + longitude = longitude, + aspectRatio = aspectRatio, + pinColor = pinColor, + pinEmoji = pinEmoji, + pinAlpha = pinAlpha, +) + +@Composable +fun LoadCityName( + geohashStr: String, + onLoading: (@Composable () -> Unit)? = null, + content: @Composable (String) -> Unit, +) = LocalNotePlatform.current.LoadCityName( + geohashStr = geohashStr, + onLoading = onLoading, + content = content, +) + +@Composable +fun RenderAudioTrack( + note: Note, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderAudioTrack( + note = note, + contentScale = contentScale, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderMusicTrack( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderMusicTrack( + note = note, + makeItShort = makeItShort, + canPreview = canPreview, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun PodcastEpisodeAudioPlayer( + audio: PodcastAudio, + note: Note, + title: String?, + image: String?, + borderModifier: Modifier, + accountViewModel: AccountViewModel, +) = LocalNotePlatform.current.PodcastEpisodeAudioPlayer( + audio = audio, + note = note, + title = title, + image = image, + borderModifier = borderModifier, + accountViewModel = accountViewModel, +) + +@Composable +fun RenderVoiceTrack( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderVoiceTrack( + note = note, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderAudioFromIMeta( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderAudioFromIMeta( + note = note, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderAudioWithWaveform( + mediaUrl: String, + title: String?, + mimeType: String?, + waveform: WaveformData?, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderAudioWithWaveform( + mediaUrl = mediaUrl, + title = title, + mimeType = mimeType, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderChessGame( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderChessGame( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderLiveChessChallenge( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderLiveChessChallenge( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderLiveChessGameEnd( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderLiveChessGameEnd( + note = note, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderGitRepositoryEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderGitPatchEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderGitIssueEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderGitIssueEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderGitPullRequestEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderGitPullRequestEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderGitPullRequestUpdateEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderGitPullRequestUpdateEvent( + baseNote = baseNote, + makeItShort = makeItShort, + canPreview = canPreview, + quotesLeft = quotesLeft, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderMeetingSpaceEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderMeetingSpaceEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderMeetingRoomEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderMeetingRoomEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderMeetingRoomPresence( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderMeetingRoomPresence( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderRootNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderRootNappletEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderNamedNappletEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderNamedNappletEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderRootSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderRootSiteEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun RenderNamedSiteEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.RenderNamedSiteEvent( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun ReactionsRow( + baseNote: Note, + showReactionDetail: Boolean, + addPadding: Boolean, + editState: State>?, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.ReactionsRow( + baseNote = baseNote, + showReactionDetail = showReactionDetail, + addPadding = addPadding, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, +) + +@Composable +fun LikeReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + nav: INav, + iconSize: Dp = Size18dp, + heartSizeModifier: Modifier = Size18Modifier, + iconFontSize: TextUnit = Font14SP, + showCounter: Boolean = true, +) = LocalNotePlatform.current.LikeReaction( + baseNote = baseNote, + grayTint = grayTint, + accountViewModel = accountViewModel, + nav = nav, + iconSize = iconSize, + heartSizeModifier = heartSizeModifier, + iconFontSize = iconFontSize, + showCounter = showCounter, +) + +@Composable +fun ZapReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + iconSize: Dp = Size20dp, + iconSizeModifier: Modifier = Size20Modifier, + animationModifier: Modifier = Size14Modifier, + showCounter: Boolean = true, + nav: INav, +) = LocalNotePlatform.current.ZapReaction( + baseNote = baseNote, + grayTint = grayTint, + accountViewModel = accountViewModel, + iconSize = iconSize, + iconSizeModifier = iconSizeModifier, + animationModifier = animationModifier, + showCounter = showCounter, + nav = nav, +) + +@Composable +fun EditPostView( + onClose: () -> Unit, + edit: Note, + versionLookingAt: Note?, + accountViewModel: AccountViewModel, + nav: INav, +) = LocalNotePlatform.current.EditPostView( + onClose = onClose, + edit = edit, + versionLookingAt = versionLookingAt, + accountViewModel = accountViewModel, + nav = nav, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt index 122d25bd84..52c5a755b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.model.ZapAmountCommentNotification import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index ab42b09a87..facefb8d7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -77,7 +77,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -111,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.LinkIcon import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier @@ -118,7 +118,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.KindChip import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindDisplayName import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent @@ -193,7 +192,7 @@ fun RenderAppDefinition( } } else { Image( - painter = painterRes(R.drawable.profile_banner, 6), + painter = painterRes(Res.drawable.profile_banner, 6), contentDescription = stringRes(id = Res.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt index 751fd843aa..7171a4d754 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt @@ -44,7 +44,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.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -72,18 +71,19 @@ import com.vitorpamplona.amethyst.commons.resources.attestor_recommendation_for_ import com.vitorpamplona.amethyst.commons.ui.components.LoadNote 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.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadUser +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UserCompose -import com.vitorpamplona.amethyst.ui.note.formatMediumDate -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.KindChip import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus @@ -199,19 +199,18 @@ fun RenderAttestation( } if (validFrom != null || validTo != null) { - val context = LocalContext.current Spacer(modifier = DoubleVertSpacer) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { validFrom?.let { Text( - text = stringRes(Res.string.attestation_valid_from, formatMediumDate(it, context)), + text = stringRes(Res.string.attestation_valid_from, formatDateTime(it * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.NONE)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } validTo?.let { Text( - text = stringRes(Res.string.attestation_valid_to, formatMediumDate(it, context)), + text = stringRes(Res.string.attestation_valid_to, formatDateTime(it * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.NONE)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index ad41fb4651..4d075a3a27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Bolt12ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Bolt12ZapEvent.kt index ce3fc8ed25..a501c7008d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Bolt12ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Bolt12ZapEvent.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent -import java.text.NumberFormat /** * Standalone card for a NIP-B1 BOLT12 zap (kind 9736), styled like the NIP-57 @@ -91,7 +91,7 @@ fun RenderBolt12Zap( RenderZappedPost(note, quotesLeft, cardBackground, accountViewModel, nav) - amountSats?.let { ActivityAmountRow(NumberFormat.getNumberInstance().format(it), orange) } + amountSats?.let { ActivityAmountRow(PlatformNumberFormatter().format(it), orange) } comment?.let { CrossfadeToDisplayComment(it, cardBackground, nav, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt index 4a9d3c91cc..e7e10d91c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarEvent.kt @@ -42,20 +42,19 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for 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.DateTimeStyle +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date @Composable fun RenderCalendarTimeSlotEvent( @@ -239,11 +238,9 @@ private fun formatTimestampRange( end: Long?, ): String? { if (start == null) return null - val formatter = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) - val startStr = formatter.format(Date(start * 1000)) + val startStr = formatDateTime(start * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT) return if (end != null && end != start) { - val endFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) - "$startStr – ${endFormatter.format(Date(end * 1000))}" + "$startStr – ${formatDateTime(end * 1000, DateTimeStyle.NONE, DateTimeStyle.SHORT)}" } else { startStr } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Citation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Citation.kt index f0620e1c72..b7fa9d92c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Citation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Citation.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.grayText @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.quartz.experimental.citations.CitationEvent import com.vitorpamplona.quartz.experimental.citations.ExternalCitationEvent import com.vitorpamplona.quartz.experimental.citations.HardcopyCitationEvent @@ -137,7 +137,7 @@ fun RenderCitation( // A prompt citation's body is the prompt itself, which the reference implementation // renders as Markdown; the other two carry a plain note. if (noteEvent is PromptCitationEvent) { - RenderContentAsMarkdown( + LocalRichTextPlatform.current.Markdown( content = noteEvent.content, tags = tags, canPreview = canPreview && !makeItShort, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index 5a95ae8410..aab4d90572 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.parseBigDecimalOrNull import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -137,7 +138,7 @@ fun RenderClassifieds( val priceTag = remember(noteEvent) { val newAmount = - price.amount.toBigDecimalOrNull()?.let { showAmount(it) } + parseBigDecimalOrNull(price.amount)?.let { showAmount(it) } ?: price.amount if (price.frequency != null && price.currency != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 5e99f039c8..037f54a95e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import android.content.Intent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -48,7 +47,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -80,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer 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 @@ -542,27 +541,11 @@ fun ShareCommunityButton( ) { val quickActionShareBrowserLinkStr = stringRes(Res.string.quick_action_share_browser_link) val quickActionShareStr = stringRes(Res.string.quick_action_share) - val actContext = LocalContext.current + val sharer = rememberTextSharer() FilledTonalIconButton( onClick = { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, quickActionShareStr) - actContext.startActivity(shareIntent) + sharer.share(externalLinkForNote(note), quickActionShareBrowserLinkStr, quickActionShareStr) }, ) { Icon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt index 6f80a63234..f74946695d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ContactList.kt @@ -44,8 +44,8 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp 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.note.GalleryUnloaded -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index 9e5588742a..06c3192b90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage @@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip31Alts.alt -import java.io.File @Composable fun FileStorageHeaderDisplay( @@ -74,7 +73,7 @@ private fun ObserverAndRenderNIP95( // Creates a new object when the event arrives to force an update of the image. val note = noteState.note val uri = header.toNostrUri() - val localDir = note.idHex.let { File(Amethyst.instance.nip95cache, it) } + val localDir = LocalCache.appHost.nip95Blobs?.path(note.idHex) val blurHash = eventHeader.blurhash() val thumbHash = eventHeader.thumbhash() val dimensions = eventHeader.dimensions() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt index af2870b36f..bdc0fcad3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.GeocacheFoundLogCard import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.tags.geohash.toGeoHash import com.vitorpamplona.quartz.nipCCGeocaching.foundLog.GeocacheFoundLogEvent @@ -117,7 +116,7 @@ fun distanceToCache( /** * A distance formatted no finer than its source can support. * - * The reader's position comes from [LocationState.geohashStateFlow], which is a **5-character + * The reader's position comes from `LocationState.geohashStateFlow`, which is a **5-character * geohash** — a 4.89km × 4.89km cell — so the centroid this measures from can be ~3.5km from * where the reader is standing. Printing metres off that would be fiction: someone standing at * the cache would read "3 km", and a cache 2km away could read "120 m". Anything inside one diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt index 8a6c08de1b..3fa7480e7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt @@ -61,8 +61,9 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent +import com.vitorpamplona.quartz.utils.BigDecimal import com.vitorpamplona.quartz.utils.TimeUtils -import java.math.BigDecimal +import com.vitorpamplona.quartz.utils.toDoubleValue import kotlin.math.roundToInt @Composable @@ -149,12 +150,12 @@ fun GoalProgressBar( val zapsState by observeNoteZaps(note, accountViewModel) var zapraiserStatus by - remember { mutableStateOf(ZapraiserStatus(0F, showAmount(goalAmountSats.toBigDecimal()))) } + remember { mutableStateOf(ZapraiserStatus(0F, showAmount(BigDecimal(goalAmountSats)))) } LaunchedEffect(key1 = zapsState) { zapsState?.note?.let { val newZapAmount = accountViewModel.account.zaps.calculateZappedAmount(note) - var percentage = newZapAmount.div(goalAmountSats.toBigDecimal()).toFloat() + var percentage = (newZapAmount.toDoubleValue() / goalAmountSats).toFloat() if (percentage > 1) percentage = 1f val left = @@ -162,7 +163,7 @@ fun GoalProgressBar( "0" } else { showAmount( - goalAmountSats.toBigDecimal() * BigDecimal(1.0 - percentage), + BigDecimal(goalAmountSats * (1.0 - percentage)), ) } zapraiserStatus = ZapraiserStatus(percentage, left) @@ -193,7 +194,7 @@ fun GoalProgressBar( stringRes( Res.string.goal_progress, totalPercentage, - showAmount(goalAmountSats.toBigDecimal()), + showAmount(BigDecimal(goalAmountSats)), ), fontSize = Font14SP, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 25beb559ee..59dea50e39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -66,11 +65,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.HighlightedQuote import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji 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.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.rememberTranslation import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindName import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor @@ -80,9 +79,9 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.Rfc3986 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.net.URL @Composable fun RenderHighlight( @@ -285,10 +284,31 @@ private fun buildTextFragmentUrl( ): String { val separator = if (baseUrl.contains("#")) "&" else "#" - val prefixPart = trimToFragmentEdge(prefix, keepStart = false)?.let { "${Uri.encode(it)}-," } ?: "" - val suffixPart = trimToFragmentEdge(suffix, keepStart = true)?.let { ",-${Uri.encode(it)}" } ?: "" + val prefixPart = trimToFragmentEdge(prefix, keepStart = false)?.let { "${encodeFragmentComponent(it)}-," } ?: "" + val suffixPart = trimToFragmentEdge(suffix, keepStart = true)?.let { ",-${encodeFragmentComponent(it)}" } ?: "" - return "$baseUrl$separator:~:text=$prefixPart${Uri.encode(exact)}$suffixPart" + return "$baseUrl$separator:~:text=$prefixPart${encodeFragmentComponent(exact)}$suffixPart" +} + +private const val FRAGMENT_SAFE = "_-!.~'()*" +private val HEX_DIGITS = "0123456789ABCDEF".toCharArray() + +/** + * Percent-encodes [text] as UTF-8, leaving only letters, digits and `_-!.~'()*` alone: what + * Android's `Uri.encode` does, so spaces become `%20` (not `+`) and `-`, `,` and `&` cannot be + * mistaken for text-fragment syntax. + */ +private fun encodeFragmentComponent(text: String): String { + val out = StringBuilder(text.length) + for (byte in text.encodeToByteArray()) { + val c = (byte.toInt() and 0xFF).toChar() + if (c < '\u0080' && (c.isLetterOrDigit() || c in FRAGMENT_SAFE)) { + out.append(c) + } else { + out.append('%').append(HEX_DIGITS[(byte.toInt() shr 4) and 0xF]).append(HEX_DIGITS[byte.toInt() and 0xF]) + } + } + return out.toString() } /** @@ -502,14 +522,14 @@ fun DisplayEntryForAUrl( val validatedUrl = remember(url) { try { - URL(url) + Rfc3986.host(url) } catch (_: Exception) { Log.w("Note Compose") { "Invalid URI: $url" } null } } - validatedUrl?.host?.let { host -> + validatedUrl?.let { host -> if (userBase != null) { Text("-", maxLines = 1) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index f4cff71689..6376c105b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -56,6 +56,8 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.live_stream_has_ended import com.vitorpamplona.amethyst.commons.resources.live_stream_is_offline import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for +import com.vitorpamplona.amethyst.commons.ui.components.CheckIfVideoIsOnline +import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -69,14 +71,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt index b3e24b63c2..04bceecaba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.commons.util.DecimalPatternFormatter import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -63,9 +64,6 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.text.DecimalFormat -import java.text.NumberFormat -import java.util.Locale import kotlin.math.abs @Preview @@ -258,7 +256,7 @@ fun RenderEyeGlassesPrescription( visionPrescription.status?.let { Text( text = "Status: ${it.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() + if (it.isLowerCase()) it.titlecase() else it.toString() }}", modifier = Modifier.padding(4.dp).fillMaxWidth(), ) @@ -350,9 +348,9 @@ fun RenderEyeGlassesPrescriptionHeaderRow() { @Composable fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) { - val numberFormat = DecimalFormat("##.00") - val pdFormat = DecimalFormat("##.0") - val integerFormat = DecimalFormat("###") + val numberFormat = DecimalPatternFormatter("##.00") + val pdFormat = DecimalPatternFormatter("##.0") + val integerFormat = DecimalPatternFormatter("###") Row( modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), @@ -362,7 +360,7 @@ fun RenderEyeGlassesPrescriptionRow(data: LensSpecification) { Text( text = data.eye?.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() + if (it.isLowerCase()) it.titlecase() else it.toString() } ?: "Unknown", modifier = Modifier.padding(4.dp).weight(1f), ) @@ -510,8 +508,8 @@ fun RenderEyeContactsPrescriptionHeaderRow() { @Composable fun RenderEyeContactsPrescriptionRow(data: LensSpecification) { - val numberFormat = DecimalFormat("##.00") - val integerFormat = DecimalFormat("###") + val numberFormat = DecimalPatternFormatter("##.00") + val integerFormat = DecimalPatternFormatter("###") Row( modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), @@ -521,7 +519,7 @@ fun RenderEyeContactsPrescriptionRow(data: LensSpecification) { Text( text = data.eye?.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() + if (it.isLowerCase()) it.titlecase() else it.toString() } ?: "Unknown", modifier = Modifier.padding(4.dp).weight(1f), ) @@ -626,7 +624,7 @@ fun RenderEyeContactsPrescriptionRow(data: LensSpecification) { fun formatOrBlank( amount: Double?, - numberFormat: NumberFormat, + numberFormat: DecimalPatternFormatter, ): String { if (amount == null) return "" if (abs(amount) < 0.01) return "" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index 45b805e6c5..bbf7e5c14b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,11 +42,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration +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.compose.ui.unit.sp -import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -68,11 +69,14 @@ import com.vitorpamplona.amethyst.commons.resources.nest_presence_listening import com.vitorpamplona.amethyst.commons.resources.nest_presence_on_stage import com.vitorpamplona.amethyst.commons.resources.nest_presence_raised_hand import com.vitorpamplona.amethyst.commons.resources.nest_presence_speaking +import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled 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.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp @@ -80,11 +84,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag as MeetingSpaceStatusTag @@ -213,17 +217,10 @@ private fun ListenToRecordingButton( url: String, accountViewModel: AccountViewModel, ) { - val context = androidx.compose.ui.platform.LocalContext.current + val uriHandler = LocalUriHandler.current val noAppMessage = stringRes(Res.string.nest_no_app_to_open_link) - androidx.compose.material3.OutlinedButton(onClick = { - val launched = - runCatching { - context.startActivity( - android.content - .Intent(android.content.Intent.ACTION_VIEW, url.toUri()) - .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK), - ) - }.isSuccess + OutlinedButton(onClick = { + val launched = runCatching { uriHandler.openUri(url) }.isSuccess if (!launched) { accountViewModel.toastManager.toast( Res.string.nest_chat_send_failed_title, @@ -564,13 +561,11 @@ fun MeetingSpacePlannedFlag( .padding(horizontal = 5.dp) }, ) - val now = System.currentTimeMillis() / 1000L + val now = TimeUtils.now() if (startsUnixSec != null && startsUnixSec > now) { val pretty = remember(startsUnixSec) { - java.text.DateFormat - .getDateTimeInstance(java.text.DateFormat.MEDIUM, java.text.DateFormat.SHORT) - .format(java.util.Date(startsUnixSec * 1000L)) + formatDateTime(startsUnixSec * 1000L, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT) } Text( text = stringRes(Res.string.meeting_space_planned_starts_at, pretty), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt index 05a30c8c97..eae1431c3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt @@ -87,11 +87,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBanner import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBannerBackground -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import kotlinx.coroutines.Dispatchers 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 a495e6188a..fcdca9ff83 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 @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhenInBackground @@ -81,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel 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/Nutzap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt index c3465e4faa..40a50ae9aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal -import java.math.BigDecimal +import com.vitorpamplona.quartz.utils.BigDecimal /** * Renders a NIP-61 nutzap (kind 9321) as an activity card, like lightning zaps. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt index 9027c2ba66..b5619e4d6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt @@ -77,13 +77,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinTx import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.text.NumberFormat private const val MEMPOOL_TX_URL = "https://mempool.space/tx/" @@ -314,7 +314,7 @@ private fun AmountRow( ) { Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Text( - text = NumberFormat.getNumberInstance().format(sats), + text = PlatformNumberFormatter().format(sats), style = MaterialTheme.typography.displaySmall, fontWeight = FontWeight.ExtraBold, color = orange, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersSection.kt index 3524ea97a3..95f7dd6c03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersSection.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.podcasts.fetchPreviewText import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_chapters import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -50,13 +51,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.podcasts.PodcastChapter import com.vitorpamplona.quartz.podcasts.PodcastChapters -import com.vitorpamplona.quartz.utils.Log -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.coroutines.executeAsync private sealed interface ChaptersUiState { data object Loading : ChaptersUiState @@ -79,8 +73,7 @@ fun PodcastChaptersSection( accountViewModel: AccountViewModel, ) { val state by produceState(ChaptersUiState.Loading, chaptersUrl) { - val client = accountViewModel.httpClientBuilder.okHttpClientForPreview(chaptersUrl) - val parsed = loadChapters(chaptersUrl, client) + val parsed = accountViewModel.httpClientBuilder.fetchPreviewText(chaptersUrl)?.let { PodcastChapters.parse(it) } value = if (parsed != null) ChaptersUiState.Loaded(parsed.chapters) else ChaptersUiState.Failed } @@ -133,30 +126,13 @@ fun PodcastChaptersSection( } } -private suspend fun loadChapters( - url: String, - client: OkHttpClient, -): PodcastChapters? = - withContext(Dispatchers.IO) { - try { - val request = Request.Builder().url(url).build() - client.newCall(request).executeAsync().use { response -> - if (response.isSuccessful) PodcastChapters.parse(response.body.string()) else null - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("PodcastChapters", "Failed to load chapters from $url", e) - null - } - } - private fun formatTimestamp(seconds: Long): String { val hours = seconds / 3600 val minutes = (seconds % 3600) / 60 val secs = seconds % 60 return if (hours > 0) { - "%d:%02d:%02d".format(hours, minutes, secs) + "$hours:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}" } else { - "%d:%02d".format(minutes, secs) + "$minutes:${secs.toString().padStart(2, '0')}" } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersView.kt index acd04253fb..25250c4119 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastChaptersView.kt @@ -43,7 +43,7 @@ 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.podcasts.PodcastRemoteContent +import com.vitorpamplona.amethyst.commons.podcasts.fetchPreviewText import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_chapters_count import com.vitorpamplona.amethyst.commons.ui.note.PodcastSoundbites @@ -67,8 +67,7 @@ fun PodcastChaptersView( accountViewModel: AccountViewModel, ) { val chapters by produceState(initialValue = emptyList(), chaptersUrl) { - val client = accountViewModel.httpClientBuilder.okHttpClientForPreview(chaptersUrl) - val body = PodcastRemoteContent.fetchText(chaptersUrl, client) + val body = accountViewModel.httpClientBuilder.fetchPreviewText(chaptersUrl) value = body?.let { PodcastChapters.parse(it)?.chapters }?.filter { it.title?.isNotBlank() == true } ?: emptyList() } 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 b1e72528a5..6cfb83a41d 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 @@ -29,12 +29,12 @@ import androidx.compose.runtime.remember 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.model.Note import com.vitorpamplona.amethyst.commons.ui.note.PodcastSoundbites 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.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.service.playback.composable.wavefront.syntheticWaveformFor import com.vitorpamplona.quartz.podcasts.PodcastAudio diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastTranscriptView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastTranscriptView.kt index 2e9f0eb4f5..a15209574a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastTranscriptView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastTranscriptView.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.podcasts.PodcastRemoteContent +import com.vitorpamplona.amethyst.commons.podcasts.fetchPreviewText import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_transcript import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -58,8 +58,7 @@ fun PodcastTranscriptView( accountViewModel: AccountViewModel, ) { val transcript by produceState(initialValue = null as String?, transcriptUrl) { - val client = accountViewModel.httpClientBuilder.okHttpClientForPreview(transcriptUrl) - val body = PodcastRemoteContent.fetchText(transcriptUrl, client) + val body = accountViewModel.httpClientBuilder.fetchPreviewText(transcriptUrl) value = body?.let { cleanTranscript(it) }?.takeIf { it.isNotBlank() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index defa09630b..d023b6fd32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import android.annotation.SuppressLint import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -100,10 +99,10 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder 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.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType @@ -769,7 +768,7 @@ fun RenderPollManualLongPreview() { } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Preview @Composable fun RenderPollGameResultsPreview() { @@ -845,7 +844,7 @@ fun RenderPollGameResultsPreview() { } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Preview @Composable fun RenderPollColorResultsPreview() { @@ -954,7 +953,7 @@ fun RenderPollColorResultsPreview() { } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Preview @Composable fun RenderPollMultiChoicePreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt index a09fe7d447..76ee6c9114 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCardPreview.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt index 9c3de10a93..0a5c291b52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt @@ -31,9 +31,8 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameMillis import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.asImageBitmap -import androidx.core.graphics.createBitmap import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.service.image.argbPixelsToImageBitmap import com.vitorpamplona.amethyst.commons.ui.note.Ps1SaveCard import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent @@ -92,9 +91,7 @@ private fun Ps1SaveIconImage(icon: Ps1SaveIcon) { val frames = remember(icon) { icon.frames.map { pixels -> - createBitmap(Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE) - .apply { setPixels(pixels, 0, Ps1SaveIcon.SIZE, 0, 0, Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE) } - .asImageBitmap() + argbPixelsToImageBitmap(pixels, Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt index d457e5bb4f..bc8758e8ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt @@ -56,12 +56,12 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadNote 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.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp 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.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent import com.vitorpamplona.quartz.experimental.publications.PublicationSectionRef @@ -129,7 +129,7 @@ fun RenderPublicationSection( if (markdown.isNotBlank()) { val tags = remember(noteEvent) { noteEvent.tags.toImmutableListOfLists() } - RenderContentAsMarkdown( + LocalRichTextPlatform.current.Markdown( content = markdown, tags = tags, canPreview = canPreview && !makeItShort, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 2052d1ad40..0cfe726665 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -88,6 +88,7 @@ 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.ui.theme.subtleBorder +import com.vitorpamplona.amethyst.commons.util.DecimalPatternFormatter import com.vitorpamplona.amethyst.commons.util.prettyMime import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.MediaAspectRatioCache @@ -758,9 +759,9 @@ fun RenderSoftwareAsset( internal fun formatBytes(bytes: Long): String { if (bytes < 1024L) return "$bytes B" val kb = bytes / 1024.0 - if (kb < 1024) return "%.1f KB".format(kb) + if (kb < 1024) return "${DecimalPatternFormatter("0.0").format(kb)} KB" val mb = kb / 1024 - if (mb < 1024) return "%.1f MB".format(mb) + if (mb < 1024) return "${DecimalPatternFormatter("0.0").format(mb)} MB" val gb = mb / 1024 - return "%.2f GB".format(gb) + return "${DecimalPatternFormatter("0.00").format(gb)} GB" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt index c1b77f9294..a3026bea3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt @@ -59,13 +59,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.blackTagModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index 690d9f4ce9..65a1241fca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -41,13 +40,12 @@ 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.platform.LocalUriHandler 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 -import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res @@ -63,9 +61,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.note.getGradient -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip35Torrents.tags.FileTag import kotlinx.collections.immutable.ImmutableList @@ -218,16 +216,14 @@ fun DisplayFileList( modifier = Modifier.fillMaxWidth(), ) - val context = LocalContext.current + val uriHandler = LocalUriHandler.current IconButton( onClick = { try { val linkStr = link() if (linkStr != null) { - val intent = Intent(Intent.ACTION_VIEW, linkStr.toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - context.startActivity(intent) + uriHandler.openUri(linkStr) } else { accountViewModel.toastManager.toast(Res.string.torrent_failure, Res.string.torrent_no_info) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index 006c7db35a..d628986d16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import kotlinx.coroutines.Dispatchers 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 889243d5a8..b55995e9b1 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 @@ -45,6 +45,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW import androidx.media3.ui.compose.state.rememberPlayPauseButtonState +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETT import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhenInBackground -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.playback.composable.controls.AnimatedSaveButton import com.vitorpamplona.amethyst.service.playback.composable.controls.AnimatedShareButton import com.vitorpamplona.amethyst.service.playback.composable.controls.MuteButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt index ccc03b159e..51816005b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -61,9 +61,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking 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 4f5c1ce497..0fcc03aaa3 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 @@ -31,7 +31,7 @@ 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.OnlineChecker +import com.vitorpamplona.amethyst.commons.service.isVideoOnline import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -40,7 +40,6 @@ 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 -import kotlinx.coroutines.withContext // Account actions that need Android (a content Uri, the media store) or the JVM HTTP stack the // app's OkHttp clients come from. They stay out of AccountViewModel so the view model needs neither. @@ -54,10 +53,7 @@ fun AccountViewModel.urlPreview( } } -suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean = - withContext(Dispatchers.IO) { - OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo) - } +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). 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 a438d39913..1dc4d27d54 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 @@ -69,6 +69,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.chats.ui.AutoScrollToNewest import com.vitorpamplona.amethyst.commons.cordn.CordnGroupManager import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -113,7 +114,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding 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.playback.composable.WaveformData import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index 7ca32aae0a..72da6ac804 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.WaveformData import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res @@ -40,7 +40,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWithWaveform @@ -64,7 +63,7 @@ fun RenderEncryptedFile( val mimeType = noteEvent.mimeType() if (algo == AESGCM.NAME && key != null && nonce != null) { - Amethyst.instance.keyCache.add(noteEvent.content, AESGCM(key, nonce), mimeType) + accountViewModel.account.encryptionKeyCache.add(noteEvent.content, AESGCM(key, nonce), mimeType) // Audio is neither an image nor a video, and the image-or-video split // below would hand it to the video player: a picture-less surface where 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 16f7c104e8..6730c11fdb 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 @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn 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.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -90,7 +91,6 @@ import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt index c2312032c6..1650680780 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt @@ -59,7 +59,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt index 96ed733ea0..b963ff4e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -20,21 +20,20 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions -import android.content.Intent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler 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.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.quick_action_copy_note_id import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier @@ -49,7 +48,8 @@ fun LinkChatButton( ) { val quickActionShareBrowserLinkStr = stringRes(Res.string.quick_action_share_browser_link) val quickActionCopyNoteIdStr = stringRes(Res.string.quick_action_copy_note_id) - val context = LocalContext.current + val uriHandler = LocalUriHandler.current + val sharer = rememberTextSharer() FilledTonalButton( modifier = @@ -57,26 +57,9 @@ fun LinkChatButton( .padding(horizontal = 3.dp) .width(50.dp), onClick = { - val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + runCatching { uriHandler.openUri(channel.toNostrUri()) } - context.startActivity(intent) - - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, channel.toNostrUri()) - putExtra(Intent.EXTRA_TITLE, quickActionShareBrowserLinkStr) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - quickActionCopyNoteIdStr, - ) - - context.startActivity(shareIntent) + sharer.share(channel.toNostrUri(), quickActionShareBrowserLinkStr, quickActionCopyNoteIdStr) }, contentPadding = ZeroPadding, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index dfcd9e5711..745ae07027 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -20,15 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions -import android.content.Intent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler 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.nip28PublicChats.PublicChatChannel @@ -46,7 +44,7 @@ fun OpenChatButton( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current + val uriHandler = LocalUriHandler.current FilledTonalButton( modifier = @@ -54,10 +52,7 @@ fun OpenChatButton( .padding(horizontal = 3.dp) .width(50.dp), onClick = { - val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - - context.startActivity(intent) + runCatching { uriHandler.openUri(channel.toNostrUri()) } }, contentPadding = ZeroPadding, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index 61597c742a..9360b4b64e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -20,13 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions -import android.content.Intent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.runtime.Composable 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 @@ -34,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier @@ -49,7 +48,7 @@ fun ShareChatButton( ) { val quickActionShareBrowserLinkStr = stringRes(Res.string.quick_action_share_browser_link) val quickActionShareStr = stringRes(Res.string.quick_action_share) - val context = LocalContext.current + val sharer = rememberTextSharer() FilledTonalButton( modifier = @@ -57,21 +56,7 @@ fun ShareChatButton( .padding(horizontal = 3.dp) .width(50.dp), onClick = { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, njumpLink(channel.toNEvent())) - putExtra(Intent.EXTRA_TITLE, quickActionShareBrowserLinkStr) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - quickActionShareStr, - ) - - context.startActivity(shareIntent) + sharer.share(njumpLink(channel.toNEvent()), quickActionShareBrowserLinkStr, quickActionShareStr) }, contentPadding = ZeroPadding, ) { 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 9aa697f25a..df6c622bd4 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 @@ -76,8 +76,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifi 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.actions.uploads.SelectSingleFromGallery -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt index 1a4b79b563..aaa001c830 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt @@ -29,12 +29,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo +import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.amethyst.commons.ui.theme.StreamingHeaderModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline @Composable fun ShowVideoStreaming( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index 67cc679efb..38bead47c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.OfflineFlag +import com.vitorpamplona.amethyst.commons.ui.components.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 715729f712..b1f912902a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -55,13 +55,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding 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.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.GalleryUnloaded import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBanner import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBannerBackground -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 2569bcff1b..d5d5ea07b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.OfflineFlag import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.ScheduledFlag import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteAndMap +import com.vitorpamplona.amethyst.commons.ui.components.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivitiesChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index 1c69311c2c..cf7b4e9927 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag 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 065426107c..dae7daa668 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 @@ -91,9 +91,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp 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.amethyst.ui.screen.loggedIn.mockAccountViewModel 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/AddOutboxRelayCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt index 522c65fbbf..acc89b1249 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayCard.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.AddInboxRelayCard @Preview 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 c164365643..0536c35f05 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 @@ -52,7 +52,6 @@ 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.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -77,8 +76,6 @@ import com.vitorpamplona.amethyst.commons.resources.feed_is_empty import com.vitorpamplona.amethyst.commons.resources.home_tab_everything import com.vitorpamplona.amethyst.commons.resources.new_threads import com.vitorpamplona.amethyst.commons.resources.refresh -import com.vitorpamplona.amethyst.commons.service.OnlineChecker -import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.PagerStateKeys @@ -105,7 +102,6 @@ import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.backups.BackupConflictCards -import com.vitorpamplona.amethyst.ui.screen.loggedIn.checkVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.NewGeoPostButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.NewHashtagPostButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription @@ -594,51 +590,6 @@ fun HomeFeedEmpty(onRefresh: () -> Unit) { } } -@Composable -fun CheckIfVideoIsOnline( - url: String, - accountViewModel: AccountViewModel, - whenOnline: @Composable (Boolean) -> Unit, -) { - val online by produceState( - initialValue = OnlineChecker.isOnlineCached(url), - key1 = url, - ) { - val isOnline = accountViewModel.checkVideoIsOnline(url) - if (value != isOnline) { - value = isOnline - } - } - - whenOnline(online) -} - -@Composable -fun CrossfadeCheckIfVideoIsOnline( - url: String, - accountViewModel: AccountViewModel, - whenOnline: @Composable () -> Unit, -) { - val online by produceState( - initialValue = OnlineChecker.isOnlineCached(url), - key1 = url, - ) { - val isOnline = accountViewModel.checkVideoIsOnline(url) - if (value != isOnline) { - value = isOnline - } - } - - CrossfadeIfEnabled( - targetState = online, - label = "CheckIfUrlIsOnline", - ) { - if (it) { - whenOnline() - } - } -} - @Composable fun WatchAccountForHomeScreen( liveFeedState: ChannelFeedContentState, 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 2da373c313..e2de31f141 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 @@ -138,9 +138,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip19Bech32.toNsec diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index 6b6cfe4151..d4d3b93b14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -103,11 +103,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding 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.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index cd875cca87..dab9ccc9cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -91,11 +91,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding 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.externalLinkForNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow 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 f1b29eb043..82bf221d7f 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 @@ -65,8 +65,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier 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.actions.uploads.SelectSingleFromGallery -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions 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 2f1c6b5576..34d71f6a98 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 @@ -65,8 +65,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier 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.actions.uploads.SelectSingleFromGallery -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt index cc7ea35434..320ffd6830 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/AddToMusicPlaylistSheet.kt @@ -78,7 +78,7 @@ 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.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel /** * Modal-style picker that lets a user toggle the visible track in/out of any of their own diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsScreen.kt index 85e9e7ba27..8174013b20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsScreen.kt @@ -37,10 +37,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicPlaylistsFilterAssemblerSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt index 1aea5dcec4..acd168e6d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicPlaylistsTopBar.kt @@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.ui.navigation.topbars.FeedFilterSpinner import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt index 10563fe41e..706f2cd47c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksScreen.kt @@ -37,10 +37,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssemblerSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt index f5fcb043cf..2f4b6c6f98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicTracksTopBar.kt @@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.ui.navigation.topbars.FeedFilterSpinner import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index c086894841..e11f744969 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt @@ -103,9 +103,9 @@ 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.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index 4d83fcf270..4c130e5156 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -91,9 +91,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar 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.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import kotlinx.collections.immutable.persistentListOf @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt index a35098ffeb..55fc8e2b05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayCard.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 4313e2c8dd..f85db97d51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -78,10 +78,10 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer 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.components.ReusableZapButton import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking 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 99894d5207..eda5b2b6e6 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 @@ -37,7 +37,7 @@ 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.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesScreen.kt index 3f8c3e269e..5bcc5c2aaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesScreen.kt @@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.PodcastEpisodesFilterAssemblerSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesTopBar.kt index f2bbcaa3c4..425e111df3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodesTopBar.kt @@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.ui.navigation.topbars.FeedFilterSpinner import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsScreen.kt index 818105d724..1a756df236 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsScreen.kt @@ -44,10 +44,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.datasource.PodcastsFilterAssemblerSubscription @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsTopBar.kt index e2e3568100..521f8e7c81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastsTopBar.kt @@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.ui.navigation.topbars.FeedFilterSpinner import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent @Composable 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 2690a694a7..647bbdfc7c 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 @@ -39,7 +39,6 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserBanner import com.vitorpamplona.amethyst.commons.resources.Res @@ -47,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.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog -import com.vitorpamplona.amethyst.ui.painterRes import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -80,8 +79,8 @@ fun DrawBanner( model = banner, contentDescription = stringRes(id = Res.string.profile_image), contentScale = ContentScale.Crop, - placeholder = painterRes(R.drawable.profile_banner, 1), - error = painterRes(R.drawable.profile_banner, 1), + placeholder = painterRes(Res.drawable.profile_banner, 1), + error = painterRes(Res.drawable.profile_banner, 1), modifier = Modifier .fillMaxWidth() @@ -107,7 +106,7 @@ fun DrawBanner( } } else { Image( - painter = painterRes(R.drawable.profile_banner, 2), + painter = painterRes(Res.drawable.profile_banner, 2), contentDescription = stringRes(id = Res.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index bcde2333e0..61f0404f45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -27,17 +27,17 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.ic_dm import com.vitorpamplona.amethyst.commons.resources.send_a_direct_message import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey @Composable @@ -62,7 +62,7 @@ fun MessageButton( contentPadding = ZeroPadding, ) { Icon( - painter = painterRes(R.drawable.ic_dm, 1), + painter = painterRes(Res.drawable.ic_dm, 1), stringRes(Res.string.send_a_direct_message), modifier = Size20Modifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt index 39493c25e3..613616dcb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt @@ -46,8 +46,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.UserActionOptions -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.ZapAmount import com.vitorpamplona.quartz.utils.BigDecimal diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index 3e3aa28980..3b71289f1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -69,8 +69,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.largeProfilePictureModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.ObserveAndDisplayNIP05 -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindChip.kt new file mode 100644 index 0000000000..55a6aed94c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindChip.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.ui.screen.loggedIn.relays + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.ui.theme.redColorOnSecondSurface +import com.vitorpamplona.quartz.kinds.KindNames +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent + +private val posts = setOf(0, 1, 6, 7, 16, 30023) +private val settings = setOf(3, 10002, 10000, 10001, 10003, 10004, 30000) +private val dms = setOf(4, GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, 10050) +private val zaps = setOf(9734, 9735, 9041, 17375, 23194, 23195) +private val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionRequestEvent.KIND, RequestToVanishEvent.KIND) + +@Composable +fun KindChip(kind: Int) { + val nameResId = kindDisplayName(kind) + val name = if (nameResId != null) stringRes(nameResId) else (KindNames.nameFor(kind) ?: "k$kind") + val (bg, fg) = + when (kind) { + in posts -> { + MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + } + + // + in settings -> { + MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.onSecondaryContainer + } + + // dms + in dms -> { + MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer + } + + // zaps + in zaps -> { + MaterialTheme.colorScheme.background to MaterialTheme.colorScheme.bitcoinColor + } + + in reports -> { + MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.redColorOnSecondSurface + } + + else -> { + MaterialTheme.colorScheme.surfaceVariant to MaterialTheme.colorScheme.onSurfaceVariant + } + } + Surface( + shape = RoundedCornerShape(50), + color = bg, + ) { + Text( + text = name, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace), + color = fg, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index 50b3cf9f93..209fc9506d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -173,20 +173,17 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor -import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor -import com.vitorpamplona.amethyst.commons.ui.theme.redColorOnSecondSurface import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.graspLink import com.vitorpamplona.amethyst.ui.note.nipLink -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssemblerSubscription -import com.vitorpamplona.quartz.kinds.KindNames import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.stats.ErrorDebugMessage import com.vitorpamplona.quartz.nip01Core.relay.client.stats.IRelayDebugMessage @@ -198,13 +195,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client import com.vitorpamplona.quartz.utils.TimeUtils @@ -474,58 +465,6 @@ fun RelayInformationBody( // Active subscriptions + outbox display // --------------------------------------------------------------------------- -val posts = setOf(0, 1, 6, 7, 16, 30023) -val settings = setOf(3, 10002, 10000, 10001, 10003, 10004, 30000) -val dms = setOf(4, GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, 10050) -val zaps = setOf(9734, 9735, 9041, 17375, 23194, 23195) -val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionRequestEvent.KIND, RequestToVanishEvent.KIND) - -@Composable -fun KindChip(kind: Int) { - val nameResId = kindDisplayName(kind) - val name = if (nameResId != null) stringRes(nameResId) else (KindNames.nameFor(kind) ?: "k$kind") - val (bg, fg) = - when (kind) { - in posts -> { - MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer - } - - // - in settings -> { - MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.onSecondaryContainer - } - - // dms - in dms -> { - MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onTertiaryContainer - } - - // zaps - in zaps -> { - MaterialTheme.colorScheme.background to MaterialTheme.colorScheme.bitcoinColor - } - - in reports -> { - MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.redColorOnSecondSurface - } - - else -> { - MaterialTheme.colorScheme.surfaceVariant to MaterialTheme.colorScheme.onSurfaceVariant - } - } - Surface( - shape = RoundedCornerShape(50), - color = bg, - ) { - Text( - text = name, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), - style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace), - color = fg, - ) - } -} - @Composable private fun FilterAttributeChip( text: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt index 382ee9a555..c239545f84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.countToHumanReadable import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt index 6d62802399..18e8baeff2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdEndPadding 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer 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 00a192e9ee..f255eec289 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 @@ -98,8 +98,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness 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.note.formatMediumDateTime -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt index d7c9b67d0a..5adaf1521c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index d66adf0caf..233461c8f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -72,8 +72,8 @@ import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon import com.vitorpamplona.amethyst.commons.ui.settings.filterSettings 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.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt index 5231b75184..8b09e9d267 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt @@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier 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.navigation.bottombars.BottomBarCategories import com.vitorpamplona.amethyst.ui.navigation.bottombars.GroupEntryAvatar import com.vitorpamplona.amethyst.ui.navigation.bottombars.GroupEntryDisplay @@ -101,7 +102,6 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCategory import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberFavoriteIconModel import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberGroupEntryDisplay -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag 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 bf55c1997d..38f77ce538 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 @@ -99,11 +99,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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 com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond import com.vitorpamplona.amethyst.service.pow.loadApproxDuration import com.vitorpamplona.amethyst.ui.note.creators.pow.POW_PRESETS -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt index b3ff1ca05d..43afd795ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt @@ -65,11 +65,11 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes 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.navigation.bottombars.NavBarCatalog import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSection import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSectionVisibility import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSections -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel @Composable @Preview(device = "spec:width=2100px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt index 547d0b6595..2a9541eea6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt @@ -74,7 +74,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import org.jetbrains.compose.resources.StringResource @Preview diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MessagesSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MessagesSettingsScreen.kt index a030e74d69..da02209205 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MessagesSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MessagesSettingsScreen.kt @@ -95,7 +95,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import org.jetbrains.compose.resources.StringResource @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt index 9519533af3..22cdf51516 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt @@ -94,11 +94,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.service.notifications.BatteryOptimizationHelper import com.vitorpamplona.amethyst.service.notifications.NotificationChannels import com.vitorpamplona.amethyst.ui.components.PushNotificationProviderTile import com.vitorpamplona.amethyst.ui.components.hasPushNotificationProvider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index 13405a93bb..f3f4610603 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt index 8ac553dd24..1cdf8f6404 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt @@ -87,7 +87,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Composable @Preview(device = "spec:width=2100px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index ac2a02f7f7..8e1bfb4217 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -69,7 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu 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.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Composable fun SecurityFiltersScreen( 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 1caa22ec2d..fc09ddd1dd 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,7 +91,7 @@ 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.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import java.util.Locale as JavaLocale @Preview(device = "spec:width=2160px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt index 07a930e399..918d6e76bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt @@ -93,7 +93,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel @Composable @Preview(device = "spec:width=2100px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt index 4458893a91..49471d66ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt @@ -57,7 +57,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.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsControlRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection 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 2ce5d78e52..64a7d549cc 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 @@ -74,7 +74,6 @@ import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.birdstar.ui.RenderBirdDetection import com.vitorpamplona.amethyst.commons.birdstar.ui.RenderBirdex import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton @@ -87,6 +86,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.expand import com.vitorpamplona.amethyst.commons.resources.hi_seller_is_this_still_available import com.vitorpamplona.amethyst.commons.resources.hi_there_is_this_still_available +import com.vitorpamplona.amethyst.commons.resources.ic_dm import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.resources.send_a_direct_message @@ -115,6 +115,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -138,6 +139,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.selectedNote import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel 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.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.MyAsyncImage @@ -268,10 +270,8 @@ import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.note.types.VoiceHeader import com.vitorpamplona.amethyst.ui.note.types.observeZapSender -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastTrailerListItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.WorkoutDisplay import com.vitorpamplona.quartz.cyberspace.CyberspaceBagEvent @@ -1408,7 +1408,7 @@ private fun RenderClassifiedsReaderForThread( verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterRes(R.drawable.ic_dm, 5), + painter = painterRes(Res.drawable.ic_dm, 5), stringRes(Res.string.send_a_direct_message), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt index 6adb5fc813..5b81a4282f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/webBookmarks/WebBookmarksScreen.kt @@ -323,7 +323,7 @@ private fun WebBookmarkCard( ) Text( - text = previewInfo?.verifiedUrl?.host ?: event.url(), + text = previewInfo?.verifiedHost ?: event.url(), style = MaterialTheme.typography.bodySmall, color = Color.Gray, maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutViewModel.kt index d8e29df202..af70161e48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutViewModel.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.text.onUiThread +import com.vitorpamplona.amethyst.commons.util.phonePrefersMiles import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent import com.vitorpamplona.quartz.experimental.fitness.workout.avgHeartRate diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt index b67b2f659b..bec7659133 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt @@ -84,11 +84,12 @@ import com.vitorpamplona.amethyst.commons.resources.workout_volume import com.vitorpamplona.amethyst.commons.resources.workout_weight import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.phonePrefersMiles import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.fitness.workout.ExerciseGroup import com.vitorpamplona.quartz.experimental.fitness.workout.ExerciseTemplateEvent import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessFormat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessFormat.kt index bd9951aa34..77423c193d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessFormat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/fitness/MyFitnessFormat.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.resources.my_fitness_unit_km import com.vitorpamplona.amethyst.commons.resources.my_fitness_unit_m import com.vitorpamplona.amethyst.commons.resources.my_fitness_unit_mi import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.phonePrefersMiles +import com.vitorpamplona.amethyst.commons.util.phonePrefersMiles import com.vitorpamplona.quartz.experimental.fitness.workout.tags.DistanceTag import com.vitorpamplona.quartz.experimental.fitness.workout.tags.Elevation import kotlin.math.roundToInt 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 4a3b824c64..d217f84ea7 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 @@ -40,6 +40,7 @@ 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.LocalInlineQuoteRenderer +import com.vitorpamplona.amethyst.commons.ui.components.LocalTranslationPlatform import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings @@ -51,6 +52,9 @@ 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.note.platform.LocalNotePlatform val chartLightColors = VicoTheme( @@ -127,10 +131,13 @@ fun AmethystTheme( colors = colors, ) { // The platform halves of the shared note and rich-text renderers: media, LaTeX, payments, - // link previews, and the quoted-note card. Every Activity root goes through this theme. + // link previews, the quoted-note card, the note types built on platform engines and the + // flavour's translator. Every Activity root goes through this theme. CompositionLocalProvider( LocalRichTextPlatform provides AndroidRichTextPlatform, LocalInlineQuoteRenderer provides DefaultInlineQuoteRenderer, + LocalTranslationPlatform provides FlavorTranslationPlatform, + LocalNotePlatform provides AndroidNotePlatform, content = content, ) } diff --git a/amethyst/src/main/res/drawable-night/nip_05.xml b/amethyst/src/main/res/drawable-night/nip_05.xml deleted file mode 100644 index bc7967c176..0000000000 --- a/amethyst/src/main/res/drawable-night/nip_05.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/MlKitTranslationPlatform.kt similarity index 87% rename from amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt rename to amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/MlKitTranslationPlatform.kt index 6d3614193a..6060d89314 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/MlKitTranslationPlatform.kt @@ -23,17 +23,14 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState 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.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.ui.components.TranslationConfig -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.components.TranslationPlatform import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthPaddingTop5dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService @@ -46,46 +43,31 @@ import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import kotlin.coroutines.coroutineContext -@Composable -fun TranslatableRichTextViewer( - content: String, - canPreview: Boolean, - quotesLeft: Int, - modifier: Modifier = Modifier, - tags: ImmutableListOfLists, - backgroundColor: MutableState, - id: String, - callbackUri: String? = null, - authorPubKey: String? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - TranslatableRichTextViewer( - content = content, - id = id, - accountViewModel = accountViewModel, - ) { - ExpandableRichTextViewer( - it, - canPreview, - quotesLeft, - modifier, - tags, - backgroundColor, - id, - callbackUri, - authorPubKey, - accountViewModel, - nav, - ) - } +/** ML Kit's on-device translator, which only the Play build ships. */ +object MlKitTranslationPlatform : TranslationPlatform { + @Composable + override fun Translatable( + content: String, + id: String, + translationMessageModifier: Modifier, + accountViewModel: AccountViewModel, + displayText: @Composable (String) -> Unit, + ) = MlKitTranslatable(content, id, translationMessageModifier, accountViewModel, displayText) + + @Composable + override fun rememberTranslation( + content: String, + accountViewModel: AccountViewModel, + ): String = rememberMlKitTranslation(content, accountViewModel) } +val FlavorTranslationPlatform: TranslationPlatform = MlKitTranslationPlatform + @Composable -fun TranslatableRichTextViewer( +private fun MlKitTranslatable( content: String, id: String, - translationMessageModifier: Modifier = MaxWidthPaddingTop5dp, + translationMessageModifier: Modifier, accountViewModel: AccountViewModel, displayText: @Composable (String) -> Unit, ) { @@ -131,13 +113,13 @@ fun TranslatableRichTextViewer( /** * The translation of [content] under the current language settings, or [content] unchanged when - * no translation applies. Same machinery and cache as [TranslatableRichTextViewer] but without + * no translation applies. Same machinery and cache as [MlKitTranslatable] but without * the status bar, for callers that already render one and need a second string translated in * step with it — e.g. a NIP-84 highlight, which must translate the quoted passage alongside the * context in order to keep locating the passage inside it. */ @Composable -fun rememberTranslation( +private fun rememberMlKitTranslation( content: String, accountViewModel: AccountViewModel, ): String { 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 d19c673374..7519d98a40 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -296,6 +296,28 @@ assemblers, `EventSync`, …). Packages are renamed on the way: root (and later the Desktop one) for the media and intents, plus KMP swaps for the formatting. RichTextViewer's own closure is 32 files with 41 exits; NoteCompose's is 144 files with 81. + + **Progress 2026-09-28, later.** The shape held: slots at the root, KMP swaps below them. + - **`RichTextViewer` moved** over `RichTextPlatform` (`LocalRichTextPlatform`): the + platform's segment renderer, its markdown renderer, and the secret-message body. The + quoted-note card is `LocalInlineQuoteRenderer`. + - **Translation** is `TranslationPlatform` (`LocalTranslationPlatform`, commonsUI). The + shared `TranslatableRichTextViewer` asks it; Play installs ML Kit and F-Droid the no-op. + - **Platform verbs** became small commonsUI expect/actuals instead of Android calls in + note code: `rememberTextSharer()` (the `ACTION_SEND` chooser), `rememberShortNotice()` + (a toast), `rememberBlossomUriOpener()` (on `LocalUriHandler`), + `argbPixelsToImageBitmap`. Plain `VIEW` intents became `LocalUriHandler.openUri`. + - **Formatting:** `formatDateTime(epochMillis, DateTimeStyle, DateTimeStyle)`, + `DecimalPatternFormatter`, `PlatformNumberFormatter`, `phonePrefersMiles()` and the + quartz `BigDecimal` (+ `parseBigDecimalOrNull`) replace `java.text` / `java.math` in + the note types. `UrlInfoItem` resolves OpenGraph URLs through `resolveHttpUrl`, and + NIP-95 local media is an okio `Path` served by `Nip95BlobStore.path(id)`. + - **The rest of the note card** goes through `NotePlatform` (`LocalNotePlatform`): the + zoomable viewer and video/GIF players, link previews, the map and reverse geocoding, + the note types built on a platform engine (audio players, chess, the git browser, + meeting rooms, napplets/nsites), the reactions/zap row and the post editor. Shared + code calls same-named shims in `ui.note.platform`, so call sites only changed imports. + Each of those pieces can later get a shared implementation and leave the slot. 6. **Screens**, feature by feature, into `commonsUI`. 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/MeasurementSystem.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.android.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/MeasurementSystem.kt rename to commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.android.kt index 24ff98836e..983f6be6b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/MeasurementSystem.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.android.kt @@ -18,19 +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.workouts +package com.vitorpamplona.amethyst.commons.util import android.icu.util.LocaleData import android.icu.util.ULocale import android.os.Build import java.util.Locale -/** Countries that use miles for road distance (imperial / US customary). */ -private val MILES_COUNTRIES = setOf("US", "GB", "LR", "MM") - /** - * Whether the phone's measurement preference favours miles for distance. - * * Honours the Android 14+ "Regional preferences → Measurement system" override, * which the platform surfaces through the default locale's Unicode `-u-ms-` * extension (`metric` / `ussystem` / `uksystem`). When no explicit override is @@ -38,7 +33,7 @@ private val MILES_COUNTRIES = setOf("US", "GB", "LR", "MM") * use miles for distance), available since API 28; on older releases it falls * back to a country-code check. */ -fun phonePrefersMiles(): Boolean { +actual fun phonePrefersMiles(): Boolean { val locale = Locale.getDefault(Locale.Category.FORMAT) locale.getUnicodeLocaleType("ms")?.let { ms -> diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/WaveformData.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/WaveformData.kt new file mode 100644 index 0000000000..28dbb7dbc7 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/WaveformData.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.audio + +import androidx.compose.runtime.Immutable + +/** An audio clip's amplitude envelope, one value per bar, as the voice-message players draw it. */ +@Immutable +class WaveformData( + val wave: List, +) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt index e35c28513f..5fdad26f7a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/Nip95BlobStore.kt @@ -38,6 +38,9 @@ interface Nip95BlobStore { /** Whether a blob is already stored under [id]. */ fun exists(id: String): Boolean + /** Where the blob for [id] lives (or would), for a media player or image loader to read. */ + fun path(id: String): Path + /** Stores [bytes] under [id]. Returns false when the write failed. */ fun store( id: String, @@ -60,6 +63,8 @@ class FileSystemNip95BlobStore( override fun exists(id: String): Boolean = fileSystem.exists(directory / id) + override fun path(id: String): Path = directory / id + override fun store( id: String, bytes: ByteArray, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.kt new file mode 100644 index 0000000000..9dd9427f85 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.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.podcasts + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder + +/** + * GETs a small text document (a Podcasting-2.0 chapters or transcript file) over the preview + * client for [url], or null on any failure or an oversized body. The iOS actual has no HTTP + * client yet and always returns null. + */ +expect suspend fun IRoleBasedHttpClientBuilder.fetchPreviewText(url: String): String? diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt similarity index 92% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt index 68c040e065..5cb955845d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/preview/UrlInfoItem.kt @@ -23,7 +23,8 @@ package com.vitorpamplona.amethyst.commons.preview import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.richtext.MediaContentKind import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import java.net.URI +import com.vitorpamplona.amethyst.commons.util.absoluteUrlHost +import com.vitorpamplona.amethyst.commons.util.resolveHttpUrl @Immutable class UrlInfoItem( @@ -43,7 +44,8 @@ class UrlInfoItem( /** `og:type` — what the page says it is, e.g. `music.song`, `video.other`, `article`. */ val type: String = "", ) { - val verifiedUrl = runCatching { URI(url).toURL() }.getOrNull() + /** The page's host, or null when [url] is not an absolute URL. */ + val verifiedHost = absoluteUrlHost(url) /** * Resolves an OpenGraph URL against the page's own address, or null when it cannot be made @@ -57,17 +59,7 @@ class UrlInfoItem( */ private fun resolve(path: String): String? { if (path.isBlank()) return null - return runCatching { - val base = verifiedUrl?.toURI() - val resolved = if (base != null) base.resolve(path) else URI(path) - if (!resolved.scheme.equals("http", ignoreCase = true) && - !resolved.scheme.equals("https", ignoreCase = true) - ) { - null - } else { - resolved.toURL().toString() - } - }.getOrNull() + return resolveHttpUrl(url.takeIf { verifiedHost != null }, path) } // Falls back to the raw value so an unresolvable image degrades to "the loader shows nothing", diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt index c066025405..5f32aea49e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt @@ -26,10 +26,8 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -// URL-based media content models (KMP). The locally-cached variants -// (MediaPreloadedContent, MediaLocalImage, MediaLocalVideo) live in -// MediaLocalContent.kt under the jvmAndroid source set because they depend -// on java.io.File. +// URL-based media content models. The locally-cached variants +// (MediaPreloadedContent, MediaLocalImage, MediaLocalVideo) are in MediaLocalContent.kt. @Immutable abstract class BaseMediaContent( diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt similarity index 82% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt index 30c0de6441..9e56131aaa 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.kt @@ -21,18 +21,16 @@ package com.vitorpamplona.amethyst.commons.richtext import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.util.platformFileSystem import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import java.io.File +import okio.Path -// Locally-cached media content. Lives in jvmAndroid because java.io.File is -// JVM-only; iOS support will arrive when we either swap to okio.Path or pull -// these classes back into commonMain behind an expect/actual File abstraction. -// URL-based media classes (MediaUrlImage, MediaUrlVideo, …) remain in -// commonMain in MediaContentModels.kt. +// Locally-cached media content: a file on this device (a NIP-95 blob) rather than a URL. +// URL-based media classes (MediaUrlImage, MediaUrlVideo, …) are in MediaContentModels.kt. @Immutable abstract class MediaPreloadedContent( - val localFile: File?, + val localFile: Path?, description: String? = null, val mimeType: String? = null, val isVerified: Boolean? = null, @@ -42,12 +40,12 @@ abstract class MediaPreloadedContent( val id: String? = null, thumbhash: String? = null, ) : BaseMediaContent(description, dim, blurhash, thumbhash) { - fun localFileExists() = localFile != null && localFile.exists() + fun localFileExists() = localFile != null && platformFileSystem.exists(localFile) } @Immutable class MediaLocalImage( - localFile: File?, + localFile: Path?, mimeType: String? = null, description: String? = null, dim: DimensionTag? = null, @@ -59,7 +57,7 @@ class MediaLocalImage( @Immutable class MediaLocalVideo( - localFile: File?, + localFile: Path?, mimeType: String? = null, description: String? = null, dim: DimensionTag? = null, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.kt new file mode 100644 index 0000000000..b0ea59c23b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.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.service + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder + +/** + * Probes whether a live stream at [url] answers, over the video client, and records the answer + * in [OnlineStatusCache]. iOS has no HTTP client yet and reports every stream online, so it hides + * none of them. + */ +expect suspend fun IRoleBasedHttpClientBuilder.isVideoOnline(url: String): Boolean diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.kt new file mode 100644 index 0000000000..db2d8e9155 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.kt @@ -0,0 +1,31 @@ +/* + * 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 + +/** + * Formats numbers with a `java.text.DecimalFormat` pattern ("##.00", "###") in the default + * locale's symbols: "-1.25" in en-US, "-1,25" in de-DE. Not thread-safe; build one per caller. + */ +expect class DecimalPatternFormatter( + pattern: String, +) { + fun format(value: Double): String +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.kt new file mode 100644 index 0000000000..74280eaf87 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.kt @@ -0,0 +1,26 @@ +/* + * 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 com.vitorpamplona.quartz.nip01Core.core.Event + +/** The event as indented JSON, for people to read (the "copy JSON" action), not for the wire. */ +expect fun Event.toPrettyJson(): String diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.kt new file mode 100644 index 0000000000..51d1ca1149 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.kt @@ -0,0 +1,34 @@ +/* + * 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 + +/** The host of [url] when it parses as an absolute URL with a scheme the platform knows, else null. */ +expect fun absoluteUrlHost(url: String): String? + +/** + * Resolves [reference] against [base] as RFC 3986 §5 describes (any relative form, not only + * root-relative ones), and returns the result only when it is an absolute `http(s)` URL. A null + * or unparseable [base] leaves [reference] to stand on its own. + */ +expect fun resolveHttpUrl( + base: String?, + reference: String, +): String? diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.kt new file mode 100644 index 0000000000..8dcbcae6d0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.kt @@ -0,0 +1,27 @@ +/* + * 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 + +/** Countries that use miles for road distance (imperial / US customary). */ +internal val MILES_COUNTRIES = setOf("US", "GB", "LR", "MM") + +/** Whether the device's measurement preference favours miles for distance. */ +expect fun phonePrefersMiles(): Boolean diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeDiffFormat.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeDiffFormat.kt new file mode 100644 index 0000000000..73614975e8 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeDiffFormat.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.util + +import com.vitorpamplona.quartz.utils.TimeUtils + +fun timeDiffAgoLong(timeDifference: Int): String = + when { + timeDifference > TimeUtils.ONE_YEAR -> (timeDifference / TimeUtils.ONE_YEAR).toString() + " years" + timeDifference > TimeUtils.ONE_MONTH -> (timeDifference / TimeUtils.ONE_MONTH).toString() + " months" + timeDifference > TimeUtils.ONE_DAY -> (timeDifference / TimeUtils.ONE_DAY).toString() + " days" + timeDifference > TimeUtils.ONE_HOUR -> (timeDifference / TimeUtils.ONE_HOUR).toString() + " hours" + timeDifference > TimeUtils.ONE_MINUTE -> (timeDifference / TimeUtils.ONE_MINUTE).toString() + " minutes" + else -> "now" + } + +fun timeDiffAgoShortish(timeDifference: Int): String = + when { + timeDifference > TimeUtils.ONE_YEAR -> (timeDifference / TimeUtils.ONE_YEAR).toString() + " yrs" + timeDifference > TimeUtils.ONE_MONTH -> (timeDifference / TimeUtils.ONE_MONTH).toString() + " mos" + timeDifference > TimeUtils.ONE_DAY -> (timeDifference / TimeUtils.ONE_DAY).toString() + " days" + timeDifference > TimeUtils.ONE_HOUR -> (timeDifference / TimeUtils.ONE_HOUR).toString() + " hrs" + timeDifference > TimeUtils.ONE_MINUTE -> (timeDifference / TimeUtils.ONE_MINUTE).toString() + " mins" + else -> "now" + } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt similarity index 90% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt index ce09af14e6..dbb83b7435 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.util -import java.util.Locale - /** * Formats seconds into a human-readable time string (M:SS or MM:SS format). * @@ -32,8 +30,8 @@ fun formatSecondsToTime(seconds: Int): String { val minutes = seconds / 60 val secs = seconds % 60 return if (minutes > 0) { - String.format(Locale.getDefault(), "%d:%02d", minutes, secs) + "$minutes:${secs.toString().padStart(2, '0')}" } else { - String.format(Locale.getDefault(), "0:%02d", secs) + "0:${secs.toString().padStart(2, '0')}" } } diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.ios.kt new file mode 100644 index 0000000000..96645c3bea --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.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.podcasts + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder + +actual suspend fun IRoleBasedHttpClientBuilder.fetchPreviewText(url: String): String? = null diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.ios.kt new file mode 100644 index 0000000000..ada112dd74 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.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.service + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder + +actual suspend fun IRoleBasedHttpClientBuilder.isVideoOnline(url: String): Boolean = true diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.ios.kt new file mode 100644 index 0000000000..d3ad46c075 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.ios.kt @@ -0,0 +1,39 @@ +/* + * 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.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterRoundHalfEven +import platform.Foundation.numberWithDouble + +/** NSNumberFormatter reads the same pattern syntax and, like DecimalFormat, rounds half-even. */ +actual class DecimalPatternFormatter actual constructor( + pattern: String, +) { + private val formatter = + NSNumberFormatter().apply { + positiveFormat = pattern + roundingMode = NSNumberFormatterRoundHalfEven + } + + actual fun format(value: Double): String = formatter.stringFromNumber(NSNumber.numberWithDouble(value)) ?: value.toString() +} diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.ios.kt new file mode 100644 index 0000000000..75dbbe389a --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.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.util + +import com.vitorpamplona.quartz.nip01Core.core.Event +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +private val prettyJson = Json { prettyPrint = true } + +actual fun Event.toPrettyJson(): String = prettyJson.encodeToString(JsonElement.serializer(), Json.parseToJsonElement(toJson())) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.ios.kt new file mode 100644 index 0000000000..325d6af833 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.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.NSURL + +actual fun absoluteUrlHost(url: String): String? = NSURL.URLWithString(url)?.takeIf { it.scheme != null }?.host + +actual fun resolveHttpUrl( + base: String?, + reference: String, +): String? { + val baseUrl = base?.let { NSURL.URLWithString(it) } + val resolved = NSURL.URLWithString(reference, relativeToURL = baseUrl)?.absoluteURL ?: return null + val scheme = resolved.scheme?.lowercase() + return if (scheme == "http" || scheme == "https") resolved.absoluteString else null +} diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.ios.kt new file mode 100644 index 0000000000..20d6fdcca6 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.ios.kt @@ -0,0 +1,28 @@ +/* + * 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.countryCode +import platform.Foundation.currentLocale + +/** By country, as on the other platforms: iOS reports the UK as metric, but its roads use miles. */ +actual fun phonePrefersMiles(): Boolean = NSLocale.currentLocale.countryCode?.uppercase() in MILES_COUNTRIES diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.jvmAndroid.kt new file mode 100644 index 0000000000..b5caa1240d --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/podcasts/PreviewTextFetch.jvmAndroid.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.podcasts + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder + +actual suspend fun IRoleBasedHttpClientBuilder.fetchPreviewText(url: String): String? = PodcastRemoteContent.fetchText(url, okHttpClientForPreview(url)) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.jvmAndroid.kt new file mode 100644 index 0000000000..e045e8c543 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaLocalContent.jvmAndroid.kt @@ -0,0 +1,26 @@ +/* + * 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.richtext + +import java.io.File + +/** [MediaPreloadedContent.localFile] as a `java.io.File`, for the JVM image loaders and players. */ +val MediaPreloadedContent.localJavaFile: File? get() = localFile?.toFile() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.jvmAndroid.kt new file mode 100644 index 0000000000..b87c9835e8 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/VideoOnlineProbe.jvmAndroid.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.service + +import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +actual suspend fun IRoleBasedHttpClientBuilder.isVideoOnline(url: String): Boolean = + withContext(Dispatchers.IO) { + OnlineChecker.isOnline(url, ::okHttpClientForVideo) + } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.jvmAndroid.kt new file mode 100644 index 0000000000..4443e37ccc --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/DecimalPatternFormatter.jvmAndroid.kt @@ -0,0 +1,31 @@ +/* + * 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.text.DecimalFormat + +actual class DecimalPatternFormatter actual constructor( + pattern: String, +) { + private val formatter = DecimalFormat(pattern) + + actual fun format(value: Double): String = formatter.format(value) +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.jvmAndroid.kt new file mode 100644 index 0000000000..98822dae5d --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventPrettyJson.jvmAndroid.kt @@ -0,0 +1,26 @@ +/* + * 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 com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper + +actual fun Event.toPrettyJson(): String = JacksonMapper.toJsonPretty(this) 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 new file mode 100644 index 0000000000..bda5b0c2a9 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/HttpUrlResolution.jvmAndroid.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.util + +import java.net.URI + +actual fun absoluteUrlHost(url: String): String? = runCatching { URI(url).toURL().host }.getOrNull() + +actual fun resolveHttpUrl( + base: String?, + reference: String, +): String? = + runCatching { + val baseUri = base?.let { runCatching { URI(it).toURL().toURI() }.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) + ) { + null + } else { + resolved.toURL().toString() + } + }.getOrNull() diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt index 43f6d5270c..a5f190f558 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt @@ -105,26 +105,6 @@ fun timeAgo( } } -fun timeDiffAgoLong(timeDifference: Int): String = - when { - timeDifference > TimeUtils.ONE_YEAR -> (timeDifference / TimeUtils.ONE_YEAR).toString() + " years" - timeDifference > TimeUtils.ONE_MONTH -> (timeDifference / TimeUtils.ONE_MONTH).toString() + " months" - timeDifference > TimeUtils.ONE_DAY -> (timeDifference / TimeUtils.ONE_DAY).toString() + " days" - timeDifference > TimeUtils.ONE_HOUR -> (timeDifference / TimeUtils.ONE_HOUR).toString() + " hours" - timeDifference > TimeUtils.ONE_MINUTE -> (timeDifference / TimeUtils.ONE_MINUTE).toString() + " minutes" - else -> "now" - } - -fun timeDiffAgoShortish(timeDifference: Int): String = - when { - timeDifference > TimeUtils.ONE_YEAR -> (timeDifference / TimeUtils.ONE_YEAR).toString() + " yrs" - timeDifference > TimeUtils.ONE_MONTH -> (timeDifference / TimeUtils.ONE_MONTH).toString() + " mos" - timeDifference > TimeUtils.ONE_DAY -> (timeDifference / TimeUtils.ONE_DAY).toString() + " days" - timeDifference > TimeUtils.ONE_HOUR -> (timeDifference / TimeUtils.ONE_HOUR).toString() + " hrs" - timeDifference > TimeUtils.ONE_MINUTE -> (timeDifference / TimeUtils.ONE_MINUTE).toString() + " mins" - else -> "now" - } - /** * Formats a Unix timestamp as a date string. * For recent dates (< 1 day), returns the provided today string. diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.jvm.kt new file mode 100644 index 0000000000..24c0b2e6ff --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/util/MeasurementSystem.jvm.kt @@ -0,0 +1,34 @@ +/* + * 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 + +/** The `-u-ms-` locale extension when set, else the country of the default format locale. */ +actual fun phonePrefersMiles(): Boolean { + val locale = Locale.getDefault(Locale.Category.FORMAT) + + locale.getUnicodeLocaleType("ms")?.let { ms -> + return ms == "ussystem" || ms == "uksystem" + } + + return locale.country.uppercase(Locale.ROOT) in MILES_COUNTRIES +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.android.kt new file mode 100644 index 0000000000..7690ed370d --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.android.kt @@ -0,0 +1,34 @@ +/* + * 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.image + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.createBitmap + +actual fun argbPixelsToImageBitmap( + pixels: IntArray, + width: Int, + height: Int, +): ImageBitmap = + createBitmap(width, height) + .apply { setPixels(pixels, 0, width, 0, 0, width, height) } + .asImageBitmap() diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt new file mode 100644 index 0000000000..24d16cd38a --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.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 android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +@Composable +actual fun rememberShortNotice(): ShortNotice { + val context = LocalContext.current + return remember(context) { ShortNotice { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.android.kt new file mode 100644 index 0000000000..5f2498a71c --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.android.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 android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +@Composable +actual fun rememberTextSharer(): TextSharer { + val context = LocalContext.current + return remember(context) { + TextSharer { text, title, chooserTitle -> + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + if (title != null) putExtra(Intent.EXTRA_TITLE, title) + } + context.startActivity(Intent.createChooser(sendIntent, chooserTitle)) + } + } +} diff --git a/amethyst/src/main/res/drawable-hdpi/ic_dm.png b/commonsUI/src/commonMain/composeResources/drawable-hdpi/ic_dm.png similarity index 100% rename from amethyst/src/main/res/drawable-hdpi/ic_dm.png rename to commonsUI/src/commonMain/composeResources/drawable-hdpi/ic_dm.png diff --git a/amethyst/src/main/res/drawable-mdpi/ic_dm.png b/commonsUI/src/commonMain/composeResources/drawable-mdpi/ic_dm.png similarity index 100% rename from amethyst/src/main/res/drawable-mdpi/ic_dm.png rename to commonsUI/src/commonMain/composeResources/drawable-mdpi/ic_dm.png diff --git a/amethyst/src/main/res/drawable-xhdpi/ic_dm.png b/commonsUI/src/commonMain/composeResources/drawable-xhdpi/ic_dm.png similarity index 100% rename from amethyst/src/main/res/drawable-xhdpi/ic_dm.png rename to commonsUI/src/commonMain/composeResources/drawable-xhdpi/ic_dm.png diff --git a/amethyst/src/main/res/drawable-xxhdpi/ic_dm.png b/commonsUI/src/commonMain/composeResources/drawable-xxhdpi/ic_dm.png similarity index 100% rename from amethyst/src/main/res/drawable-xxhdpi/ic_dm.png rename to commonsUI/src/commonMain/composeResources/drawable-xxhdpi/ic_dm.png diff --git a/amethyst/src/main/res/drawable-xxxhdpi/ic_dm.png b/commonsUI/src/commonMain/composeResources/drawable-xxxhdpi/ic_dm.png similarity index 100% rename from amethyst/src/main/res/drawable-xxxhdpi/ic_dm.png rename to commonsUI/src/commonMain/composeResources/drawable-xxxhdpi/ic_dm.png diff --git a/amethyst/src/main/res/drawable-xxxhdpi/profile_banner.webp b/commonsUI/src/commonMain/composeResources/drawable-xxxhdpi/profile_banner.webp similarity index 100% rename from amethyst/src/main/res/drawable-xxxhdpi/profile_banner.webp rename to commonsUI/src/commonMain/composeResources/drawable-xxxhdpi/profile_banner.webp diff --git a/amethyst/src/main/res/drawable/nip_05.xml b/commonsUI/src/commonMain/composeResources/drawable/nip_05.xml similarity index 100% rename from amethyst/src/main/res/drawable/nip_05.xml rename to commonsUI/src/commonMain/composeResources/drawable/nip_05.xml diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt similarity index 100% rename from commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt similarity index 93% rename from commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt index cfc643fb71..4ec874e85f 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt @@ -37,13 +37,12 @@ import com.vitorpamplona.amethyst.commons.resources.live_stream_live_tag import com.vitorpamplona.amethyst.commons.resources.live_stream_offline_tag import com.vitorpamplona.amethyst.commons.resources.live_stream_planned_tag import com.vitorpamplona.amethyst.commons.resources.live_stream_private_tag +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.liveStreamTag import com.vitorpamplona.quartz.utils.TimeUtils -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date @Composable fun LiveFlag() { @@ -117,11 +116,7 @@ fun ScheduledFlag(starts: Long?) { remember(starts) { starts?.let { if (it > TimeUtils.now()) { - SimpleDateFormat - .getDateTimeInstance( - DateFormat.SHORT, - DateFormat.SHORT, - ).format(Date(starts * 1000)) + formatDateTime(starts * 1000, DateTimeStyle.SHORT, DateTimeStyle.SHORT) } else { null } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.kt new file mode 100644 index 0000000000..27674aa677 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.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.service.image + +import androidx.compose.ui.graphics.ImageBitmap + +/** An [ImageBitmap] of [width] × [height] from row-major ARGB (0xAARRGGBB) [pixels]. */ +expect fun argbPixelsToImageBitmap( + pixels: IntArray, + width: Int, + height: Int, +): ImageBitmap diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/PainterRes.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/PainterRes.kt new file mode 100644 index 0000000000..f0790beeb5 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/PainterRes.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 + +import androidx.collection.LruCache +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + +// Caches the few bitmaps and vectors every feed row draws, so a scroll does not reload them. +private val iconCache = LruCache>(30) + +/** + * The Compose-resources twin of the Android app's `painterRes`: [painterResource] behind a cache + * keyed by resource and [sizeReference]. A cached painter can be the only copy on the screen, so + * callers drawing it at different sizes pass different references. + */ +@Composable +fun painterRes( + resource: DrawableResource, + sizeReference: Int, +): Painter { + val bySize = iconCache[resource] + bySize?.get(sizeReference)?.let { return it } + + val loaded = painterResource(resource) + + val sizes = bySize ?: LruCache(10).also { iconCache.put(resource, it) } + sizes.put(sizeReference, loaded) + + return loaded +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BlossomUriOpener.kt similarity index 58% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BlossomUriOpener.kt index 5319b2fc89..f35735139c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BlossomUriOpener.kt @@ -18,29 +18,38 @@ * AN ACTION OF CONTRACT, TORT OR 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.uploads.blossom.bud10 +package com.vitorpamplona.amethyst.commons.ui.components -import android.content.Context -import android.content.Intent -import androidx.core.net.toUri +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalUriHandler import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.no_blossom_apps_found_description import com.vitorpamplona.amethyst.commons.resources.no_blossom_apps_found_title import org.jetbrains.compose.resources.StringResource import kotlin.coroutines.cancellation.CancellationException -fun openBlossomUriAsIntent( - context: Context, - blossomUri: String, - onError: (StringResource, StringResource) -> Unit, -) { - try { - val intent = Intent(Intent.ACTION_VIEW, blossomUri.toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK +/** Opens a `blossom:` URI in whichever installed app handles it. */ +fun interface BlossomUriOpener { + /** Opens [blossomUri], or reports through [onError] that no app can. */ + fun open( + blossomUri: String, + onError: (StringResource, StringResource) -> Unit, + ) +} - context.startActivity(intent) - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(Res.string.no_blossom_apps_found_title, Res.string.no_blossom_apps_found_description) +/** A [BlossomUriOpener] over the platform's URI handler (a `VIEW` intent on Android). */ +@Composable +fun rememberBlossomUriOpener(): BlossomUriOpener { + val uriHandler = LocalUriHandler.current + return remember(uriHandler) { + BlossomUriOpener { blossomUri, onError -> + try { + uriHandler.openUri(blossomUri) + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(Res.string.no_blossom_apps_found_title, Res.string.no_blossom_apps_found_description) + } + } } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt new file mode 100644 index 0000000000..afcf42777d --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.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.ui.components + +import androidx.compose.runtime.Composable + +/** A brief, non-blocking confirmation ("Copied to clipboard"). */ +fun interface ShortNotice { + fun show(message: String) +} + +/** + * The platform's brief confirmation: an Android toast. Desktop and iOS have no system toast + * wired yet and only log the message. + */ +@Composable +expect fun rememberShortNotice(): ShortNotice diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.kt new file mode 100644 index 0000000000..cb555403ba --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.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 a piece of text to the platform's share sheet. */ +fun interface TextSharer { + /** + * Shares [text], labelled [title] where the target shows one, behind a chooser titled + * [chooserTitle] where the platform has a chooser. + */ + fun share( + text: String, + title: String?, + chooserTitle: String, + ) +} + +/** + * The platform's share sheet: Android's `ACTION_SEND` chooser. Desktop and iOS have no system + * share sheet wired yet and put the text on the clipboard instead. + */ +@Composable +expect fun rememberTextSharer(): TextSharer diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TranslationPlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TranslationPlatform.kt new file mode 100644 index 0000000000..4f8ccedfe1 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TranslationPlatform.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.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel + +/** + * The on-device translator behind every translatable text, supplied by the platform through + * [LocalTranslationPlatform]. Android's Play build installs its ML Kit translator; builds and + * platforms without one fall back to [None], which shows the text as written. + */ +@Stable +interface TranslationPlatform { + /** + * Renders [content] through [displayText], translated when the account's language settings + * ask for it, with the platform's "translated from" bar under it. + */ + @Composable + fun Translatable( + content: String, + id: String, + translationMessageModifier: Modifier, + accountViewModel: AccountViewModel, + displayText: @Composable (String) -> Unit, + ) + + /** The translation of [content] under the current settings, or [content] when none applies. */ + @Composable + fun rememberTranslation( + content: String, + accountViewModel: AccountViewModel, + ): String + + /** No translator: the content is always its own translation. */ + object None : TranslationPlatform { + @Composable + override fun Translatable( + content: String, + id: String, + translationMessageModifier: Modifier, + accountViewModel: AccountViewModel, + displayText: @Composable (String) -> Unit, + ) = displayText(content) + + @Composable + override fun rememberTranslation( + content: String, + accountViewModel: AccountViewModel, + ): String = content + } +} + +val LocalTranslationPlatform = staticCompositionLocalOf { TranslationPlatform.None } diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt similarity index 99% rename from commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt index 95ff1c5d00..a084cb0edf 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt @@ -133,7 +133,7 @@ fun UrlPreviewCard( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = previewInfo.verifiedUrl?.host ?: previewInfo.url, + text = previewInfo.verifiedHost ?: previewInfo.url, style = MaterialTheme.typography.bodySmall, modifier = Modifier.weight(1f, fill = false), color = Color.Gray, diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewState.kt similarity index 100% rename from commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewState.kt diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/VideoOnlineCheck.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/VideoOnlineCheck.kt new file mode 100644 index 0000000000..773aaa1afb --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/VideoOnlineCheck.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.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import com.vitorpamplona.amethyst.commons.service.OnlineStatusCache +import com.vitorpamplona.amethyst.commons.service.isVideoOnline +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel + +@Composable +fun CheckIfVideoIsOnline( + url: String, + accountViewModel: AccountViewModel, + whenOnline: @Composable (Boolean) -> Unit, +) { + val online by produceState( + initialValue = OnlineStatusCache.isOnlineCached(url), + key1 = url, + ) { + val isOnline = accountViewModel.httpClientBuilder.isVideoOnline(url) + if (value != isOnline) { + value = isOnline + } + } + + whenOnline(online) +} + +@Composable +fun CrossfadeCheckIfVideoIsOnline( + url: String, + accountViewModel: AccountViewModel, + whenOnline: @Composable () -> Unit, +) { + val online by produceState( + initialValue = OnlineStatusCache.isOnlineCached(url), + key1 = url, + ) { + val isOnline = accountViewModel.httpClientBuilder.isVideoOnline(url) + if (value != isOnline) { + value = isOnline + } + } + + CrossfadeIfEnabled( + targetState = online, + label = "CheckIfUrlIsOnline", + ) { + if (it) { + whenOnline() + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.kt new file mode 100644 index 0000000000..c69a4c4133 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.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.ui.note + +/** The locale's predefined date or time lengths, as `java.text.DateFormat` and `NSDateFormatter` name them. */ +enum class DateTimeStyle { NONE, SHORT, MEDIUM, LONG } + +/** + * Formats an instant in the default locale and time zone with the locale's predefined [date] and + * [time] styles ("Sep 28, 2026, 2:32:10 PM" for MEDIUM/MEDIUM in en-US). A NONE side is left out; + * both NONE formats nothing. Safe to call from any thread. + */ +expect fun formatDateTime( + epochMillis: Long, + date: DateTimeStyle, + time: DateTimeStyle, +): String diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextPlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextPlatform.kt index 2d29abddc9..ec824271de 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextPlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextPlatform.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -64,6 +65,20 @@ interface RichTextPlatform { nav: INav, ) + /** + * Renders a decoded secret message (text hidden in an emoji's variation selectors) with the + * platform's leaves, the way the viewer renders a note body. + */ + @Composable + fun SecretMessage( + content: RichTextViewerState, + callbackUri: String?, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) + /** Text for every platform segment, and markdown shown as its source. */ object Plain : RichTextPlatform { override fun segmentRenderer( @@ -85,6 +100,16 @@ interface RichTextPlatform { accountViewModel: AccountViewModel, nav: INav, ) = Text(content) + + @Composable + override fun SecretMessage( + content: RichTextViewerState, + callbackUri: String?, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, + ) = Text(content.paragraphs.joinToString("\n") { paragraph -> paragraph.words.joinToString(" ") { it.segmentText } }) } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.kt new file mode 100644 index 0000000000..8c3b9082f0 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.kt @@ -0,0 +1,34 @@ +/* + * 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.viewmodels + +import androidx.compose.runtime.Composable + +/** + * A logged-in [AccountViewModel] over an offline client and the shared cache, for `@Preview`s. + * Built once and reused. Available where previews render (Android and Desktop); iOS has none. + */ +@Composable +expect fun mockAccountViewModel(): AccountViewModel + +/** [mockAccountViewModel] as a read-only (public key only) account. */ +@Composable +expect fun mockVitorAccountViewModel(): AccountViewModel diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt new file mode 100644 index 0000000000..a991b09cfb --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.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 +import com.vitorpamplona.quartz.utils.Log + +private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { message } } + +@Composable +actual fun rememberShortNotice(): ShortNotice = loggedNotice diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.ios.kt new file mode 100644 index 0000000000..7796eb2308 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.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 +import platform.UIKit.UIPasteboard + +private val clipboardSharer = TextSharer { text, _, _ -> UIPasteboard.generalPasteboard.string = text } + +@Composable +actual fun rememberTextSharer(): TextSharer = clipboardSharer diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.ios.kt new file mode 100644 index 0000000000..88e39056a7 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.ios.kt @@ -0,0 +1,52 @@ +/* + * 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 platform.Foundation.NSDate +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSDateFormatterLongStyle +import platform.Foundation.NSDateFormatterMediumStyle +import platform.Foundation.NSDateFormatterNoStyle +import platform.Foundation.NSDateFormatterShortStyle +import platform.Foundation.NSDateFormatterStyle +import platform.Foundation.dateWithTimeIntervalSince1970 + +private fun DateTimeStyle.nsStyle(): NSDateFormatterStyle = + when (this) { + DateTimeStyle.NONE -> NSDateFormatterNoStyle + DateTimeStyle.SHORT -> NSDateFormatterShortStyle + DateTimeStyle.MEDIUM -> NSDateFormatterMediumStyle + DateTimeStyle.LONG -> NSDateFormatterLongStyle + } + +actual fun formatDateTime( + epochMillis: Long, + date: DateTimeStyle, + time: DateTimeStyle, +): String { + if (date == DateTimeStyle.NONE && time == DateTimeStyle.NONE) return "" + val formatter = + NSDateFormatter().apply { + dateStyle = date.nsStyle() + timeStyle = time.nsStyle() + } + return formatter.stringFromDate(NSDate.dateWithTimeIntervalSince1970(epochMillis / 1000.0)) +} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.ios.kt new file mode 100644 index 0000000000..e1ddc9efe5 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.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.viewmodels + +import androidx.compose.runtime.Composable + +@Composable +actual fun mockAccountViewModel(): AccountViewModel = error("Previews render on Android and Desktop only") + +@Composable +actual fun mockVitorAccountViewModel(): AccountViewModel = error("Previews render on Android and Desktop only") diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.jvmAndroid.kt new file mode 100644 index 0000000000..d5ce157428 --- /dev/null +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateTimeStyle.jvmAndroid.kt @@ -0,0 +1,47 @@ +/* + * 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 java.text.DateFormat +import java.util.Date + +private fun DateTimeStyle.javaStyle(): Int = + when (this) { + DateTimeStyle.SHORT -> DateFormat.SHORT + DateTimeStyle.MEDIUM -> DateFormat.MEDIUM + DateTimeStyle.LONG, DateTimeStyle.NONE -> DateFormat.LONG + } + +/** A fresh `DateFormat` per call: they are not thread-safe, and the locale may have changed. */ +actual fun formatDateTime( + epochMillis: Long, + date: DateTimeStyle, + time: DateTimeStyle, +): String { + val formatter = + when { + date == DateTimeStyle.NONE && time == DateTimeStyle.NONE -> return "" + date == DateTimeStyle.NONE -> DateFormat.getTimeInstance(time.javaStyle()) + time == DateTimeStyle.NONE -> DateFormat.getDateInstance(date.javaStyle()) + else -> DateFormat.getDateTimeInstance(date.javaStyle(), time.javaStyle()) + } + return formatter.format(Date(epochMillis)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelPreviews.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelPreviews.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt index d33c760e3e..4c9a3d0b40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelPreviews.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.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 +package com.vitorpamplona.amethyst.commons.viewmodels -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import com.vitorpamplona.amethyst.commons.model.Account @@ -31,6 +30,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.marmot.MarmotGroupNotifier import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientBuilder @@ -41,9 +41,6 @@ import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.commons.tor.TorType -import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost -import com.vitorpamplona.amethyst.model.accountsCache.defaultMarmotStreamTransport import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient @@ -62,11 +59,11 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import okhttp3.OkHttpClient -var mockedCache: AccountViewModel? = null +private var mockedCache: AccountViewModel? = null -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Composable -fun mockAccountViewModel(): AccountViewModel { +actual fun mockAccountViewModel(): AccountViewModel { mockedCache?.let { return it } val scope = rememberCoroutineScope() @@ -97,10 +94,7 @@ fun mockAccountViewModel(): AccountViewModel { signer = NostrSignerInternal(keyPair), geolocationFlow = { MutableStateFlow(LocationResult.Loading) }, nwcFilterAssembler = { nwcFilters }, - cashuMintDirectoryFilterAssembler = { - com.vitorpamplona.amethyst.commons.relayClient.assemblers - .CashuMintDirectoryFilterAssembler(client) - }, + cashuMintDirectoryFilterAssembler = { CashuMintDirectoryFilterAssembler(client) }, cashuMintTransport = OkHttpMintTransport { OkHttpClient() }, otsResolverBuilder = { EmptyOtsResolverBuilder.build() }, cache = LocalCache, @@ -112,7 +106,7 @@ fun mockAccountViewModel(): AccountViewModel { marmotNotifier = { MarmotGroupNotifier.None }, nip46Consent = Nip46ConsentPrompter.Unanswered, geohashIdentityStore = InMemoryGeohashIdentityStore(), - marmotStreamTransportFactory = ::defaultMarmotStreamTransport, + marmotStreamTransportFactory = { error("Previews do not stream") }, ) return AccountViewModel( @@ -128,11 +122,11 @@ fun mockAccountViewModel(): AccountViewModel { } } -var vitorCache: AccountViewModel? = null +private var vitorCache: AccountViewModel? = null -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Composable -fun mockVitorAccountViewModel(): AccountViewModel { +actual fun mockVitorAccountViewModel(): AccountViewModel { mockedCache?.let { return it } val scope = rememberCoroutineScope() @@ -161,10 +155,7 @@ fun mockVitorAccountViewModel(): AccountViewModel { signer = NostrSignerInternal(keyPair), geolocationFlow = { MutableStateFlow(LocationResult.Loading) }, nwcFilterAssembler = { nwcFilters }, - cashuMintDirectoryFilterAssembler = { - com.vitorpamplona.amethyst.commons.relayClient.assemblers - .CashuMintDirectoryFilterAssembler(client) - }, + cashuMintDirectoryFilterAssembler = { CashuMintDirectoryFilterAssembler(client) }, cashuMintTransport = OkHttpMintTransport { OkHttpClient() }, otsResolverBuilder = { EmptyOtsResolverBuilder.build() }, cache = LocalCache, @@ -176,7 +167,7 @@ fun mockVitorAccountViewModel(): AccountViewModel { marmotNotifier = { MarmotGroupNotifier.None }, nip46Consent = Nip46ConsentPrompter.Unanswered, geohashIdentityStore = InMemoryGeohashIdentityStore(), - marmotStreamTransportFactory = ::defaultMarmotStreamTransport, + marmotStreamTransportFactory = { error("Previews do not stream") }, ) return AccountViewModel( diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt new file mode 100644 index 0000000000..a991b09cfb --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.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 +import com.vitorpamplona.quartz.utils.Log + +private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { message } } + +@Composable +actual fun rememberShortNotice(): ShortNotice = loggedNotice diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.jvm.kt new file mode 100644 index 0000000000..f18e140df6 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/TextSharer.jvm.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 +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection + +private val clipboardSharer = TextSharer { text, _, _ -> Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(text), null) } + +@Composable +actual fun rememberTextSharer(): TextSharer = clipboardSharer diff --git a/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.skiko.kt b/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.skiko.kt new file mode 100644 index 0000000000..ad049ea672 --- /dev/null +++ b/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/service/image/ArgbImageBitmap.skiko.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.service.image + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asComposeImageBitmap +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.ColorAlphaType +import org.jetbrains.skia.ImageInfo + +/** Skia's N32 is BGRA in memory on every Skiko target, so each ARGB int is written low byte first. */ +actual fun argbPixelsToImageBitmap( + pixels: IntArray, + width: Int, + height: Int, +): ImageBitmap { + val bytes = ByteArray(pixels.size * 4) + for (i in pixels.indices) { + val argb = pixels[i] + val offset = i * 4 + bytes[offset] = (argb and 0xFF).toByte() + bytes[offset + 1] = ((argb shr 8) and 0xFF).toByte() + bytes[offset + 2] = ((argb shr 16) and 0xFF).toByte() + bytes[offset + 3] = ((argb shr 24) and 0xFF).toByte() + } + val bitmap = Bitmap() + bitmap.allocPixels(ImageInfo.makeN32(width, height, ColorAlphaType.UNPREMUL)) + bitmap.installPixels(bytes) + bitmap.setImmutable() + return bitmap.asComposeImageBitmap() +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/BigDecimalOps.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/BigDecimalOps.kt index a7df80889f..02c409d91d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/BigDecimalOps.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/BigDecimalOps.kt @@ -38,3 +38,12 @@ expect fun BigDecimal.toLongValue(): Long /** The nearest Double, as Number.toDouble() gives on every platform. An expect for [toLongValue]'s reason. */ expect fun BigDecimal.toDoubleValue(): Double + +private val PLAIN_DECIMAL = Regex("[+-]?(\\d+\\.?\\d*|\\.\\d+)") + +/** + * Parses a plain decimal ("12", "-0.5", ".5") into a [BigDecimal], or null for anything else, + * exponents included. The platform constructors disagree on bad input (the JVM throws, the others + * do not), so the check happens here, before any of them sees the text. + */ +fun parseBigDecimalOrNull(text: String): BigDecimal? = if (PLAIN_DECIMAL.matches(text)) BigDecimal(text) else null