mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4210 from vitorpamplona/claude/affectionate-ptolemy-0q909o
refactor(commonsUI): move ScreenLayout, TimeAgo, the theme core, chat bubbles and single-blocker components
This commit is contained in:
@@ -24768,17 +24768,17 @@ SPLcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;->getActivityOrNull(La
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
|
||||
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$0(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/ui/screen/AccountState;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$2(Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;Lcom/vitorpamplona/amethyst/ui/screen/AccountState;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
@@ -25632,6 +25632,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize40dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize50dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;
|
||||
@@ -25648,13 +25649,13 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9(Landroid
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/model/FontSizeType;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->accentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystDarkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->lightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystLightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccent-8_81llA(J)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccentContainer-DxMtmZc(JZ)J
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;
|
||||
@@ -25685,7 +25686,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpaci
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont14SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont17SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
|
||||
+1
-1
@@ -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.ui.components.AudioWaveformReadOnly
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
|
||||
import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly
|
||||
|
||||
@Composable
|
||||
fun Waveform(
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider
|
||||
import com.vitorpamplona.amethyst.debugState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
@@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForPointer
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NowProvider
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink
|
||||
|
||||
+1
-1
@@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.commons.resources.re_record
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
+1
-97
@@ -24,9 +24,6 @@ import android.Manifest
|
||||
import android.os.Build
|
||||
import android.view.Window
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
@@ -47,19 +44,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
@@ -75,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_download_has_started_toast
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.animatedViewerChromeInset
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
@@ -83,29 +75,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
// Chrome shared by the full-screen media viewers -- the zoomable image/video dialog and the PDF
|
||||
// viewer. Both are opened the same way (tap a media card in a feed), so they immerse, auto-hide,
|
||||
// and lay their controls out identically.
|
||||
|
||||
// Opening churn -- insets arriving, then the bars being hidden -- must not look like a user
|
||||
// gesture, so the row snaps through it and only animates afterwards.
|
||||
private const val CONTROLS_SETTLE_BEFORE_ANIMATING_MS = 350L
|
||||
|
||||
// Roughly the system bars' own show/hide duration, so the row travels with them rather than
|
||||
// trailing after they have already arrived.
|
||||
private const val CONTROLS_SLIDE_MS = 200
|
||||
|
||||
// Keeps the row off the screen edge -- and off the rounded corners -- while the bars are hidden.
|
||||
private val VIEWER_CHROME_EDGE_GAP = 16.dp
|
||||
|
||||
// How long the controls stay up before the viewer fades them out on its own.
|
||||
private const val CONTROLS_AUTO_HIDE_DELAY_MS = 2000L
|
||||
|
||||
/**
|
||||
* Goes fully immersive for as long as the viewer is on screen: hides both OS bars and restores them
|
||||
* on the way out. BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE lets the user swipe to peek the bars back.
|
||||
@@ -123,77 +98,6 @@ fun ImmersiveSystemBarsEffect(window: Window?) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility of the viewer controls: they start on screen, fade out on their own after
|
||||
* [CONTROLS_AUTO_HIDE_DELAY_MS], and the caller flips the returned state on tap.
|
||||
*
|
||||
* [holdOpen] freezes the timer while something anchored to the controls -- the share sheet, say --
|
||||
* is up, and re-arms it once that closes. [armed] withholds the countdown until there is something
|
||||
* to look at, so a viewer that spends three seconds fetching its media doesn't reveal the first
|
||||
* frame with the controls already gone.
|
||||
*
|
||||
* A tap that brings the controls back deliberately gets no timer: the user asked for them, so they
|
||||
* stay until tapped away. That is why the countdown races the controls going away rather than just
|
||||
* sleeping -- a timer left over from an earlier show would otherwise wipe controls the user tapped
|
||||
* back up in the meantime.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberViewerControlsVisibility(
|
||||
holdOpen: Boolean,
|
||||
armed: Boolean = true,
|
||||
): MutableState<Boolean> {
|
||||
val visible = remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(armed, holdOpen) {
|
||||
if (!armed || holdOpen) return@LaunchedEffect
|
||||
|
||||
val hiddenFirst =
|
||||
withTimeoutOrNull(CONTROLS_AUTO_HIDE_DELAY_MS) {
|
||||
snapshotFlow { visible.value }.first { !it }
|
||||
}
|
||||
|
||||
if (hiddenFirst == null) visible.value = false
|
||||
}
|
||||
|
||||
return visible
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the viewer chrome sits from a screen edge: the system bar's own height while the bar is
|
||||
* on screen, and a thin constant once it is hidden -- so the chrome follows the bar instead of
|
||||
* reserving space for one that is not there.
|
||||
*
|
||||
* This only works because the viewer asks for BEHAVIOR_DEFAULT rather than transient bars. Under
|
||||
* BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE a peeked bar is painted over the content and dispatches no
|
||||
* insets at all -- `systemBars` stays 0 and `isVisible` stays false the whole time it is on screen
|
||||
* -- so nothing here could react to it.
|
||||
*
|
||||
* The value is animated, but snapped for [CONTROLS_SETTLE_BEFORE_ANIMATING_MS] after the chrome
|
||||
* appears. Opening moves the inset twice for reasons the user did not cause: the window has not
|
||||
* been told its insets yet (they read 0), and ImmersiveSystemBarsEffect hides the bars from a
|
||||
* DisposableEffect that runs after composition. Animating either would play a slide on open.
|
||||
*/
|
||||
@Composable
|
||||
fun animatedViewerChromeInset(atBottom: Boolean): Dp {
|
||||
val density = LocalDensity.current
|
||||
val bars = WindowInsets.systemBars
|
||||
val barPx = if (atBottom) bars.getBottom(density) else bars.getTop(density)
|
||||
val target = with(density) { maxOf(barPx, VIEWER_CHROME_EDGE_GAP.roundToPx()).toDp() }
|
||||
|
||||
var settled by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(CONTROLS_SETTLE_BEFORE_ANIMATING_MS)
|
||||
settled = true
|
||||
}
|
||||
|
||||
val animated by animateDpAsState(
|
||||
targetValue = target,
|
||||
animationSpec = if (settled) tween(durationMillis = CONTROLS_SLIDE_MS) else snap(),
|
||||
label = "viewerChromeInset",
|
||||
)
|
||||
return animated
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays a viewer control row along a screen edge -- the top by default, the bottom when [atBottom].
|
||||
*
|
||||
|
||||
+2
@@ -80,8 +80,10 @@ 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.ui.components.SlidingCarousel
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components.pdf
|
||||
|
||||
import coil3.disk.DiskCache
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import java.io.IOException
|
||||
|
||||
object PdfFetcher {
|
||||
/**
|
||||
* Returns a snapshot of the cached PDF for [url], downloading it if necessary. The caller is
|
||||
* responsible for closing the returned snapshot; while it's open the cache entry cannot be
|
||||
* evicted, so the underlying file stays valid for `PdfRenderer`.
|
||||
*
|
||||
* Reuses the Coil disk cache (`Amethyst.instance.diskCache`) so PDFs share the same LRU
|
||||
* eviction and disk budget as images.
|
||||
*/
|
||||
suspend fun fetchSnapshot(
|
||||
url: String,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
): DiskCache.Snapshot =
|
||||
withContext(Dispatchers.IO) {
|
||||
val diskCache = Amethyst.instance.diskCache
|
||||
// Covers the cache-hit fast path too, not just the download below it. openSnapshot()
|
||||
// contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a
|
||||
// burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller
|
||||
// that happens to be on the main thread stalls the frame for that whole burst, and the
|
||||
// hit path is exactly the one a feed takes when a PDF card scrolls back into view.
|
||||
diskCache.openSnapshot(url)?.let { return@withContext it }
|
||||
|
||||
val editor = diskCache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url")
|
||||
try {
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
okHttpClient(url).newCall(request).executeAsync().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("PDF download failed: ${response.code}")
|
||||
}
|
||||
diskCache.fileSystem.write(editor.data) {
|
||||
val bytes = writeAll(response.body.source())
|
||||
if (bytes == 0L) throw IOException("PDF download failed: empty response body")
|
||||
}
|
||||
}
|
||||
|
||||
editor.commitAndOpenSnapshot() ?: throw IOException("Unable to commit cache editor for $url")
|
||||
} catch (t: Throwable) {
|
||||
runCatching { editor.abort() }
|
||||
throw t
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-15
@@ -44,22 +44,23 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
|
||||
import com.vitorpamplona.amethyst.commons.service.pdf.PdfFetcher
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size6dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.commons.util.extractFilename
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.FileAttachmentRow
|
||||
import com.vitorpamplona.amethyst.ui.components.ShareMediaAction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
// Hard ceiling on the inline thumbnail bitmap, in pixels. Prevents OOM on very tall/large pages.
|
||||
private const val THUMBNAIL_MAX_DIM_PX = 1600
|
||||
@@ -155,12 +156,12 @@ private fun LoadedPdfPreviewCard(
|
||||
value =
|
||||
try {
|
||||
PdfFetcher
|
||||
.fetchSnapshot(content.url) { url ->
|
||||
accountViewModel.httpClientBuilder.okHttpClientForPreview(url)
|
||||
}.use { snapshot ->
|
||||
withContext(Dispatchers.IO) {
|
||||
renderFirstPage(snapshot.data.toFile(), targetWidthPx)
|
||||
}
|
||||
.useSnapshot(
|
||||
url = content.url,
|
||||
diskCache = { Amethyst.instance.diskCache },
|
||||
okHttpClient = { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) },
|
||||
) { snapshot ->
|
||||
renderFirstPage(snapshot.data.toFile(), targetWidthPx)
|
||||
}.also { result ->
|
||||
// Same cache the image and video paths use, so a PDF that has been rendered
|
||||
// once lays out at its real shape on every later visit instead of growing
|
||||
@@ -327,10 +328,4 @@ internal fun cappedRenderSize(
|
||||
return w to h
|
||||
}
|
||||
|
||||
internal fun extractFilename(url: String): String {
|
||||
val afterQuery = url.substringBefore('?').substringBefore('#')
|
||||
val name = afterQuery.substringAfterLast('/', afterQuery)
|
||||
return if (name.isBlank()) url else name
|
||||
}
|
||||
|
||||
internal fun pageCountLabel(pageCount: Int): String = if (pageCount == 1) "1 page" else "$pageCount pages"
|
||||
|
||||
+13
-3
@@ -61,8 +61,11 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.graphics.createBitmap
|
||||
import coil3.disk.DiskCache
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
|
||||
import com.vitorpamplona.amethyst.commons.service.pdf.PdfFetcher
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.components.ImmersiveSystemBarsEffect
|
||||
@@ -70,7 +73,6 @@ import com.vitorpamplona.amethyst.ui.components.ViewerBackButton
|
||||
import com.vitorpamplona.amethyst.ui.components.ViewerControlsRow
|
||||
import com.vitorpamplona.amethyst.ui.components.ViewerSaveToGalleryButton
|
||||
import com.vitorpamplona.amethyst.ui.components.ViewerShareButton
|
||||
import com.vitorpamplona.amethyst.ui.components.rememberViewerControlsVisibility
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -80,6 +82,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -194,7 +197,7 @@ private fun PdfViewerContent(
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val snapshot =
|
||||
PdfFetcher.fetchSnapshot(content.url) { url ->
|
||||
PdfFetcher.fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url ->
|
||||
accountViewModel.httpClientBuilder.okHttpClientForPreview(url)
|
||||
}
|
||||
try {
|
||||
@@ -221,7 +224,14 @@ private fun PdfViewerContent(
|
||||
val handleForDispose = handleState
|
||||
DisposableEffect(handleForDispose) {
|
||||
onDispose {
|
||||
handleForDispose?.close()
|
||||
// Off the main thread: closing the cache snapshot takes Coil's global DiskLruCache
|
||||
// lock, which its cleanup pass holds across bursts of unlinks. Under the render mutex,
|
||||
// so a page render still in flight finishes before the renderer is closed under it.
|
||||
handleForDispose?.let { handle ->
|
||||
Amethyst.instance.applicationIOScope.launch {
|
||||
handle.mutex.withLock { handle.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
|
||||
@@ -20,126 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.ScreenLayoutSpec
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberScreenLayoutSpec
|
||||
|
||||
/**
|
||||
* Minimum window width for the docked notification panel: a leading navigation pane, a
|
||||
* readable center pane and the panel ([NotificationPanelWidth]) only coexist comfortably from
|
||||
* a landscape-tablet-sized window up. Sized against the widest leading pane, the permanent
|
||||
* drawer ([PermanentDrawerWidth]); the rail is narrower, so a railed window that clears this
|
||||
* gets a roomier center pane rather than a tighter one.
|
||||
*/
|
||||
private const val NOTIFICATION_PANEL_MIN_WINDOW_DP = 1200
|
||||
|
||||
val PermanentDrawerWidth = 300.dp
|
||||
|
||||
val NotificationPanelWidth = 360.dp
|
||||
|
||||
/**
|
||||
* Maximum width of a screen's content column inside a wide center pane. Every NavHost
|
||||
* destination is wrapped in [CappedScreenContent] (via the builders in NavigationEffects),
|
||||
* so the whole screen — top bar, tabs, feed, settings rows — shares one centered reading
|
||||
* column instead of stretching across the pane. Screens that genuinely need the full pane
|
||||
* (Messages' two-pane split, the embedded browser surfaces) opt out at registration.
|
||||
*/
|
||||
val FeedContentMaxWidth = 600.dp
|
||||
|
||||
/**
|
||||
* Minimum window height for the docked drawer. Higher than Material's 480dp Compact/Medium
|
||||
* height boundary on purpose: the permanent drawer's own header — banner, avatar, status
|
||||
* editor, follower counts — fills most of a ~540dp column before the first navigation row, so
|
||||
* below this the rail shows more of the menu than the dock does.
|
||||
*/
|
||||
private const val DOCK_MIN_WINDOW_HEIGHT_DP = 600
|
||||
|
||||
/**
|
||||
* The navigation tier for a window of this shape.
|
||||
*
|
||||
* The dock is not a width decision. A tablet is past the Expanded breakpoint in both
|
||||
* orientations, so keying on width alone pins 300dp of menu open in portrait with no closed
|
||||
* state to fall back on (issue #4024). It docks only when the window is wide, landscape, and
|
||||
* tall enough for the drawer's own content to be usable; everything else that is not Compact
|
||||
* falls through to the rail, which pairs with the existing swipe-in modal drawer.
|
||||
*
|
||||
* A square window counts as landscape and docks; `Configuration.ORIENTATION_LANDSCAPE`
|
||||
* breaks that tie the other way, so the two disagree at exactly width == height.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
internal fun decideNavigationStyle(
|
||||
windowWidthDp: Int,
|
||||
windowHeightDp: Int,
|
||||
): NavigationStyle {
|
||||
val widthSizeClass =
|
||||
WindowSizeClass
|
||||
.calculateFromSize(DpSize(windowWidthDp.dp, windowHeightDp.dp))
|
||||
.widthSizeClass
|
||||
|
||||
return when {
|
||||
widthSizeClass == WindowWidthSizeClass.Expanded &&
|
||||
windowWidthDp >= windowHeightDp &&
|
||||
windowHeightDp >= DOCK_MIN_WINDOW_HEIGHT_DP -> NavigationStyle.PERMANENT_DRAWER
|
||||
widthSizeClass != WindowWidthSizeClass.Compact -> NavigationStyle.NAV_RAIL
|
||||
else -> NavigationStyle.BOTTOM_BAR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the window is wide enough to dock the notification feed beside the content.
|
||||
*
|
||||
* Deliberately not keyed on [NavigationStyle]: a wide portrait window now gets the rail, and
|
||||
* gating on the dock would strip a panel it has today.
|
||||
*/
|
||||
internal fun hasRoomForNotificationPanel(windowWidthDp: Int): Boolean = windowWidthDp >= NOTIFICATION_PANEL_MIN_WINDOW_DP
|
||||
|
||||
/**
|
||||
* Centers a destination's content at [FeedContentMaxWidth]. The outer box paints the theme
|
||||
* background so the gutters match the screens' own surfaces; on Compact windows the cap is
|
||||
* wider than the pane and this is a visual no-op.
|
||||
*/
|
||||
@Composable
|
||||
fun CappedScreenContent(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.widthIn(max = FeedContentMaxWidth)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Android's window size is the configuration's, which already excludes system decorations. */
|
||||
@Composable
|
||||
fun rememberScreenLayoutSpec(): ScreenLayoutSpec {
|
||||
val configuration = LocalConfiguration.current
|
||||
val windowWidthDp = configuration.screenWidthDp
|
||||
val windowHeightDp = configuration.screenHeightDp
|
||||
return remember(windowWidthDp, windowHeightDp) {
|
||||
ScreenLayoutSpec(
|
||||
navigationStyle = decideNavigationStyle(windowWidthDp, windowHeightDp),
|
||||
hasRoomForNotificationPanel = hasRoomForNotificationPanel(windowWidthDp),
|
||||
)
|
||||
}
|
||||
return rememberScreenLayoutSpec(configuration.screenWidthDp, configuration.screenHeightDp)
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.vitorpamplona.amethyst.ui.layouts.CappedScreenContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.CappedScreenContent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
+1
-1
@@ -119,6 +119,7 @@ import com.vitorpamplona.amethyst.commons.resources.show_npub_as_a_qr_code
|
||||
import com.vitorpamplona.amethyst.commons.resources.status_update
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
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.richtext.CreateTextWithEmoji
|
||||
@@ -150,7 +151,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses
|
||||
import com.vitorpamplona.amethyst.ui.layouts.PermanentDrawerWidth
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
|
||||
@@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.new_badge_award_notif
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
|
||||
@@ -96,6 +96,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation
|
||||
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.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -133,8 +136,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.Reward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add
|
||||
import com.vitorpamplona.amethyst.commons.resources.posts_received
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
|
||||
+1
-1
@@ -63,10 +63,10 @@ import com.vitorpamplona.amethyst.commons.resources.expiration_date_select
|
||||
import com.vitorpamplona.amethyst.commons.resources.expiration_expires_in
|
||||
import com.vitorpamplona.amethyst.commons.resources.expiration_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.next
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
+1
-1
@@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.next
|
||||
import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.poll_closing_in
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.Instant
|
||||
|
||||
+1
-1
@@ -76,10 +76,10 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_time_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi
|
||||
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single
|
||||
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
|
||||
+1
-1
@@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.next
|
||||
import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.poll_closing_in
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.Instant
|
||||
|
||||
@@ -29,9 +29,9 @@ 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.HeaderPill
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadOts
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
@@ -30,7 +30,7 @@ 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.RichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.components.FileAttachmentCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentCard
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.CopyIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
@@ -59,7 +60,6 @@ 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.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@@ -68,6 +68,7 @@ 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.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.FileAttachmentCard
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
|
||||
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.long_form_reading_minutes
|
||||
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ private fun PermanentDrawerShell(
|
||||
|
||||
/**
|
||||
* Hosts the navigation content. Screen width capping happens per NavHost destination
|
||||
* ([com.vitorpamplona.amethyst.ui.layouts.CappedScreenContent] via the NavigationEffects
|
||||
* ([com.vitorpamplona.amethyst.commons.ui.layouts.CappedScreenContent] via the NavigationEffects
|
||||
* builders), so this pane just claims the leftover row width.
|
||||
*/
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -95,8 +95,8 @@ import com.vitorpamplona.amethyst.commons.resources.backup_conflict_count_remove
|
||||
import com.vitorpamplona.amethyst.commons.resources.backup_conflict_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff
|
||||
|
||||
+1
-1
@@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_review_title_updated
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
@@ -101,7 +102,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+2
-2
@@ -90,11 +90,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
+9
-5
@@ -82,15 +82,16 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_more
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_dm_new
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.now
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_messages
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.TimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isValid
|
||||
@@ -127,6 +128,8 @@ fun BuzzDmListScreen(
|
||||
// Hidden conversations stay collapsed behind a header — they are off Messages by the user's own
|
||||
// choice, so they must not compete with the live inbox; they only need to be *reachable* again.
|
||||
var showHidden by remember { mutableStateOf(false) }
|
||||
// Resolved once for the list, not per row.
|
||||
val timeLabels = rememberTimeAgoLabels()
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringRes(Res.string.buzz_dm_title), nav) },
|
||||
@@ -149,7 +152,7 @@ fun BuzzDmListScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(rows, key = { it.channelId }) { row ->
|
||||
DmRowCard(row, isHidden = false, viewModel = viewModel, accountViewModel = accountViewModel, nav = nav)
|
||||
DmRowCard(row, isHidden = false, viewModel = viewModel, timeLabels = timeLabels, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
if (hiddenRows.isNotEmpty()) {
|
||||
item(key = "hidden-header") {
|
||||
@@ -161,7 +164,7 @@ fun BuzzDmListScreen(
|
||||
}
|
||||
if (showHidden) {
|
||||
items(hiddenRows, key = { "hidden-${it.channelId}" }) { row ->
|
||||
DmRowCard(row, isHidden = true, viewModel = viewModel, accountViewModel = accountViewModel, nav = nav)
|
||||
DmRowCard(row, isHidden = true, viewModel = viewModel, timeLabels = timeLabels, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +214,7 @@ private fun DmRowCard(
|
||||
row: BuzzDmListViewModel.DmRow,
|
||||
isHidden: Boolean,
|
||||
viewModel: BuzzDmListViewModel,
|
||||
timeLabels: TimeAgoLabels,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -256,7 +260,7 @@ private fun DmRowCard(
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)),
|
||||
text = timeAgoShort(row.lastActivity, timeLabels),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
+1
-1
@@ -54,11 +54,11 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChann
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_no_messages_yet
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription
|
||||
|
||||
+2
-2
@@ -88,12 +88,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_job_board_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
+2
-2
@@ -160,12 +160,12 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_for_ap
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_on
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
|
||||
+1
-1
@@ -118,10 +118,10 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_deta
|
||||
import com.vitorpamplona.amethyst.commons.resources.cordn_member_count
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.model.cordn.CordnCoverage
|
||||
import com.vitorpamplona.amethyst.model.cordn.CordnGroupCreation
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex
|
||||
|
||||
+1
-1
@@ -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.chats.ui.AutoScrollToNewest
|
||||
import com.vitorpamplona.amethyst.commons.cordn.CordnGroupManager
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -121,7 +122,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.AutoScrollToNewest
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
|
||||
+7
-7
@@ -55,8 +55,13 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.CHAT_GROUP_WINDOW_SECONDS
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatBubbleLayout
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatDivisor
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.UserDisplayNameLayout
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiFontSize
|
||||
import com.vitorpamplona.amethyst.commons.cordn.CordnMentions
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -89,6 +94,8 @@ import com.vitorpamplona.amethyst.commons.resources.today
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
@@ -98,8 +105,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ActionTile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatChipFlowRow
|
||||
@@ -109,11 +114,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ReactionChipView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.SectionDivider
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.TileRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.authorNameColorFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiFontSize
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.CHAT_GROUP_WINDOW_SECONDS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
+2
-4
@@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
@@ -70,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.chat_delivery_sending
|
||||
import com.vitorpamplona.amethyst.commons.resources.close
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAbsolute
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
@@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAbsolute
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -203,9 +202,8 @@ private fun ChatDeliveryDetailDialog(
|
||||
) {
|
||||
// Absolute timestamp header (the chat time no longer toggles to absolute
|
||||
// on tap — it opens this dialog instead).
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
text = timeAbsolute(baseNote.createdAt(), context, prefix = "").trim(),
|
||||
text = timeAbsolute(baseNote.createdAt(), prefix = "").trim(),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = Font12SP,
|
||||
)
|
||||
|
||||
+3
-1
@@ -36,6 +36,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.AutoScrollToNewest
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.NewDateOrSubjectDivisor
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.watchChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -48,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.watchChatGroupPosition
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
+3
-2
@@ -45,6 +45,9 @@ import androidx.compose.ui.Alignment.Companion.CenterStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatBubbleLayout
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit
|
||||
import com.vitorpamplona.amethyst.commons.model.latestConcordEdit
|
||||
@@ -60,8 +63,6 @@ import com.vitorpamplona.amethyst.ui.note.WatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote
|
||||
|
||||
+3
-3
@@ -40,15 +40,15 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.expiration_date_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_pinned_content_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
|
||||
|
||||
+2
-2
@@ -26,6 +26,8 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiFontSize
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
|
||||
@@ -40,8 +42,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAudioFromIMeta
|
||||
import com.vitorpamplona.amethyst.ui.note.types.appendMissingImetaUrls
|
||||
import com.vitorpamplona.amethyst.ui.note.types.getAudioMetaWithWaveform
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiFontSize
|
||||
|
||||
@Composable
|
||||
fun RenderRegularTextNote(
|
||||
|
||||
+1
-1
@@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -144,7 +145,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
+2
-2
@@ -84,13 +84,13 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_tab_new_requests_coun
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_unread_messages
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasMip04Media
|
||||
|
||||
+1
-1
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_
|
||||
import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder
|
||||
@@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.showCount
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||
|
||||
+1
-1
@@ -103,10 +103,10 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreviewLoader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription
|
||||
|
||||
+1
-1
@@ -71,13 +71,13 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer
|
||||
|
||||
+1
-1
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor
|
||||
import com.vitorpamplona.amethyst.commons.resources.owner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.MuteChatButton
|
||||
|
||||
+1
-1
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.resources.created_at
|
||||
import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor
|
||||
import com.vitorpamplona.amethyst.commons.resources.owner
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
|
||||
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags
|
||||
|
||||
+9
-3
@@ -85,7 +85,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_see_all_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_forum_create_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_import_loading
|
||||
import com.vitorpamplona.amethyst.commons.resources.now
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_channels_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_channels_not_nip29
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_create_title
|
||||
@@ -100,6 +99,9 @@ import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.TimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -109,7 +111,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzAddPeopleDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmListViewModel
|
||||
@@ -263,6 +264,8 @@ fun RelayGroupChannelListScreen(
|
||||
// as the workspace overflow menu (Add people / Invite), whose own doc notes "any member sees them,
|
||||
// the relay only serves the owner/admin ones."
|
||||
val myPubkey = accountViewModel.account.signer.pubKey
|
||||
// Resolved once for the list, not per DM row.
|
||||
val timeLabels = rememberTimeAgoLabels()
|
||||
|
||||
val buzzVm: BuzzRelayImportViewModel = viewModel(key = "BuzzImport-${relay.url}")
|
||||
LaunchedEffect(relay, isBuzz) { if (isBuzz) buzzVm.bind(accountViewModel.account, relay.url) }
|
||||
@@ -644,6 +647,7 @@ fun RelayGroupChannelListScreen(
|
||||
BuzzDmInlineRow(
|
||||
row = row,
|
||||
myPubkey = myPubkey,
|
||||
timeLabels = timeLabels,
|
||||
isHidden = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -675,6 +679,7 @@ fun RelayGroupChannelListScreen(
|
||||
BuzzDmInlineRow(
|
||||
row = row,
|
||||
myPubkey = myPubkey,
|
||||
timeLabels = timeLabels,
|
||||
isHidden = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@@ -828,6 +833,7 @@ private fun SectionAddButton(
|
||||
private fun BuzzDmInlineRow(
|
||||
row: BuzzDmListViewModel.DmRow,
|
||||
myPubkey: HexKey,
|
||||
timeLabels: TimeAgoLabels,
|
||||
isHidden: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
@@ -885,7 +891,7 @@ private fun BuzzDmInlineRow(
|
||||
}
|
||||
if (row.lastActivity > 0) {
|
||||
Text(
|
||||
text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)),
|
||||
text = timeAgoShort(row.lastActivity, timeLabels),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
+1
-1
@@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -96,7 +97,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.relayGroupDiscoveryChannelFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.toGroupConstraints
|
||||
|
||||
+2
-2
@@ -111,6 +111,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier
|
||||
@@ -133,8 +135,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex
|
||||
|
||||
+1
-1
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_untitled
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.StatusKind
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
@@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.clickableNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
|
||||
+1
-1
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_tags
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_updated
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_untitled
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
@@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip34Git.git.GitCommit
|
||||
|
||||
+1
-1
@@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_loading
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
|
||||
@@ -97,7 +98,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag
|
||||
|
||||
+1
-1
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.route_notifications
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.NotificationPanelWidth
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
@@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size12dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.layouts.NotificationPanelWidth
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -43,13 +43,13 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_episode_number
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_season
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_season_episode
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoWith
|
||||
import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.podcasts.PodcastEpisode
|
||||
|
||||
+2
-2
@@ -42,11 +42,11 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_trailer
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_season
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoWith
|
||||
import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent
|
||||
|
||||
+4
-4
@@ -94,6 +94,10 @@ import com.vitorpamplona.amethyst.commons.resources.poll_results_your_pick
|
||||
import com.vitorpamplona.amethyst.commons.resources.poll_single_choice
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
@@ -115,10 +119,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.note.types.UserGallery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.results.datasources.PollResponsesFilterAssemblerSubscription
|
||||
|
||||
+1
-1
@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.resources.telegram
|
||||
import com.vitorpamplona.amethyst.commons.resources.twitter
|
||||
import com.vitorpamplona.amethyst.commons.resources.website
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.lastSeenSentence
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier
|
||||
@@ -78,7 +79,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.util.LongPressCopyText
|
||||
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
|
||||
import com.vitorpamplona.amethyst.ui.note.lastSeenSentence
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
|
||||
|
||||
+1
-1
@@ -112,13 +112,13 @@ import com.vitorpamplona.amethyst.commons.resources.relay_info
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.relayAuthPurposeLabelRes
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile
|
||||
|
||||
+1
-1
@@ -158,6 +158,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.appendLink
|
||||
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.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
@@ -180,7 +181,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.graspLink
|
||||
import com.vitorpamplona.amethyst.ui.note.nipLink
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
|
||||
+1
-1
@@ -106,11 +106,11 @@ import com.vitorpamplona.amethyst.commons.resources.event_sync_what_happens_titl
|
||||
import com.vitorpamplona.amethyst.commons.resources.event_sync_wifi_warning
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDotNoDay
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.note.formatMediumDate
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDotNoDay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
|
||||
+2
-2
@@ -129,12 +129,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.scheduledposts.ScheduledPostWorker
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
+2
-2
@@ -131,9 +131,9 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.contentColorOnAccent
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.previewColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.toFontFamily
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.previewColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.toFontFamily
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+1
-1
@@ -110,10 +110,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.model.cordn.CordnRuntime
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CopyableKeyRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection
|
||||
|
||||
+1
-1
@@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_ping
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_shared_pubkey
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_signed_kind
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_activity_denied
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
|
||||
private val LiveGreen = Color(0xFF3DDC84)
|
||||
|
||||
|
||||
+2
-2
@@ -102,6 +102,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RenderCodeSnippetHeaderForThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
@@ -164,8 +166,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.Reward
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayCommentScope
|
||||
import com.vitorpamplona.amethyst.ui.note.observeEdits
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
|
||||
+1
-1
@@ -33,12 +33,12 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -21,98 +21,31 @@
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons
|
||||
import com.vitorpamplona.amethyst.commons.model.AccentColorType
|
||||
import com.vitorpamplona.amethyst.commons.model.FontFamilyType
|
||||
import com.vitorpamplona.amethyst.commons.model.FontSizeType
|
||||
import com.vitorpamplona.amethyst.commons.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalAnimationsEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentOrangeDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentOrangeLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentPinkDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentPinkLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentRedDark
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AccentRedLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Purple200
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Purple500
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Shapes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Teal200
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Typography
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.amethystDarkColorScheme
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColorScheme
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.AmethystMaterialTheme
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.amethystDarkColors
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColors
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isDarkTheme
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.withFontFamily
|
||||
|
||||
// The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color.
|
||||
// Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent
|
||||
// uses its single hue across primary and secondary for a cohesive single-color theme.
|
||||
private fun accentPrimary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color =
|
||||
when (accent) {
|
||||
AccentColorType.PURPLE -> if (dark) Purple200 else Purple500
|
||||
AccentColorType.BLUE -> if (dark) AccentBlueDark else AccentBlueLight
|
||||
AccentColorType.GREEN -> if (dark) AccentGreenDark else AccentGreenLight
|
||||
AccentColorType.ORANGE -> if (dark) AccentOrangeDark else AccentOrangeLight
|
||||
AccentColorType.RED -> if (dark) AccentRedDark else AccentRedLight
|
||||
AccentColorType.PINK -> if (dark) AccentPinkDark else AccentPinkLight
|
||||
}
|
||||
|
||||
private fun accentSecondary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color = if (accent == AccentColorType.PURPLE) Teal200 else accentPrimary(accent, dark)
|
||||
|
||||
// Representative colour for an accent option, used by the Settings accent-picker swatches — the
|
||||
// same primary the theme would apply for the given light/dark mode, so the swatch previews the
|
||||
// real result.
|
||||
fun AccentColorType.previewColor(dark: Boolean): Color = accentPrimary(this, dark)
|
||||
|
||||
private fun darkColors(accent: AccentColorType): ColorScheme =
|
||||
amethystDarkColorScheme(
|
||||
primary = accentPrimary(accent, dark = true),
|
||||
secondary = accentSecondary(accent, dark = true),
|
||||
inversePrimary = accentPrimary(accent, dark = false),
|
||||
)
|
||||
|
||||
private fun lightColors(accent: AccentColorType): ColorScheme =
|
||||
amethystLightColorScheme(
|
||||
primary = accentPrimary(accent, dark = false),
|
||||
secondary = accentSecondary(accent, dark = false),
|
||||
inversePrimary = accentPrimary(accent, dark = true),
|
||||
)
|
||||
|
||||
val chartLightColors =
|
||||
VicoTheme(
|
||||
@@ -155,6 +88,10 @@ fun AmethystTheme(content: @Composable () -> Unit) {
|
||||
AmethystTheme(theme, accentColor, fontFamily, fontSize, displaySettings, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared [AmethystMaterialTheme] plus what only Android has: the system status and
|
||||
* navigation bars, tinted to match.
|
||||
*/
|
||||
@Composable
|
||||
fun AmethystTheme(
|
||||
prefTheme: ThemeType,
|
||||
@@ -168,44 +105,22 @@ fun AmethystTheme(
|
||||
// MODIFY_DAY_NIGHT_MODE, which this app does not declare, so the call silently no-ops — and it
|
||||
// ran on every recomposition of the theme, writing device state from inside composition. The
|
||||
// in-app choice is applied through the colour scheme below, which is what actually took effect.
|
||||
val darkTheme =
|
||||
when (prefTheme) {
|
||||
ThemeType.DARK -> true
|
||||
ThemeType.LIGHT -> false
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
val darkTheme = isDarkTheme(prefTheme)
|
||||
val colors =
|
||||
remember(darkTheme, accentColor) {
|
||||
if (darkTheme) darkColors(accentColor) else lightColors(accentColor)
|
||||
if (darkTheme) amethystDarkColors(accentColor) else amethystLightColors(accentColor)
|
||||
}
|
||||
|
||||
val resolvedFontFamily = remember(fontFamily) { fontFamily.toFontFamily() }
|
||||
val typography = remember(fontFamily) { Typography.withFontFamily(resolvedFontFamily) }
|
||||
|
||||
val density = LocalDensity.current
|
||||
val scaledDensity =
|
||||
remember(density, fontSize) {
|
||||
Density(density.density, density.fontScale * fontSize.scale)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
typography = typography,
|
||||
shapes = Shapes,
|
||||
content = {
|
||||
ProvideAppIcons {
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaledDensity,
|
||||
// ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge.
|
||||
LocalProfilePictureCache provides true,
|
||||
LocalDisplaySettings provides displaySettings,
|
||||
// Performance mode turns decorative animations (crossfades) off app-wide.
|
||||
LocalAnimationsEnabled provides !displaySettings.performanceMode,
|
||||
LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
},
|
||||
AmethystMaterialTheme(
|
||||
darkTheme = darkTheme,
|
||||
accentColor = accentColor,
|
||||
fontFamily = fontFamily,
|
||||
fontSize = fontSize,
|
||||
displaySettings = displaySettings,
|
||||
// ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge.
|
||||
profilePictureCache = true,
|
||||
colors = colors,
|
||||
content = content,
|
||||
)
|
||||
|
||||
val view = LocalView.current
|
||||
@@ -226,13 +141,3 @@ fun AmethystTheme(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maps the user-selected font preference to a Compose [FontFamily].
|
||||
// SYSTEM returns null so the platform default is used unchanged.
|
||||
fun FontFamilyType.toFontFamily(): FontFamily? =
|
||||
when (this) {
|
||||
FontFamilyType.SYSTEM -> null
|
||||
FontFamilyType.SANS_SERIF -> FontFamily.SansSerif
|
||||
FontFamilyType.SERIF -> FontFamily.Serif
|
||||
FontFamilyType.MONOSPACE -> FontFamily.Monospace
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.cordn
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.cordnGroupPositionFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.sameDayAs
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition
|
||||
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessage
|
||||
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnEnvelope
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@@ -1432,3 +1432,124 @@ functional bugs. The follow-ups:
|
||||
Java ignored, which no browser encoder produces. `%1$d` arguments now render ASCII
|
||||
digits in every locale, the same as every other migrated string.
|
||||
`ChannelFeedContentState` reaches `LocalCache.appHost` for its main-thread check.
|
||||
|
||||
## 2026-09-27 — the rest of the single-blocker UI, `TimeAgo`, and the theme
|
||||
|
||||
### Moved
|
||||
|
||||
| What | To | The blocker, and how it went |
|
||||
|---|---|---|
|
||||
| `ScreenLayout` tier logic, `CappedScreenContent`, the pane widths | `commonsUI/…/ui/layouts/ScreenLayout.kt` | `material3-window-size-class` (app-only): its width breakpoints (Medium ≥ 600dp, Expanded ≥ 840dp) are inlined, and `ScreenLayoutTest` (18 cases, now in commonsUI jvmTest) passes against them. `LocalConfiguration` stays in the app, which asks the shared `rememberScreenLayoutSpec(widthDp, heightDp)`, so Desktop can supply its own window size. |
|
||||
| `animatedViewerChromeInset`, `rememberViewerControlsVisibility` | `commonsUI/…/ui/components/ViewerChrome.kt` | They sat in a file full of Android window code. Only `ImmersiveSystemBarsEffect` (Window/insets controller) stays. |
|
||||
| `SlidingCarousel` | `commonsUI/…/ui/components` | needed the inset above |
|
||||
| `AudioWaveformReadOnly` | `commonsUI/…/ui/components` | two enums from the Android-only audiowaveform library, now local enums with the same values |
|
||||
| `FileAttachmentCard` (+ `FileAttachmentRow`, now public) | `commonsUI/…/ui/components` | `extractFilename` hoisted to `commons/…/util/MimeTypeLabels.kt` |
|
||||
| `PdfFetcher` | `commonsUI/src/jvmAndroid/…/service/pdf` | `Amethyst.instance.diskCache` → a `diskCache` parameter. commonsUI jvmAndroid gained `okhttp-coroutines` (already on every module that ships OkHttp). |
|
||||
| `TimeAgoFormatter` (`timeAgo*`, `timeAbsolute*`, `dateFormatter`, `lastSeenSentence`, `timeAgoShort`, `TimeAgoLabels`) | `commonsUI/…/ui/note/TimeAgoFormatter.kt` | `android.text.format` — see below |
|
||||
| `ToggleableTimeAgoText`, `TimeAgo`, `NormalTimeAgo`, `TimeAgoStyle`, `NowProvider`, `LocalNowSeconds` | `commonsUI/…/ui/note/elements` | followed the formatter |
|
||||
| `AmethystTheme`'s scheme, typography and providers | `commonsUI/…/ui/theme/AmethystTheme.kt` (`AmethystMaterialTheme`, `amethystDark/LightColors`, `isDarkTheme`, `previewColor`, `toFontFamily`) | The app's `AmethystTheme` resolves the prefs, calls it, then tints the system bars. The Vico chart colours stay app-side (Vico is Android-only here). |
|
||||
|
||||
**The date-format seam.** `PlatformDateFormat.kt` (commonsUI) has four expects:
|
||||
|
||||
- `DateSkeletonFormatter(skeleton)`:
|
||||
- Android: `getBestDateTimePattern` in a `ThreadLocal` `SimpleDateFormat`, exactly the old code.
|
||||
- JVM: `DateTimeFormatter.ofLocalizedPattern` (JDK 19+, the same CLDR skeleton lookup), cached per locale and zone.
|
||||
- iOS: `NSDateFormatter.setLocalizedDateFormatFromTemplate`.
|
||||
- `calendarYearAndDay`: `Calendar` on jvmAndroid, `NSCalendar` on iOS.
|
||||
- `rememberTimeOfDayFormatter`:
|
||||
- Android: `DateFormat.getTimeFormat(context)` per call, as before, so the system 12/24-hour setting is still followed.
|
||||
- JVM: the locale's SHORT time.
|
||||
- iOS: `NSDateFormatterShortStyle`.
|
||||
- `relativeTimeSpanShort`:
|
||||
- Android: `DateUtils`, as before.
|
||||
- Elsewhere: the compact "5m" form.
|
||||
|
||||
`DateSkeletonFormatterTest` pins the JVM side: en-US vs en-GB order from the same instance after a locale switch, the three skeletons, day/year boundaries, and the same-day branch of `timeAbsoluteWith`.
|
||||
|
||||
Desktop still has its own `ToggleableTimeAgoText` and the older `commons/…/util/TimeAgoFormatter.kt` (hard-coded English units, `DateFormat.MEDIUM`). Merging those onto this one is the Desktop phase.
|
||||
|
||||
### What is left in `amethyst/ui`, measured (2026-09-27)
|
||||
|
||||
A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after this round:
|
||||
|
||||
- **80 files have no blocker left.**
|
||||
- Many are headless and belong in `commons`, not `commonsUI`: the filter assemblers and
|
||||
`*LastRead`, `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`,
|
||||
and the Tor status/dialog VM.
|
||||
- The Compose ones include the chat bubble set (`ChatBubbleLayout`, `ChatGroupPosition`,
|
||||
`JumboEmoji`, `NewDateOrSubjectDivisor`, `AutoScrollToNewest`). It moved to `commonsUI`
|
||||
in the follow-up below. Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of it.
|
||||
- **`AccountViewModel` is the wall:**
|
||||
- 892 files touch it, and 310 touch nothing else app-side. Yet swapping it for an interface frees only 76 files by itself, because the rest call hub composables that are blocked themselves.
|
||||
- The ui files use 210 distinct members of it. The note renderers use 28.
|
||||
- Only ~10 note renderers become movable with a context interface alone.
|
||||
- **The real levers are about ten hub composables**, each blocking the ui files that call it:
|
||||
|
||||
| Hub | Files blocked |
|
||||
|---|---|
|
||||
| `UserProfilePicture` | 123 |
|
||||
| `RouteMaker` | 89 |
|
||||
| `UsernameDisplay` | 77 |
|
||||
| `Loaders` | 66 |
|
||||
| `DisappearingScaffold` | 64 (AVM only, 53 lines) |
|
||||
| `RichTextViewer` | 62 |
|
||||
| `NoteCompose` | 58 |
|
||||
| `FeedContentStateView` / `FeedView` | AVM only |
|
||||
|
||||
Also on the list:
|
||||
- the `reqCommand` `observe*` helpers (176 files; they take `accountViewModel` themselves);
|
||||
- the flavour-only `TranslatableRichTextViewer` (54 files), which wants a slot or a CompositionLocal.
|
||||
- **Two corrections to the MOVE-AFTER table above:**
|
||||
- `INav`/`Route` are no longer blockers: every ui file imports the commons ones.
|
||||
- `ui/note/types` is 111 files, not ~89, and "AVM threading" understates it: the hubs matter more than the parameter.
|
||||
|
||||
### Follow-up in the same round: the chat bubble group, and the audit
|
||||
|
||||
- **Moved to `commonsUI/…/commons/chats/ui`**, beside `ChatDivisor` and
|
||||
`UserDisplayNameLayout`: `ChatBubbleLayout`, `ChatGroupPosition`, `JumboEmoji`,
|
||||
`NewDateOrSubjectDivisor` and `AutoScrollToNewest`. `AutoScrollToNewest` and
|
||||
`CHAT_GROUP_WINDOW_SECONDS` went from `internal` to public so the app can reach them.
|
||||
Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of this one (no group
|
||||
position, jumbo emoji, swipe-to-reply or reaction row). Replacing it is the Desktop
|
||||
phase.
|
||||
- **Audit follow-ups:**
|
||||
- `PdfFetcher` takes the disk cache as a provider, read on the IO dispatcher, so a PDF
|
||||
card composing on a cold start doesn't build the app's lazy cache on the main thread.
|
||||
- The two DM lists resolve `TimeAgoLabels` once per list instead of once per row.
|
||||
- The iOS `DateSkeletonFormatter` rebuilds on a locale change, as Android's does. (The
|
||||
JVM one also keys on the time zone; iOS caches its system zone until reset, so keying on
|
||||
it there would cost lookups and still not notice.)
|
||||
- `ScreenLayoutTest` pins the inclusive 600dp boundary.
|
||||
- Two stale KDoc links and a same-package import are fixed.
|
||||
- The generated baseline profile has its stable-name entries repointed:
|
||||
`NowProviderKt`, and the five theme functions now in `AmethystThemeKt`. Its R8
|
||||
lambda entries were already stale before this round (it still lists
|
||||
`MarkDownStyleOnDark` under `ThemeKt`), and the new theme root
|
||||
(`AmethystThemeKt;->AmethystMaterialTheme`, `isDarkTheme`) has no entries at all, so
|
||||
**regenerate the profile** after these moves.
|
||||
|
||||
### Second audit, and one Desktop decision
|
||||
|
||||
- **Fixed:**
|
||||
- `PdfPreviewCard` closed its cache snapshot on the main thread, after `withContext(IO)`
|
||||
returned. That was older than this branch. Closing takes the same global DiskLruCache
|
||||
lock as opening, so `PdfFetcher.useSnapshot` now fetches, runs the block and closes, all
|
||||
in one IO block, leaving no suspension point for cancellation to leak through.
|
||||
- The PDF viewer's `onDispose` now closes its handle on the app IO scope, under the
|
||||
render mutex.
|
||||
- JVM and iOS built a time-of-day formatter per feed item; they now share one cached
|
||||
instance. `NSDateFormatter` is costly to build.
|
||||
- `timeAgoShort` no longer allocates an unused fallback lambda per tick on Android.
|
||||
- The iOS skeleton formatter keys on the locale only, like Android. iOS caches its system
|
||||
zone until reset, so keying on the zone cost lookups and still never saw a change.
|
||||
- New tests: `JumboEmojiTest` (counts, ZWJ/skin tone/flag/keycap sequences, bubble
|
||||
shapes). `ScreenLayoutTest` moved to commonsUI jvmTest, beside the code it tests.
|
||||
- **Desktop needs `jdk.localedata` before it uses these formatters.**
|
||||
- `desktopApp/build.gradle.kts` `nativeDistributions.modules(...)` doesn't include it,
|
||||
so the packaged runtime carries only en/root CLDR data.
|
||||
- `DateTimeFormatter.ofLocalizedPattern` then gives en-GB `Jan 5, 2024` and de-DE
|
||||
`2024 Jan 5`, and the JDK tests (full runtime) can't see it.
|
||||
- Desktop's existing `java.time` formatting (`DesktopScheduleAtPicker`) already has the
|
||||
same gap.
|
||||
- Adding the module costs about +28 MB to the unpacked runtime. That is a packaging
|
||||
call, left for the Desktop phase.
|
||||
|
||||
+7
@@ -50,3 +50,10 @@ fun prettyMime(mime: String): String =
|
||||
"application/webbundle" -> "Web Bundle"
|
||||
else -> mime
|
||||
}
|
||||
|
||||
/** The last path segment of [url], without query or fragment; the whole [url] when that is blank. */
|
||||
fun extractFilename(url: String): String {
|
||||
val afterQuery = url.substringBefore('?').substringBefore('#')
|
||||
val name = afterQuery.substringAfterLast('/', afterQuery)
|
||||
return if (name.isBlank()) url else name
|
||||
}
|
||||
|
||||
@@ -160,6 +160,8 @@ kotlin {
|
||||
|
||||
// OkHttp for the Blossom read-auth Coil fetcher.
|
||||
implementation(libs.okhttp)
|
||||
// executeAsync for PdfFetcher; already on every module that ships OkHttp.
|
||||
implementation(libs.okhttpCoroutines)
|
||||
|
||||
// Markdown rendering (richtext-commonmark). The single
|
||||
// consumer (RenderMarkdown.kt) lives in jvmAndroid.
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Per-thread cached [SimpleDateFormat] keyed off the current default [Locale].
|
||||
*
|
||||
* `SimpleDateFormat` is mutable and not thread-safe, and these formatters are read from both
|
||||
* the UI thread (composition) and background coroutines. `ThreadLocal` gives each thread its
|
||||
* own instance: no locks, no allocation per call, and a lazy rebuild on locale change.
|
||||
*/
|
||||
actual class DateSkeletonFormatter actual constructor(
|
||||
private val skeleton: String,
|
||||
) {
|
||||
private val cache = ThreadLocal<Pair<Locale, SimpleDateFormat>>()
|
||||
|
||||
private fun get(): SimpleDateFormat {
|
||||
val current = Locale.getDefault()
|
||||
val cached = cache.get()
|
||||
if (cached != null && cached.first == current) return cached.second
|
||||
val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current)
|
||||
cache.set(current to fresh)
|
||||
return fresh
|
||||
}
|
||||
|
||||
actual fun format(epochMillis: Long): String = get().format(epochMillis)
|
||||
}
|
||||
|
||||
/** Built per call, as before the move: the system 12/24-hour setting can change under a running app. */
|
||||
@Composable
|
||||
actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String {
|
||||
val context = LocalContext.current
|
||||
return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } }
|
||||
}
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
nowLabel: String,
|
||||
): String? {
|
||||
val humanReadable =
|
||||
DateUtils
|
||||
.getRelativeTimeSpanString(
|
||||
epochMillis,
|
||||
nowMillis,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_ALL,
|
||||
).toString()
|
||||
return if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) nowLabel else humanReadable
|
||||
}
|
||||
+2
-2
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -46,7 +46,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
* message they just wrote somewhere off-screen below.
|
||||
*/
|
||||
@Composable
|
||||
internal fun AutoScrollToNewest(
|
||||
fun AutoScrollToNewest(
|
||||
listState: LazyListState,
|
||||
newest: Any?,
|
||||
mine: Boolean = false,
|
||||
+1
-2
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animate
|
||||
@@ -73,7 +73,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.UserDisplayNameLayout
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.CommentIcon
|
||||
+3
-3
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.dateFormatter
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeBottom
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeMiddle
|
||||
@@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemBottom
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemMiddle
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemTop
|
||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
@@ -88,7 +88,7 @@ fun chatBubbleShapeFor(
|
||||
}
|
||||
|
||||
/** Messages more than this far apart never group, even from the same author. */
|
||||
internal const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L
|
||||
const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L
|
||||
|
||||
/**
|
||||
* Event kinds that don't render as regular bubbles (zaps, raids, clips) or that
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
+2
-3
@@ -18,17 +18,16 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatDivisor
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.never
|
||||
import com.vitorpamplona.amethyst.commons.resources.today
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.dateFormatter
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.note.dateFormatter
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
|
||||
@Composable
|
||||
+7
-3
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -45,8 +45,6 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceIn
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.linc.audiowaveform.model.AmplitudeType
|
||||
import com.linc.audiowaveform.model.WaveformAlignment
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -204,3 +202,9 @@ internal fun Iterable<Float>.normalize(
|
||||
}
|
||||
|
||||
private fun Int.safeDiv(value: Int): Float = if (value == 0) 0F else this / value.toFloat()
|
||||
|
||||
/** Where each bar sits in the row. Same values as the audiowaveform library's enum, which is Android-only. */
|
||||
enum class WaveformAlignment { Top, Center, Bottom }
|
||||
|
||||
/** How a bucket of samples becomes one bar. Same values as the audiowaveform library's enum. */
|
||||
enum class AmplitudeType { Avg, Min, Max }
|
||||
+4
-4
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -44,8 +44,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
|
||||
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
|
||||
import com.vitorpamplona.amethyst.commons.util.extractFilename
|
||||
import com.vitorpamplona.amethyst.commons.util.prettyMime
|
||||
import com.vitorpamplona.amethyst.ui.components.pdf.extractFilename
|
||||
|
||||
/**
|
||||
* The renderer for a declared file that none of the media viewers can display — a webxdc app,
|
||||
@@ -91,10 +91,10 @@ fun FileAttachmentCard(
|
||||
/**
|
||||
* The icon + title + subtitle row shared by every card that stands in for a file it can't
|
||||
* render inline: this one and the PDF placeholder/skeleton in
|
||||
* [com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard].
|
||||
* the Android app's `PdfPreviewCard`.
|
||||
*/
|
||||
@Composable
|
||||
internal fun FileAttachmentRow(
|
||||
fun FileAttachmentRow(
|
||||
symbol: MaterialSymbol,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
// Chrome shared by the full-screen media viewers and the carousels inside them: how the
|
||||
// controls auto-hide, and how far they sit from a screen edge. The platform half (hiding
|
||||
// the OS bars) stays with each front end's viewer.
|
||||
|
||||
// Opening churn -- insets arriving, then the bars being hidden -- must not look like a user
|
||||
// gesture, so the row snaps through it and only animates afterwards.
|
||||
private const val CONTROLS_SETTLE_BEFORE_ANIMATING_MS = 350L
|
||||
|
||||
// Roughly the system bars' own show/hide duration, so the row travels with them rather than
|
||||
// trailing after they have already arrived.
|
||||
private const val CONTROLS_SLIDE_MS = 200
|
||||
|
||||
// Keeps the row off the screen edge -- and off the rounded corners -- while the bars are hidden.
|
||||
private val VIEWER_CHROME_EDGE_GAP = 16.dp
|
||||
|
||||
// How long the controls stay up before the viewer fades them out on its own.
|
||||
private const val CONTROLS_AUTO_HIDE_DELAY_MS = 2000L
|
||||
|
||||
/**
|
||||
* Visibility of the viewer controls: they start on screen, fade out on their own after
|
||||
* [CONTROLS_AUTO_HIDE_DELAY_MS], and the caller flips the returned state on tap.
|
||||
*
|
||||
* [holdOpen] freezes the timer while something anchored to the controls -- the share sheet, say --
|
||||
* is up, and re-arms it once that closes. [armed] withholds the countdown until there is something
|
||||
* to look at, so a viewer that spends three seconds fetching its media doesn't reveal the first
|
||||
* frame with the controls already gone.
|
||||
*
|
||||
* A tap that brings the controls back deliberately gets no timer: the user asked for them, so they
|
||||
* stay until tapped away. That is why the countdown races the controls going away rather than just
|
||||
* sleeping -- a timer left over from an earlier show would otherwise wipe controls the user tapped
|
||||
* back up in the meantime.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberViewerControlsVisibility(
|
||||
holdOpen: Boolean,
|
||||
armed: Boolean = true,
|
||||
): MutableState<Boolean> {
|
||||
val visible = remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(armed, holdOpen) {
|
||||
if (!armed || holdOpen) return@LaunchedEffect
|
||||
|
||||
val hiddenFirst =
|
||||
withTimeoutOrNull(CONTROLS_AUTO_HIDE_DELAY_MS) {
|
||||
snapshotFlow { visible.value }.first { !it }
|
||||
}
|
||||
|
||||
if (hiddenFirst == null) visible.value = false
|
||||
}
|
||||
|
||||
return visible
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the viewer chrome sits from a screen edge: the system bar's own height while the bar is
|
||||
* on screen, and a thin constant once it is hidden -- so the chrome follows the bar instead of
|
||||
* reserving space for one that is not there.
|
||||
*
|
||||
* This only works because the viewer asks for BEHAVIOR_DEFAULT rather than transient bars. Under
|
||||
* BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE a peeked bar is painted over the content and dispatches no
|
||||
* insets at all -- `systemBars` stays 0 and `isVisible` stays false the whole time it is on screen
|
||||
* -- so nothing here could react to it.
|
||||
*
|
||||
* The value is animated, but snapped for [CONTROLS_SETTLE_BEFORE_ANIMATING_MS] after the chrome
|
||||
* appears. Opening moves the inset twice for reasons the user did not cause: the window has not
|
||||
* been told its insets yet (they read 0), and ImmersiveSystemBarsEffect hides the bars from a
|
||||
* DisposableEffect that runs after composition. Animating either would play a slide on open.
|
||||
*/
|
||||
@Composable
|
||||
fun animatedViewerChromeInset(atBottom: Boolean): Dp {
|
||||
val density = LocalDensity.current
|
||||
val bars = WindowInsets.systemBars
|
||||
val barPx = if (atBottom) bars.getBottom(density) else bars.getTop(density)
|
||||
val target = with(density) { maxOf(barPx, VIEWER_CHROME_EDGE_GAP.roundToPx()).toDp() }
|
||||
|
||||
var settled by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(CONTROLS_SETTLE_BEFORE_ANIMATING_MS)
|
||||
settled = true
|
||||
}
|
||||
|
||||
val animated by animateDpAsState(
|
||||
targetValue = target,
|
||||
animationSpec = if (settled) tween(durationMillis = CONTROLS_SLIDE_MS) else snap(),
|
||||
label = "viewerChromeInset",
|
||||
)
|
||||
return animated
|
||||
}
|
||||
+121
@@ -20,8 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.layouts
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** How the app shell presents its top-level navigation for the current window size. */
|
||||
enum class NavigationStyle {
|
||||
@@ -60,3 +70,114 @@ data class ScreenLayoutSpec(
|
||||
}
|
||||
|
||||
val LocalScreenLayout = compositionLocalOf { ScreenLayoutSpec.Phone }
|
||||
|
||||
/**
|
||||
* Material 3's window width classes: Compact below 600dp, Medium below 840dp, Expanded from
|
||||
* there. Inlined rather than taken from `material3-window-size-class`, which only the Android
|
||||
* app depends on.
|
||||
*/
|
||||
private const val MEDIUM_WIDTH_DP = 600
|
||||
private const val EXPANDED_WIDTH_DP = 840
|
||||
|
||||
/**
|
||||
* Minimum window width for the docked notification panel: a leading navigation pane, a
|
||||
* readable center pane and the panel ([NotificationPanelWidth]) only coexist comfortably from
|
||||
* a landscape-tablet-sized window up. Sized against the widest leading pane, the permanent
|
||||
* drawer ([PermanentDrawerWidth]); the rail is narrower, so a railed window that clears this
|
||||
* gets a roomier center pane rather than a tighter one.
|
||||
*/
|
||||
private const val NOTIFICATION_PANEL_MIN_WINDOW_DP = 1200
|
||||
|
||||
val PermanentDrawerWidth = 300.dp
|
||||
|
||||
val NotificationPanelWidth = 360.dp
|
||||
|
||||
/**
|
||||
* Maximum width of a screen's content column inside a wide center pane. Every NavHost
|
||||
* destination is wrapped in [CappedScreenContent] (via the builders in NavigationEffects),
|
||||
* so the whole screen — top bar, tabs, feed, settings rows — shares one centered reading
|
||||
* column instead of stretching across the pane. Screens that genuinely need the full pane
|
||||
* (Messages' two-pane split, the embedded browser surfaces) opt out at registration.
|
||||
*/
|
||||
val FeedContentMaxWidth = 600.dp
|
||||
|
||||
/**
|
||||
* Minimum window height for the docked drawer. Higher than Material's 480dp Compact/Medium
|
||||
* height boundary on purpose: the permanent drawer's own header — banner, avatar, status
|
||||
* editor, follower counts — fills most of a ~540dp column before the first navigation row, so
|
||||
* below this the rail shows more of the menu than the dock does.
|
||||
*/
|
||||
private const val DOCK_MIN_WINDOW_HEIGHT_DP = 600
|
||||
|
||||
/**
|
||||
* The navigation tier for a window of this shape.
|
||||
*
|
||||
* The dock is not a width decision. A tablet is past the Expanded breakpoint in both
|
||||
* orientations, so keying on width alone pins 300dp of menu open in portrait with no closed
|
||||
* state to fall back on (issue #4024). It docks only when the window is wide, landscape, and
|
||||
* tall enough for the drawer's own content to be usable; everything else that is not Compact
|
||||
* falls through to the rail, which pairs with the existing swipe-in modal drawer.
|
||||
*
|
||||
* A square window counts as landscape and docks; `Configuration.ORIENTATION_LANDSCAPE`
|
||||
* breaks that tie the other way, so the two disagree at exactly width == height.
|
||||
*/
|
||||
fun decideNavigationStyle(
|
||||
windowWidthDp: Int,
|
||||
windowHeightDp: Int,
|
||||
): NavigationStyle =
|
||||
when {
|
||||
windowWidthDp >= EXPANDED_WIDTH_DP &&
|
||||
windowWidthDp >= windowHeightDp &&
|
||||
windowHeightDp >= DOCK_MIN_WINDOW_HEIGHT_DP -> NavigationStyle.PERMANENT_DRAWER
|
||||
windowWidthDp >= MEDIUM_WIDTH_DP -> NavigationStyle.NAV_RAIL
|
||||
else -> NavigationStyle.BOTTOM_BAR
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the window is wide enough to dock the notification feed beside the content.
|
||||
*
|
||||
* Deliberately not keyed on [NavigationStyle]: a wide portrait window now gets the rail, and
|
||||
* gating on the dock would strip a panel it has today.
|
||||
*/
|
||||
fun hasRoomForNotificationPanel(windowWidthDp: Int): Boolean = windowWidthDp >= NOTIFICATION_PANEL_MIN_WINDOW_DP
|
||||
|
||||
/**
|
||||
* Centers a destination's content at [FeedContentMaxWidth]. The outer box paints the theme
|
||||
* background so the gutters match the screens' own surfaces; on Compact windows the cap is
|
||||
* wider than the pane and this is a visual no-op.
|
||||
*/
|
||||
@Composable
|
||||
fun CappedScreenContent(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.widthIn(max = FeedContentMaxWidth)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The layout tier for a window of [windowWidthDp] x [windowHeightDp]. Each front end reads
|
||||
* its own window size (Android: `Configuration.screenWidthDp`) and asks here, so the tier
|
||||
* rule lives in one place.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberScreenLayoutSpec(
|
||||
windowWidthDp: Int,
|
||||
windowHeightDp: Int,
|
||||
): ScreenLayoutSpec =
|
||||
remember(windowWidthDp, windowHeightDp) {
|
||||
ScreenLayoutSpec(
|
||||
navigationStyle = decideNavigationStyle(windowWidthDp, windowHeightDp),
|
||||
hasRoomForNotificationPanel = hasRoomForNotificationPanel(windowWidthDp),
|
||||
)
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/**
|
||||
* Formats instants with a Unicode LDML date skeleton ("yMMMd", "MMMd", "yMMM"), letting the
|
||||
* platform pick the default locale's field order ("MMM d, y" in en-US, "d MMM y" in en-GB).
|
||||
* Safe to call from any thread. Rebuilds itself when the default locale changes (the JVM actual
|
||||
* also on a time-zone change; Android and iOS keep the zone they were built with).
|
||||
*/
|
||||
expect class DateSkeletonFormatter(
|
||||
skeleton: String,
|
||||
) {
|
||||
fun format(epochMillis: Long): String
|
||||
}
|
||||
|
||||
/** The instant's calendar year and day-of-year in the default time zone, as year * 1000 + day. */
|
||||
expect fun calendarYearAndDay(epochMillis: Long): Int
|
||||
|
||||
/**
|
||||
* A time-of-day formatter ("14:32" / "2:32 PM"). Android and iOS follow the system 12/24-hour
|
||||
* setting; the JVM follows the locale. Identity-stable, so it can key a `remember`, and cheap to
|
||||
* call for every item in a feed: platforms whose formatter is costly share one cached instance.
|
||||
*/
|
||||
@Composable
|
||||
expect fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String
|
||||
|
||||
/**
|
||||
* The platform's abbreviated relative span ("5 min. ago", "Yesterday"), [nowLabel] for an
|
||||
* instant that rounds to now or lies ahead, or null where the platform has no such formatter.
|
||||
*/
|
||||
expect fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
nowLabel: String,
|
||||
): String?
|
||||
+42
-84
@@ -18,11 +18,8 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -46,52 +43,23 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.round
|
||||
|
||||
// Skeletons follow Unicode LDML — DateFormat.getBestDateTimePattern picks the
|
||||
// correct locale-specific ordering (e.g. "MMM d, y" in en-US vs "d MMM y" in en-GB).
|
||||
// Skeletons follow Unicode LDML: the platform picks the locale-specific ordering
|
||||
// (e.g. "MMM d, y" in en-US vs "d MMM y" in en-GB). See [DateSkeletonFormatter].
|
||||
private const val YEAR_SKELETON = "yMMMd"
|
||||
private const val MONTH_SKELETON = "MMMd"
|
||||
private const val YEAR_NO_DAY_SKELETON = "yMMM"
|
||||
|
||||
/**
|
||||
* Per-thread cached [SimpleDateFormat] keyed off the current default [Locale].
|
||||
*
|
||||
* `SimpleDateFormat` is mutable and not thread-safe, and these formatters are
|
||||
* read from both the UI thread (composition) and background coroutines
|
||||
* (e.g. `LocalCache.justVerify` logging failed event verifications). A bare
|
||||
* `var` shared across threads would race on the formatter's internal Calendar.
|
||||
* Using `ThreadLocal` gives each thread its own instance — no locks, no
|
||||
* allocation per call, and we rebuild lazily on locale change.
|
||||
*/
|
||||
private class LocaleAwareFormatter(
|
||||
private val skeleton: String,
|
||||
) {
|
||||
private val cache = ThreadLocal<Pair<Locale, SimpleDateFormat>>()
|
||||
|
||||
fun get(): SimpleDateFormat {
|
||||
val current = Locale.getDefault()
|
||||
val cached = cache.get()
|
||||
if (cached != null && cached.first == current) return cached.second
|
||||
val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current)
|
||||
cache.set(current to fresh)
|
||||
return fresh
|
||||
}
|
||||
}
|
||||
|
||||
private val yearFormatter = LocaleAwareFormatter(YEAR_SKELETON)
|
||||
private val monthFormatter = LocaleAwareFormatter(MONTH_SKELETON)
|
||||
private val yearNoDayFormatter = LocaleAwareFormatter(YEAR_NO_DAY_SKELETON)
|
||||
private val yearFormatter = DateSkeletonFormatter(YEAR_SKELETON)
|
||||
private val monthFormatter = DateSkeletonFormatter(MONTH_SKELETON)
|
||||
private val yearNoDayFormatter = DateSkeletonFormatter(YEAR_NO_DAY_SKELETON)
|
||||
|
||||
/**
|
||||
* The handful of unit labels the relative formatters splice into their output.
|
||||
*
|
||||
* Resolved once in composition so the formatters themselves can stay ordinary
|
||||
* functions. [com.vitorpamplona.amethyst.ui.note.elements.TimeAgo] builds its text
|
||||
* functions. [com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo] builds its text
|
||||
* inside a `derivedStateOf`, which is not a composable scope, so a @Composable
|
||||
* formatter could not be called from there at all.
|
||||
*/
|
||||
@@ -133,8 +101,8 @@ fun timeAgoWith(
|
||||
val timeDifference = TimeUtils.now() - time
|
||||
|
||||
return when {
|
||||
timeDifference > TimeUtils.ONE_YEAR -> prefix + yearFormatter.get().format(time * 1000)
|
||||
timeDifference > TimeUtils.ONE_MONTH -> prefix + monthFormatter.get().format(time * 1000)
|
||||
timeDifference > TimeUtils.ONE_YEAR -> prefix + yearFormatter.format(time * 1000)
|
||||
timeDifference > TimeUtils.ONE_MONTH -> prefix + monthFormatter.format(time * 1000)
|
||||
timeDifference > TimeUtils.ONE_DAY -> prefix + (timeDifference / TimeUtils.ONE_DAY).toString() + labels.days
|
||||
timeDifference > TimeUtils.ONE_HOUR -> prefix + (timeDifference / TimeUtils.ONE_HOUR).toString() + labels.hours
|
||||
timeDifference > TimeUtils.ONE_MINUTE -> prefix + (timeDifference / TimeUtils.ONE_MINUTE).toString() + labels.minutes
|
||||
@@ -142,10 +110,13 @@ fun timeAgoWith(
|
||||
}
|
||||
}
|
||||
|
||||
/** Plain-function core of [timeAbsolute], callable outside composition. */
|
||||
/**
|
||||
* Plain-function core of [timeAbsolute], callable outside composition. [timeOfDay] comes
|
||||
* from [rememberTimeOfDayFormatter].
|
||||
*/
|
||||
fun timeAbsoluteWith(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
timeOfDay: (epochMillis: Long) -> String,
|
||||
never: String,
|
||||
prefix: String = " • ",
|
||||
): String {
|
||||
@@ -153,43 +124,39 @@ fun timeAbsoluteWith(
|
||||
if (time == 0L) return prefix + never
|
||||
|
||||
val timeMs = time * 1000
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().apply { timeInMillis = timeMs }
|
||||
val now = calendarYearAndDay(TimeUtils.nowMillis())
|
||||
val then = calendarYearAndDay(timeMs)
|
||||
|
||||
val sameYear = now.get(Calendar.YEAR) == then.get(Calendar.YEAR)
|
||||
val sameDay = sameYear && now.get(Calendar.DAY_OF_YEAR) == then.get(Calendar.DAY_OF_YEAR)
|
||||
val sameYear = now / 1000 == then / 1000
|
||||
val sameDay = now == then
|
||||
|
||||
val timeOfDay = DateFormat.getTimeFormat(context).format(Date(timeMs))
|
||||
val timeOfDay = timeOfDay(timeMs)
|
||||
|
||||
return when {
|
||||
sameDay -> prefix + timeOfDay
|
||||
sameYear -> prefix + monthFormatter.get().format(timeMs) + ", " + timeOfDay
|
||||
else -> prefix + yearFormatter.get().format(timeMs)
|
||||
sameYear -> prefix + monthFormatter.format(timeMs) + ", " + timeOfDay
|
||||
else -> prefix + yearFormatter.format(timeMs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a Unix timestamp (seconds) as an absolute date/time string, picking the
|
||||
* granularity from how far away the timestamp is:
|
||||
* - same day → time only (locale + system 12/24-hr aware via [DateFormat.getTimeFormat])
|
||||
* - same day → time only (locale + system 12/24-hr aware via [rememberTimeOfDayFormatter])
|
||||
* - same year → "Jan 5, 14:32" / "5 Jan 14:32" / "Jan 5, 2:32 PM" (locale + system aware)
|
||||
* - older → "Jan 5, 2024" / "5 Jan 2024" (locale aware)
|
||||
*
|
||||
* Used by [com.vitorpamplona.amethyst.ui.note.elements.TimeAgo] when the user
|
||||
* Used by [com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo] when the user
|
||||
* taps the relative timestamp to reveal the absolute one.
|
||||
*/
|
||||
@Composable
|
||||
fun timeAbsolute(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
prefix: String = " • ",
|
||||
): String = timeAbsoluteWith(time, context, stringRes(Res.string.never), prefix)
|
||||
): String = timeAbsoluteWith(time, rememberTimeOfDayFormatter(), stringRes(Res.string.never), prefix)
|
||||
|
||||
@Composable
|
||||
fun timeAbsoluteNoDot(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
): String = timeAbsolute(time, context, prefix = "")
|
||||
fun timeAbsoluteNoDot(time: Long?): String = timeAbsolute(time, prefix = "")
|
||||
|
||||
@Composable
|
||||
fun timeAgo(
|
||||
@@ -210,11 +177,11 @@ fun timeAgoNoDot(time: Long?): String {
|
||||
|
||||
return when {
|
||||
timeDifference > TimeUtils.ONE_YEAR -> {
|
||||
yearFormatter.get().format(time * 1000)
|
||||
yearFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_MONTH -> {
|
||||
monthFormatter.get().format(time * 1000)
|
||||
monthFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_DAY -> {
|
||||
@@ -244,11 +211,11 @@ fun timeAgoNoDotNoDay(time: Long?): String {
|
||||
|
||||
return when {
|
||||
timeDifference > TimeUtils.ONE_YEAR -> {
|
||||
yearNoDayFormatter.get().format(time * 1000)
|
||||
yearNoDayFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_MONTH -> {
|
||||
monthFormatter.get().format(time * 1000)
|
||||
monthFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_DAY -> {
|
||||
@@ -278,11 +245,11 @@ fun timeAheadNoDot(time: Long?): String {
|
||||
|
||||
return when {
|
||||
timeDifference > TimeUtils.ONE_YEAR -> {
|
||||
yearFormatter.get().format(time * 1000)
|
||||
yearFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_MONTH -> {
|
||||
monthFormatter.get().format(time * 1000)
|
||||
monthFormatter.format(time * 1000)
|
||||
}
|
||||
|
||||
timeDifference > TimeUtils.ONE_DAY -> {
|
||||
@@ -314,9 +281,9 @@ fun dateFormatter(
|
||||
val timeDifference = TimeUtils.now() - time
|
||||
|
||||
return if (timeDifference > TimeUtils.ONE_YEAR) {
|
||||
yearFormatter.get().format(time * 1000)
|
||||
yearFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.ONE_DAY) {
|
||||
monthFormatter.get().format(time * 1000)
|
||||
monthFormatter.format(time * 1000)
|
||||
} else {
|
||||
today
|
||||
}
|
||||
@@ -394,28 +361,19 @@ fun lastSeenSentence(time: Long?): String {
|
||||
}
|
||||
}
|
||||
|
||||
val dateText = yearFormatter.get().format(time * 1000)
|
||||
val dateText = yearFormatter.format(time * 1000)
|
||||
|
||||
return stringRes(Res.string.last_seen_on_date, dateText, durationText)
|
||||
}
|
||||
|
||||
/**
|
||||
* "5 min. ago" style on platforms that have an abbreviated relative-span formatter (Android's
|
||||
* DateUtils), the compact relative form ("5m") elsewhere.
|
||||
*/
|
||||
fun timeAgoShort(
|
||||
mills: Long?,
|
||||
stringForNow: String,
|
||||
time: Long?,
|
||||
labels: TimeAgoLabels,
|
||||
): String {
|
||||
if (mills == null) return " "
|
||||
|
||||
var humanReadable =
|
||||
DateUtils
|
||||
.getRelativeTimeSpanString(
|
||||
mills * 1000,
|
||||
System.currentTimeMillis(),
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_ALL,
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = stringForNow
|
||||
}
|
||||
|
||||
return humanReadable
|
||||
if (time == null) return " "
|
||||
return relativeTimeSpanShortOrNull(time * 1000, TimeUtils.nowMillis(), labels.now) ?: timeAgoWith(time, labels, prefix = "")
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.elements
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
+12
-16
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.elements
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -33,18 +33,15 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.now
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAbsoluteWith
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAbsoluteWith
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoWith
|
||||
|
||||
/**
|
||||
* Which relative-time format to use when the timestamp is *not* toggled to absolute.
|
||||
@@ -94,21 +91,20 @@ fun ToggleableTimeAgoText(
|
||||
// chat timestamp opening the relay/delivery dialog) receives the tap instead.
|
||||
toggleable: Boolean = true,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val timeOfDay = rememberTimeOfDayFormatter()
|
||||
val nowState = LocalNowSeconds.current
|
||||
val nowStr = stringRes(id = Res.string.now)
|
||||
val labels = rememberTimeAgoLabels()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var showAbsolute by remember(timestamp) { mutableStateOf(false) }
|
||||
|
||||
val text by
|
||||
remember(timestamp, context, style, nowState, nowStr, labels) {
|
||||
remember(timestamp, timeOfDay, style, nowState, labels) {
|
||||
derivedStateOf {
|
||||
if (showAbsolute) {
|
||||
when (style) {
|
||||
TimeAgoStyle.Dotted -> timeAbsoluteWith(timestamp, context, labels.never)
|
||||
TimeAgoStyle.DottedTight -> timeAbsoluteWith(timestamp, context, labels.never).trimStart()
|
||||
TimeAgoStyle.Short -> timeAbsoluteWith(timestamp, context, labels.never, prefix = "")
|
||||
TimeAgoStyle.Dotted -> timeAbsoluteWith(timestamp, timeOfDay, labels.never)
|
||||
TimeAgoStyle.DottedTight -> timeAbsoluteWith(timestamp, timeOfDay, labels.never).trimStart()
|
||||
TimeAgoStyle.Short -> timeAbsoluteWith(timestamp, timeOfDay, labels.never, prefix = "")
|
||||
}
|
||||
} else {
|
||||
// Read nowState only when displaying a relative time, so an item
|
||||
@@ -117,7 +113,7 @@ fun ToggleableTimeAgoText(
|
||||
when (style) {
|
||||
TimeAgoStyle.Dotted -> timeAgoWith(timestamp, labels)
|
||||
TimeAgoStyle.DottedTight -> timeAgoWith(timestamp, labels).trimStart()
|
||||
TimeAgoStyle.Short -> timeAgoShort(timestamp, nowStr)
|
||||
TimeAgoStyle.Short -> timeAgoShort(timestamp, labels)
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.Density
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons
|
||||
import com.vitorpamplona.amethyst.commons.model.AccentColorType
|
||||
import com.vitorpamplona.amethyst.commons.model.FontFamilyType
|
||||
import com.vitorpamplona.amethyst.commons.model.FontSizeType
|
||||
import com.vitorpamplona.amethyst.commons.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalAnimationsEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
|
||||
// The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color.
|
||||
// Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent
|
||||
// uses its single hue across primary and secondary for a cohesive single-color theme.
|
||||
private fun accentPrimary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color =
|
||||
when (accent) {
|
||||
AccentColorType.PURPLE -> if (dark) Purple200 else Purple500
|
||||
AccentColorType.BLUE -> if (dark) AccentBlueDark else AccentBlueLight
|
||||
AccentColorType.GREEN -> if (dark) AccentGreenDark else AccentGreenLight
|
||||
AccentColorType.ORANGE -> if (dark) AccentOrangeDark else AccentOrangeLight
|
||||
AccentColorType.RED -> if (dark) AccentRedDark else AccentRedLight
|
||||
AccentColorType.PINK -> if (dark) AccentPinkDark else AccentPinkLight
|
||||
}
|
||||
|
||||
private fun accentSecondary(
|
||||
accent: AccentColorType,
|
||||
dark: Boolean,
|
||||
): Color = if (accent == AccentColorType.PURPLE) Teal200 else accentPrimary(accent, dark)
|
||||
|
||||
// Representative colour for an accent option, used by the Settings accent-picker swatches — the
|
||||
// same primary the theme would apply for the given light/dark mode, so the swatch previews the
|
||||
// real result.
|
||||
fun AccentColorType.previewColor(dark: Boolean): Color = accentPrimary(this, dark)
|
||||
|
||||
fun amethystDarkColors(accent: AccentColorType): ColorScheme =
|
||||
amethystDarkColorScheme(
|
||||
primary = accentPrimary(accent, dark = true),
|
||||
secondary = accentSecondary(accent, dark = true),
|
||||
inversePrimary = accentPrimary(accent, dark = false),
|
||||
)
|
||||
|
||||
fun amethystLightColors(accent: AccentColorType): ColorScheme =
|
||||
amethystLightColorScheme(
|
||||
primary = accentPrimary(accent, dark = false),
|
||||
secondary = accentSecondary(accent, dark = false),
|
||||
inversePrimary = accentPrimary(accent, dark = true),
|
||||
)
|
||||
|
||||
/** Whether [prefTheme] resolves to dark, following the system for anything but an explicit choice. */
|
||||
@Composable
|
||||
fun isDarkTheme(prefTheme: ThemeType): Boolean =
|
||||
when (prefTheme) {
|
||||
ThemeType.DARK -> true
|
||||
ThemeType.LIGHT -> false
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* Amethyst's Material theme and the app-wide composition locals, for any front end: the
|
||||
* accent-derived colour scheme, the chosen font family and size, the display settings, and the
|
||||
* icon font. Platform chrome (Android's system bars) is the caller's to apply.
|
||||
*
|
||||
* [profilePictureCache] is true where the image loader registers the avatar thumbnail cache.
|
||||
*/
|
||||
@Composable
|
||||
fun AmethystMaterialTheme(
|
||||
darkTheme: Boolean,
|
||||
accentColor: AccentColorType = AccentColorType.PURPLE,
|
||||
fontFamily: FontFamilyType = FontFamilyType.SYSTEM,
|
||||
fontSize: FontSizeType = FontSizeType.NORMAL,
|
||||
displaySettings: DisplaySettings = DisplaySettings(),
|
||||
profilePictureCache: Boolean = false,
|
||||
colors: ColorScheme = remember(darkTheme, accentColor) { if (darkTheme) amethystDarkColors(accentColor) else amethystLightColors(accentColor) },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val resolvedFontFamily = remember(fontFamily) { fontFamily.toFontFamily() }
|
||||
val typography = remember(fontFamily) { Typography.withFontFamily(resolvedFontFamily) }
|
||||
|
||||
val density = LocalDensity.current
|
||||
val scaledDensity =
|
||||
remember(density, fontSize) {
|
||||
Density(density.density, density.fontScale * fontSize.scale)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
typography = typography,
|
||||
shapes = Shapes,
|
||||
content = {
|
||||
ProvideAppIcons {
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaledDensity,
|
||||
LocalProfilePictureCache provides profilePictureCache,
|
||||
LocalDisplaySettings provides displaySettings,
|
||||
// Performance mode turns decorative animations (crossfades) off app-wide.
|
||||
LocalAnimationsEnabled provides !displaySettings.performanceMode,
|
||||
LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Maps the user-selected font preference to a Compose [FontFamily].
|
||||
// SYSTEM returns null so the platform default is used unchanged.
|
||||
fun FontFamilyType.toFontFamily(): FontFamily? =
|
||||
when (this) {
|
||||
FontFamilyType.SYSTEM -> null
|
||||
FontFamilyType.SANS_SERIF -> FontFamily.SansSerif
|
||||
FontFamilyType.SERIF -> FontFamily.Serif
|
||||
FontFamilyType.MONOSPACE -> FontFamily.Monospace
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import platform.Foundation.NSCalendar
|
||||
import platform.Foundation.NSCalendarUnitDay
|
||||
import platform.Foundation.NSCalendarUnitYear
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSDateFormatter
|
||||
import platform.Foundation.NSDateFormatterNoStyle
|
||||
import platform.Foundation.NSDateFormatterShortStyle
|
||||
import platform.Foundation.NSLocale
|
||||
import platform.Foundation.currentLocale
|
||||
import platform.Foundation.dateWithTimeIntervalSince1970
|
||||
import platform.Foundation.localeIdentifier
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
private fun dateOf(epochMillis: Long) = NSDate.dateWithTimeIntervalSince1970(epochMillis / 1000.0)
|
||||
|
||||
/**
|
||||
* NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order.
|
||||
* Rebuilt when the current locale changes, as on Android. The time zone is the one in effect when
|
||||
* it was built: iOS caches the system zone until `NSTimeZone.resetSystemTimeZone()`, so keying
|
||||
* on it would cost two lookups per call and still not notice a change.
|
||||
*/
|
||||
actual class DateSkeletonFormatter actual constructor(
|
||||
private val skeleton: String,
|
||||
) {
|
||||
private class Cached(
|
||||
val key: String,
|
||||
val formatter: NSDateFormatter,
|
||||
)
|
||||
|
||||
@Volatile private var cached: Cached? = null
|
||||
|
||||
actual fun format(epochMillis: Long): String {
|
||||
val locale = NSLocale.currentLocale
|
||||
val key = locale.localeIdentifier
|
||||
val formatter =
|
||||
cached?.takeIf { it.key == key }?.formatter
|
||||
?: NSDateFormatter()
|
||||
.apply {
|
||||
this.locale = locale
|
||||
setLocalizedDateFormatFromTemplate(skeleton)
|
||||
}.also { cached = Cached(key, it) }
|
||||
return formatter.stringFromDate(dateOf(epochMillis))
|
||||
}
|
||||
}
|
||||
|
||||
actual fun calendarYearAndDay(epochMillis: Long): Int {
|
||||
val calendar = NSCalendar.currentCalendar
|
||||
val date = dateOf(epochMillis)
|
||||
val year = calendar.component(NSCalendarUnitYear, fromDate = date).toInt()
|
||||
val day = calendar.ordinalityOfUnit(NSCalendarUnitDay, inUnit = NSCalendarUnitYear, forDate = date).toInt()
|
||||
return year * 1000 + day
|
||||
}
|
||||
|
||||
// NSDateFormatter is costly to build, so every feed item shares this one instead of each
|
||||
// composing its own. It follows the system 12/24-hour setting (the short time style does).
|
||||
private val timeOfDayFormatter by lazy {
|
||||
NSDateFormatter().apply {
|
||||
dateStyle = NSDateFormatterNoStyle
|
||||
timeStyle = NSDateFormatterShortStyle
|
||||
}
|
||||
}
|
||||
|
||||
private val timeOfDay: (Long) -> String = { epochMillis -> timeOfDayFormatter.stringFromDate(dateOf(epochMillis)) }
|
||||
|
||||
@Composable
|
||||
actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
nowLabel: String,
|
||||
): String? = null
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.service.pdf
|
||||
|
||||
import coil3.disk.DiskCache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import java.io.IOException
|
||||
|
||||
object PdfFetcher {
|
||||
/**
|
||||
* Returns a snapshot of the cached PDF for [url], downloading it if necessary. The caller is
|
||||
* responsible for closing the returned snapshot; while it's open the cache entry cannot be
|
||||
* evicted, so the underlying file stays valid for `PdfRenderer`.
|
||||
*
|
||||
* Pass the app's Coil disk cache so PDFs share the same LRU eviction and disk budget as
|
||||
* images. It is a provider, read on the IO dispatcher: the app builds its cache lazily
|
||||
* (statvfs, directory setup), and a card composing on a cold start must not do that on main.
|
||||
*/
|
||||
suspend fun fetchSnapshot(
|
||||
url: String,
|
||||
diskCache: () -> DiskCache,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
): DiskCache.Snapshot = withContext(Dispatchers.IO) { openOrDownload(url, diskCache, okHttpClient) }
|
||||
|
||||
/** The fetch itself. Callers run it on the IO dispatcher. */
|
||||
private suspend fun openOrDownload(
|
||||
url: String,
|
||||
diskCache: () -> DiskCache,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
): DiskCache.Snapshot {
|
||||
val cache = diskCache()
|
||||
// Covers the cache-hit fast path too, not just the download below it. openSnapshot()
|
||||
// contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a
|
||||
// burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller
|
||||
// that happens to be on the main thread stalls the frame for that whole burst, and the
|
||||
// hit path is exactly the one a feed takes when a PDF card scrolls back into view.
|
||||
cache.openSnapshot(url)?.let { return it }
|
||||
|
||||
val editor = cache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url")
|
||||
try {
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
okHttpClient(url).newCall(request).executeAsync().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("PDF download failed: ${response.code}")
|
||||
}
|
||||
cache.fileSystem.write(editor.data) {
|
||||
val bytes = writeAll(response.body.source())
|
||||
if (bytes == 0L) throw IOException("PDF download failed: empty response body")
|
||||
}
|
||||
}
|
||||
|
||||
return editor.commitAndOpenSnapshot() ?: throw IOException("Unable to commit cache editor for $url")
|
||||
} catch (t: Throwable) {
|
||||
runCatching { editor.abort() }
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches like [fetchSnapshot], runs [block] on the snapshot, and closes it, all on the IO
|
||||
* dispatcher. Prefer this to `fetchSnapshot(...).use { }` from a main-thread caller: there the
|
||||
* close would run on main, and closing takes the same global DiskLruCache lock as opening.
|
||||
* The snapshot is created by the last non-suspending step of the fetch and handed straight
|
||||
* to `use`, inside one `withContext`, so a cancellation cannot land between the two and leak
|
||||
* it open (which returning through [fetchSnapshot]'s own `withContext` could).
|
||||
*/
|
||||
suspend fun <T> useSnapshot(
|
||||
url: String,
|
||||
diskCache: () -> DiskCache,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
block: (DiskCache.Snapshot) -> T,
|
||||
): T =
|
||||
withContext(Dispatchers.IO) {
|
||||
openOrDownload(url, diskCache, okHttpClient).use(block)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import java.util.Calendar
|
||||
|
||||
actual fun calendarYearAndDay(epochMillis: Long): Int {
|
||||
val calendar = Calendar.getInstance().apply { timeInMillis = epochMillis }
|
||||
return calendar.get(Calendar.YEAR) * 1000 + calendar.get(Calendar.DAY_OF_YEAR)
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* [DateTimeFormatter.ofLocalizedPattern] (JDK 19+) resolves the skeleton against the locale's
|
||||
* CLDR data, the JVM counterpart of Android's `getBestDateTimePattern`. The formatter is
|
||||
* immutable, so one cached instance serves every thread; it is rebuilt when the default locale
|
||||
* or time zone changes.
|
||||
*/
|
||||
actual class DateSkeletonFormatter actual constructor(
|
||||
private val skeleton: String,
|
||||
) {
|
||||
private class Cached(
|
||||
val locale: Locale,
|
||||
val zone: ZoneId,
|
||||
val formatter: DateTimeFormatter,
|
||||
)
|
||||
|
||||
@Volatile private var cached: Cached? = null
|
||||
|
||||
actual fun format(epochMillis: Long): String {
|
||||
val locale = Locale.getDefault()
|
||||
val zone = ZoneId.systemDefault()
|
||||
val current = cached?.takeIf { it.locale == locale && it.zone == zone }
|
||||
val formatter =
|
||||
current?.formatter
|
||||
?: DateTimeFormatter.ofLocalizedPattern(skeleton).withLocale(locale).withZone(zone).also {
|
||||
cached = Cached(locale, zone, it)
|
||||
}
|
||||
return formatter.format(Instant.ofEpochMilli(epochMillis))
|
||||
}
|
||||
}
|
||||
|
||||
private class CachedTimeOfDay(
|
||||
val locale: Locale,
|
||||
val zone: ZoneId,
|
||||
val formatter: DateTimeFormatter,
|
||||
)
|
||||
|
||||
@Volatile private var cachedTimeOfDay: CachedTimeOfDay? = null
|
||||
|
||||
/** One formatter for every item, rebuilt only when the locale or zone changes. */
|
||||
private val timeOfDay: (Long) -> String = { epochMillis ->
|
||||
val locale = Locale.getDefault()
|
||||
val zone = ZoneId.systemDefault()
|
||||
val formatter =
|
||||
cachedTimeOfDay?.takeIf { it.locale == locale && it.zone == zone }?.formatter
|
||||
?: DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).withZone(zone).also {
|
||||
cachedTimeOfDay = CachedTimeOfDay(locale, zone, it)
|
||||
}
|
||||
formatter.format(Instant.ofEpochMilli(epochMillis))
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
nowLabel: String,
|
||||
): String? = null
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeTop
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemBottom
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemMiddle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
// Emoji are written as escapes so the file stays unambiguous on disk (see CLAUDE.md).
|
||||
class JumboEmojiTest {
|
||||
private val grin = "\uD83D\uDE00" // U+1F600
|
||||
private val thumbsUpMedium = "\uD83D\uDC4D\uD83C\uDFFD" // U+1F44D + skin tone U+1F3FD
|
||||
private val family = "\uD83D\uDC69\u200D\uD83D\uDC67" // woman ZWJ girl: draws as one
|
||||
private val flagUs = "\uD83C\uDDFA\uD83C\uDDF8" // two regional indicators
|
||||
private val keycapOne = "1\uFE0F\u20E3" // '1' + VS16 + combining keycap
|
||||
private val redHeart = "\u2764\uFE0F" // U+2764 + VS16
|
||||
|
||||
@Test
|
||||
fun countsUpToThreeEmoji() {
|
||||
assertEquals(1, jumboEmojiCount(grin))
|
||||
assertEquals(2, jumboEmojiCount(grin + redHeart))
|
||||
assertEquals(3, jumboEmojiCount(grin + grin + grin))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun moreThanThreeIsNotJumbo() = assertEquals(0, jumboEmojiCount(grin + grin + grin + grin))
|
||||
|
||||
@Test
|
||||
fun anyTextMakesItNotJumbo() {
|
||||
assertEquals(0, jumboEmojiCount("hi $grin"))
|
||||
assertEquals(0, jumboEmojiCount("${grin}a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whitespaceIsIgnored() = assertEquals(2, jumboEmojiCount(" $grin \n $grin "))
|
||||
|
||||
@Test
|
||||
fun emptyOrBlankIsZero() {
|
||||
assertEquals(0, jumboEmojiCount(""))
|
||||
assertEquals(0, jumboEmojiCount(" "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun modifiersAndZwjSequencesCountAsOne() {
|
||||
assertEquals(1, jumboEmojiCount(thumbsUpMedium))
|
||||
assertEquals(1, jumboEmojiCount(family))
|
||||
assertEquals(1, jumboEmojiCount(redHeart))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun flagsNeedBothRegionalIndicators() {
|
||||
assertEquals(1, jumboEmojiCount(flagUs))
|
||||
assertEquals(0, jumboEmojiCount("\uD83C\uDDFA")) // half a flag
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keycapsNeedTheirMark() {
|
||||
assertEquals(1, jumboEmojiCount(keycapOne))
|
||||
assertEquals(0, jumboEmojiCount("1")) // a bare digit is text
|
||||
assertEquals(0, jumboEmojiCount("1 $grin"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bubbleShapeFollowsSideAndPosition() {
|
||||
assertSame(ChatBubbleShapeMe, chatBubbleShapeFor(true, ChatGroupPosition.SINGLE))
|
||||
assertSame(ChatBubbleShapeMeTop, chatBubbleShapeFor(true, ChatGroupPosition.TOP))
|
||||
assertSame(ChatBubbleShapeThemMiddle, chatBubbleShapeFor(false, ChatGroupPosition.MIDDLE))
|
||||
assertSame(ChatBubbleShapeThemBottom, chatBubbleShapeFor(false, ChatGroupPosition.BOTTOM))
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -18,9 +18,8 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.layouts
|
||||
package com.vitorpamplona.amethyst.commons.ui.layouts
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -86,6 +85,10 @@ class ScreenLayoutTest {
|
||||
@Test
|
||||
fun compactWidthKeepsTheBottomBar() = assertStyle(NavigationStyle.BOTTOM_BAR, 599, 900)
|
||||
|
||||
// The Medium breakpoint is inclusive: exactly 600dp is no longer Compact.
|
||||
@Test
|
||||
fun exactlyAtTheMediumBreakpointRails() = assertStyle(NavigationStyle.NAV_RAIL, 600, 900)
|
||||
|
||||
// ---- Notification panel ----
|
||||
|
||||
@Test
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class DateSkeletonFormatterTest {
|
||||
private lateinit var savedLocale: Locale
|
||||
private lateinit var savedZone: TimeZone
|
||||
|
||||
@Before
|
||||
fun pin() {
|
||||
savedLocale = Locale.getDefault()
|
||||
savedZone = TimeZone.getDefault()
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@After
|
||||
fun restore() {
|
||||
Locale.setDefault(savedLocale)
|
||||
TimeZone.setDefault(savedZone)
|
||||
}
|
||||
|
||||
private fun millis(
|
||||
year: Int,
|
||||
month: Int,
|
||||
day: Int,
|
||||
hour: Int = 12,
|
||||
) = LocalDateTime
|
||||
.of(year, month, day, hour, 0)
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
|
||||
@Test
|
||||
fun skeletonFollowsTheLocalesFieldOrder() {
|
||||
val formatter = DateSkeletonFormatter("yMMMd")
|
||||
Locale.setDefault(Locale.US)
|
||||
assertEquals("Jan 5, 2024", formatter.format(millis(2024, 1, 5)))
|
||||
// Same instance, new locale: the cache must not hand back the US pattern.
|
||||
Locale.setDefault(Locale.UK)
|
||||
assertEquals("5 Jan 2024", formatter.format(millis(2024, 1, 5)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun monthDayAndYearMonthSkeletons() {
|
||||
Locale.setDefault(Locale.US)
|
||||
assertEquals("Jan 5", DateSkeletonFormatter("MMMd").format(millis(2024, 1, 5)))
|
||||
assertEquals("Jan 2024", DateSkeletonFormatter("yMMM").format(millis(2024, 1, 5)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calendarDayDistinguishesDaysAndYears() {
|
||||
assertEquals(calendarYearAndDay(millis(2024, 3, 1, hour = 1)), calendarYearAndDay(millis(2024, 3, 1, hour = 23)))
|
||||
assertTrue(calendarYearAndDay(millis(2024, 3, 1)) != calendarYearAndDay(millis(2024, 3, 2)))
|
||||
assertEquals(2024, calendarYearAndDay(millis(2024, 12, 31)) / 1000)
|
||||
assertEquals(366, calendarYearAndDay(millis(2024, 12, 31)) % 1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun absoluteTimeUsesTimeOfDayOnlyForToday() {
|
||||
Locale.setDefault(Locale.US)
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
val timeOfDay: (Long) -> String = { "T" }
|
||||
assertEquals(" • T", timeAbsoluteWith(nowSec, timeOfDay, "never"))
|
||||
assertEquals("never", timeAbsoluteWith(0L, timeOfDay, "never", prefix = ""))
|
||||
// Two years back is always a different year: the date, with no time of day.
|
||||
val old = nowSec - 2L * 366 * 24 * 3600
|
||||
assertTrue(!timeAbsoluteWith(old, timeOfDay, "never").contains("T"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user