refactor(note): cut JVM/Android ties out of the NoteCompose tree; translation and note slots

KMP swaps so the note types can live in commonMain:
- formatDateTime(DateTimeStyle) replaces SimpleDateFormat in NoteCompose, DisplayOts,
  CalendarEvent, Attestation, MeetingSpace and the live-stream flags
- DecimalPatternFormatter, PlatformNumberFormatter and the quartz BigDecimal (plus
  parseBigDecimalOrNull) replace java.text / java.math in the zap, goal, reward, poll,
  medical, classifieds and software-app types
- phonePrefersMiles() is a commons expect; timeDiffAgo* and formatSecondsToTime are common
- UrlInfoItem resolves OpenGraph URLs through resolveHttpUrl and moves to commonMain with
  the URL preview card, recording indicators and streaming flags
- NIP-95 local media is an okio Path from Nip95BlobStore.path(id)
- ic_dm, nip_05 and profile_banner move to commonsUI Compose resources (painterRes)
- podcast chapters/transcripts fetch through fetchPreviewText; live streams probe through
  isVideoOnline; the event "copy JSON" goes through toPrettyJson

Platform verbs as small commonsUI expect/actuals: rememberTextSharer (share sheet),
rememberShortNotice (toast), rememberBlossomUriOpener, argbPixelsToImageBitmap. Plain VIEW
intents use LocalUriHandler; the zap poll pays through AccountViewModelHost.

Slots: TranslationPlatform (Play installs ML Kit, F-Droid none) behind a shared
TranslatableRichTextViewer; RichTextPlatform gains SecretMessage; the preview
AccountViewModel mocks move to commonsUI. NotePlatform (media players, map, audio/chess/
git/meeting/napplet note types, reactions row, post editor) is defined with its Android
implementation installed in the theme; call sites are switched in the next step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-28 18:55:15 +00:00
parent 3fc3cc6311
commit 83add4d355
210 changed files with 3846 additions and 1057 deletions
@@ -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
@@ -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<Float>,
)
@Composable
fun VideoView(
videoUri: String,
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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(
@@ -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
@@ -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
@@ -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<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) = CoreSecretMessage(content, callbackUri, quotesLeft, backgroundColor, accountViewModel, nav)
}
@@ -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)
@@ -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
@@ -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<Boolean>,
) {
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<Boolean>) =
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,
)
}
@@ -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)
}
}
@@ -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) } },
)
}
@@ -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)
@@ -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 =
@@ -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<Boolean>,
) {
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<Boolean>) =
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()
@@ -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
@@ -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,
@@ -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
@@ -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,
@@ -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
@@ -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)
@@ -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,
@@ -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()
@@ -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,
@@ -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),
)
},
)
@@ -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,
@@ -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
@@ -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<StringToastMsg?>(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()
@@ -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<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
var zappedValue: MutableState<BigDecimal> = mutableStateOf(ZERO),
var tally: MutableState<Float> = mutableFloatStateOf(0f),
var consensusThreadhold: MutableState<Boolean> = mutableStateOf(false),
var zappedByLoggedIn: MutableState<Boolean> = mutableStateOf(false),
@@ -57,13 +61,11 @@ class PollNoteViewModel : ViewModel() {
private var pollOptions: Map<Int, String>? = 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<Long>): List<Long> {
val options =
@@ -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,
@@ -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
@@ -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,
@@ -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),
)
},
)
@@ -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) {
@@ -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()
}
@@ -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
@@ -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)) {
@@ -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<BaseMediaContent>,
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<BaseMediaContent>,
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<Color>,
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<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) = AppRenderChessGame(
note = note,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
@Composable
override fun RenderLiveChessChallenge(
note: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) = AppRenderLiveChessChallenge(
note = note,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
@Composable
override fun RenderLiveChessGameEnd(
note: Note,
backgroundColor: MutableState<Color>,
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<Color>,
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<Color>,
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<Color>,
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<Color>,
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<GenericLoadable<EditState>>?,
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,
)
}
@@ -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<BaseMediaContent>,
roundedCorner: Boolean,
contentScale: ContentScale,
accountViewModel: AccountViewModel,
) {}
@Composable
fun ZoomableImageDialog(
imageUrl: BaseMediaContent,
allImages: ImmutableList<BaseMediaContent>,
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<Color>,
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<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {}
@Composable
fun RenderLiveChessChallenge(
note: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {}
@Composable
fun RenderLiveChessGameEnd(
note: Note,
backgroundColor: MutableState<Color>,
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<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {}
@Composable
fun RenderGitIssueEvent(
baseNote: Note,
makeItShort: Boolean,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {}
@Composable
fun RenderGitPullRequestEvent(
baseNote: Note,
makeItShort: Boolean,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {}
@Composable
fun RenderGitPullRequestUpdateEvent(
baseNote: Note,
makeItShort: Boolean,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
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<GenericLoadable<EditState>>?,
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> { NotePlatform.None }
@@ -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<BaseMediaContent> = 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<BaseMediaContent> = 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<Color>,
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<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) = LocalNotePlatform.current.RenderChessGame(
note = note,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
@Composable
fun RenderLiveChessChallenge(
note: Note,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) = LocalNotePlatform.current.RenderLiveChessChallenge(
note = note,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
@Composable
fun RenderLiveChessGameEnd(
note: Note,
backgroundColor: MutableState<Color>,
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<Color>,
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<Color>,
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<Color>,
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<Color>,
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<GenericLoadable<EditState>>?,
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,
)
@@ -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
@@ -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 =
@@ -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,
)
@@ -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
@@ -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)
@@ -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
}
@@ -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,
@@ -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) {
@@ -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(
@@ -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
@@ -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()
@@ -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
@@ -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,
@@ -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)
}
@@ -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
@@ -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 ""
@@ -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),
@@ -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
@@ -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
@@ -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.
@@ -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,
@@ -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>(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')}"
}
}
@@ -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<PodcastChapter>(), 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()
}
@@ -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
@@ -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() }
}
@@ -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() {
@@ -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
@@ -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)
}
}
@@ -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,
@@ -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"
}
@@ -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
@@ -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)
}
@@ -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
@@ -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
@@ -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
@@ -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).
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
) {
@@ -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,
) {
@@ -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,
) {
@@ -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
@@ -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(
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
@@ -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
@@ -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
@@ -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

Some files were not shown because too many files have changed in this diff Show More