From 5175d2ad01d0bde70e4f68f61c2074fd3118de68 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 22:50:27 +0000 Subject: [PATCH 1/3] refactor(commonsUI): move ScreenLayout, TimeAgo, the theme core and the single-blocker components Continues the move of shared UI out of the Android app. - ScreenLayout: the tier rule, CappedScreenContent and the pane widths go to commonsUI. The window-size-class breakpoints (600/840dp) are inlined, since material3-window-size-class is app-only; ScreenLayoutTest's 17 cases pass against them. Android still reads LocalConfiguration and asks the shared rememberScreenLayoutSpec(width, height). - Viewer chrome inset + controls visibility, SlidingCarousel, AudioWaveformReadOnly (local enums replace the Android-only library's), FileAttachmentCard (extractFilename -> commons util) move to commonsUI. PdfFetcher moves to commonsUI jvmAndroid with an explicit diskCache instead of Amethyst.instance; commonsUI jvmAndroid gains okhttp-coroutines, which every OkHttp-shipping module already has. - TimeAgo: the formatter, ToggleableTimeAgoText/TimeAgo, NowProvider and the labels move to commonsUI over four expects (skeleton formatter, calendar day, time-of-day formatter, short relative span). Android keeps its exact behaviour (getBestDateTimePattern, the system 12/24h setting, DateUtils); the JVM uses DateTimeFormatter.ofLocalizedPattern, iOS NSDateFormatter. DateSkeletonFormatterTest pins the JVM side. - Theme: the colour scheme, typography, density and providers become the shared AmethystMaterialTheme; the app's AmethystTheme reads the prefs, calls it, and still tints the system bars. The Vico chart colours stay app-side. The sweep plan records the round and a fresh measurement of what is left. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../playback/composable/wavefront/Waveform.kt | 2 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../ui/actions/uploads/VoiceMessagePreview.kt | 2 +- .../ui/components/FullScreenViewerChrome.kt | 98 +----------- .../ui/components/ZoomableContentDialog.kt | 2 + .../ui/components/pdf/PdfPreviewCard.kt | 13 +- .../ui/components/pdf/PdfViewerDialog.kt | 6 +- .../ui/layouts/ChatHeaderLayoutPreview.kt | 2 +- .../amethyst/ui/layouts/ScreenLayout.kt | 118 +------------- .../layouts/listItem/SlimListItemPreview.kt | 2 +- .../ui/navigation/NavigationEffects.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 2 +- .../amethyst/ui/note/BadgeCompose.kt | 1 + .../amethyst/ui/note/NoteCompose.kt | 5 +- .../amethyst/ui/note/RelayCompose.kt | 1 + .../expiration/ExpirationDatePicker.kt | 2 +- .../note/creators/polls/PollDeadlinePicker.kt | 2 +- .../creators/scheduling/ScheduleAtPicker.kt | 2 +- .../zappolls/ZapPollDeadlinePicker.kt | 2 +- .../amethyst/ui/note/elements/DisplayOts.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 2 +- .../amethyst/ui/note/types/Fundraiser.kt | 2 +- .../amethyst/ui/note/types/Library.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 2 +- .../loggedIn/backups/BackupConflictCards.kt | 2 +- .../backups/BackupConflictReviewScreen.kt | 2 +- .../loggedIn/buzz/AgentWorkBoardScreen.kt | 4 +- .../screen/loggedIn/buzz/BuzzDmListScreen.kt | 6 +- .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 2 +- .../ui/screen/loggedIn/buzz/JobBoardScreen.kt | 4 +- .../loggedIn/buzz/WorkflowRunBoardScreen.kt | 4 +- .../cordnGroup/CordnCreateGroupScreen.kt | 2 +- .../chats/cordnGroup/CordnMessageRow.kt | 4 +- .../loggedIn/chats/feed/ChatDeliveryTicks.kt | 6 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 6 +- .../chats/feed/NewDateOrSubjectDivisor.kt | 2 +- .../chats/feed/layouts/ChatGroupPosition.kt | 2 +- .../marmotGroup/MarmotGroupInfoScreen.kt | 2 +- .../marmotGroup/MarmotGroupListScreen.kt | 4 +- .../send/PrivateMessageEditFieldRow.kt | 2 +- .../concord/ConcordChannelListScreen.kt | 2 +- .../concord/ConcordHomeScreen.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../relayGroup/RelayGroupChannelListScreen.kt | 6 +- .../relayGroup/RelayGroupDiscoveryScreen.kt | 2 +- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +- .../screen/loggedIn/gitRepo/GitItemListRow.kt | 2 +- .../loggedIn/gitRepo/GitRepositoryHomeUi.kt | 2 +- .../screen/loggedIn/nests/NestsFeedLoaded.kt | 2 +- .../notifications/NotificationSidePanel.kt | 2 +- .../podcasts/PodcastEpisodeListItem.kt | 4 +- .../podcasts/PodcastTrailerListItem.kt | 4 +- .../polls/results/PollResultsScreen.kt | 8 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../relayauth/RelayAuthSettingsScreen.kt | 2 +- .../loggedIn/relays/RelayInformationScreen.kt | 2 +- .../relays/eventsync/EventSyncScreen.kt | 2 +- .../scheduledposts/ScheduledPostsScreen.kt | 4 +- .../loggedIn/settings/AppSettingsScreen.kt | 4 +- .../settings/cordn/CordnCoordinatorsScreen.kt | 2 +- .../settings/nip46/Nip46ActivityUi.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 4 +- .../screen/loggedIn/video/UserCardHeader.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 135 +++------------- .../amethyst/ui/layouts/ScreenLayoutTest.kt | 2 + .../2026-08-30-commons-migration-sweep.md | 69 ++++++++ .../amethyst/commons/util/MimeTypeLabels.kt | 7 + commonsUI/build.gradle.kts | 2 + .../ui/note/PlatformDateFormat.android.kt | 78 +++++++++ .../ui/components/AudioWaveformReadOnly.kt | 10 +- .../ui/components/FileAttachmentCard.kt | 6 +- .../commons}/ui/components/SlidingCarousel.kt | 2 +- .../commons/ui/components/ViewerChrome.kt | 130 +++++++++++++++ .../commons/ui/layouts/ScreenLayout.kt | 121 ++++++++++++++ .../commons/ui/note/PlatformDateFormat.kt | 56 +++++++ .../commons}/ui/note/TimeAgoFormatter.kt | 126 +++++---------- .../commons}/ui/note/elements/NowProvider.kt | 2 +- .../commons}/ui/note/elements/TimeAgo.kt | 29 ++-- .../commons/ui/theme/AmethystTheme.kt | 149 ++++++++++++++++++ .../commons/ui/note/PlatformDateFormat.ios.kt | 76 +++++++++ .../commons/service}/pdf/PdfFetcher.kt | 9 +- .../commons/ui/note/CalendarDay.jvmAndroid.kt | 28 ++++ .../commons/ui/note/PlatformDateFormat.jvm.kt | 74 +++++++++ .../ui/note/DateSkeletonFormatterTest.kt | 97 ++++++++++++ 85 files changed, 1077 insertions(+), 531 deletions(-) create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/AudioWaveformReadOnly.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/FileAttachmentCard.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/SlidingCarousel.kt (98%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/TimeAgoFormatter.kt (75%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/elements/NowProvider.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/elements/TimeAgo.kt (88%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service}/pdf/PdfFetcher.kt (92%) create mode 100644 commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index be8bdd5d03..e4bc115142 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.linc.audiowaveform.infiniteLinearGradient +import com.vitorpamplona.amethyst.commons.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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 194a0f8180..e21a488fe6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index 768bed0ade..6db110e268 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt index 8814b623dd..96c0050c96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt @@ -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 { - 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]. * diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index c108571f73..3d9d867150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -80,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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 1228c7a635..4b72f34f30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -44,16 +44,19 @@ 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 @@ -155,7 +158,7 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url) { url -> + .fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) }.use { snapshot -> withContext(Dispatchers.IO) { @@ -327,10 +330,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" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index e7c14e71ce..e063369bc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -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 @@ -194,7 +196,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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt index 1f49f5ac52..c2b95a98e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt index bd5e34e558..4c36ffe9bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt @@ -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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt index f4529fc097..2243aa9c6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt index 4f630d4b99..d7e44af2db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 2fe3758aa3..55c1b8a449 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -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.observeUserContactCardsFollowerCount 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index a0373864e2..ff1515cf19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 08ae330cde..bf7f3246d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index bf580ec3d1..c964eb47d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 0a6874d75f..d1775e6f0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt index e3ced94604..3c98337d8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt index 779ad35dbb..b7b183eedd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index 6ae96c6d6b..d53f4c10e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 623ea87a6c..40de13910d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 81a4a54709..c1a7d5b569 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt index 564ee7077d..3a34469f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt index ca09e1fbc1..18d383e4b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 7e87980caa..463d760f48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -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.LongTextNoteEvent import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index 769c911ae4..465bfe8e8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 6497b54846..e7cd48f968 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt index 54ae79e5a1..580444c5c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index 4c8804f15c..341b508ca2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt @@ -82,15 +82,15 @@ 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.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 @@ -256,7 +256,7 @@ private fun DmRowCard( } Spacer(Modifier.width(8.dp)) Text( - text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)), + text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index 5fe26fb721..53fd592704 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt index 2f3505c4ab..aed7f61cfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt index 42717bf0f3..48e3bdf00b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt index 0b78b0ea5e..6479e51eb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index 031512faea..6d98298451 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -89,6 +89,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 +100,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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt index d5d5e83096..3457109346 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt @@ -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, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 7f0627d365..8b3328cfdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -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.PrivateDmEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt index b72eaa2e2a..355576e500 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt @@ -27,8 +27,8 @@ 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt index d942a54dd9..b710a72044 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 66c97b3f0d..7321b36d9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index 4c8d52184f..7894287c53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -84,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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 49d996db90..bb752cb228 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 3075b9a87a..d95788cda4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index d95ba0659d..95a86e69f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 3f2a7a7496..9a7cd2b7f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index acfaebc195..fc50788692 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index c6e2de0394..0b62d8571f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -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,8 @@ 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.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 +110,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 @@ -885,7 +885,7 @@ private fun BuzzDmInlineRow( } if (row.lastActivity > 0) { Text( - text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)), + text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt index afdf3562b4..3ebf4f1f44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index f758b7bdcc..5556468cd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt index 059df45250..abf6b78359 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt index 382157f68d..6e86a1266f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 52ca972185..cddb8b18e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt index 6a362fffb3..2ca840ae1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt @@ -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 /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index c111567372..2f36698470 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt index 702523008c..0cfe5c96db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt index 05441ae449..884d0d827a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 235be10c53..731c811b4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt index b0bd3b3ed1..23d56d30ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index a49752d925..2d10db1d61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt index 1056c3988c..18b9e8c273 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt index 9d7eedbcfb..097bb9282c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 1651960528..2faf472a8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index 3459eec409..cdae46aa2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt index 16c4a71e1e..e4180fef22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index fbd05a0120..3e68c643d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt index 62b3f8d31a..a1a1076c97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 5521b8f719..d10655579f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -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 - } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt index 46c164ae5d..d6600bd914 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.layouts import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle +import com.vitorpamplona.amethyst.commons.ui.layouts.decideNavigationStyle +import com.vitorpamplona.amethyst.commons.ui.layouts.hasRoomForNotificationPanel import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 46de62e8af..35693ab7e1 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1432,3 +1432,72 @@ 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`'s 17 cases pass 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`: + - filter assemblers and `*LastRead`; + - `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`; + - the Tor status/dialog VM; + - `ChatBubbleLayout` + `ChatGroupPosition` + `JumboEmoji` + `NewDateOrSubjectDivisor` + `AutoScrollToNewest`. + - The chat bubble set is the next obvious batch: 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. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt index 1911a22019..5d1f7c8fd1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt @@ -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 +} diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 4d54405f96..87e9b9a0ea 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -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. diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt new file mode 100644 index 0000000000..1aa0e916ea --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.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>() + + 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 relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> 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 +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt index ea26b2582a..712a76b999 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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.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 } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt index 07bd4f82be..5960bb8db3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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, @@ -94,7 +94,7 @@ fun FileAttachmentCard( * [com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard]. */ @Composable -internal fun FileAttachmentRow( +fun FileAttachmentRow( symbol: MaterialSymbol, title: String, subtitle: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt index ea7976700c..6af0b5d492 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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 diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt new file mode 100644 index 0000000000..c93a2283c7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt @@ -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 { + 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 +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt index 03fb009018..260e228a53 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt @@ -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), + ) + } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt new file mode 100644 index 0000000000..36bfd747a6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.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. + */ +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") that follows the platform's own 12/24-hour + * preference, which on Android is a system setting rather than a locale property. + */ +@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, and [fallback] where the platform has no such + * formatter. + */ +expect fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt similarity index 75% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt index 455c2bedb0..e0a8137456 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt @@ -18,11 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.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>() - - 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 relativeTimeSpanShort(time * 1000, TimeUtils.nowMillis(), labels.now) { timeAgoWith(time, labels, prefix = "") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt index f3e7a65ee6..4dfa5f2175 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt index 312f8dc11f..1463dd1d88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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,16 @@ 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.elements.LocalNowSeconds +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 +92,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 +114,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) } } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt new file mode 100644 index 0000000000..4a988d5716 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt @@ -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 + } diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt new file mode 100644 index 0000000000..060ad318f9 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -0,0 +1,76 @@ +/* + * 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 androidx.compose.runtime.remember +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 + +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. */ +actual class DateSkeletonFormatter actual constructor( + skeleton: String, +) { + private val formatter = + NSDateFormatter().apply { + locale = NSLocale.currentLocale + setLocalizedDateFormatFromTemplate(skeleton) + } + + actual fun format(epochMillis: Long): String = 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 +} + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = + remember { + val formatter = + NSDateFormatter().apply { + dateStyle = NSDateFormatterNoStyle + timeStyle = NSDateFormatterShortStyle + } + val format: (Long) -> String = { epochMillis -> formatter.stringFromDate(dateOf(epochMillis)) } + format + } + +actual fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String = fallback() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index c1fcee999a..6b562a668c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -18,10 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, 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 +package com.vitorpamplona.amethyst.commons.service.pdf import coil3.disk.DiskCache -import com.vitorpamplona.amethyst.Amethyst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -35,15 +34,15 @@ object PdfFetcher { * 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. + * Pass the app's Coil [diskCache] so PDFs share the same LRU eviction and disk budget as + * images. */ suspend fun fetchSnapshot( url: String, + diskCache: DiskCache, 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 diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt new file mode 100644 index 0000000000..789aa61268 --- /dev/null +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.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) +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt new file mode 100644 index 0000000000..18b5273f35 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt @@ -0,0 +1,74 @@ +/* + * 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 androidx.compose.runtime.remember +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)) + } +} + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = + remember { + val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()) + val format: (Long) -> String = { epochMillis -> formatter.format(Instant.ofEpochMilli(epochMillis)) } + format + } + +actual fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String = fallback() diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt new file mode 100644 index 0000000000..e95be9295e --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt @@ -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")) + } +} From 78e7561fb950766bddd968205940cbbb628d00f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 23:10:42 +0000 Subject: [PATCH 2/3] refactor(commonsUI): move the chat bubble group, and fix the round's audit findings The chat bubble layout, its group-position shapes, jumbo emoji sizing, the date/subject divisor and the scroll-to-newest effect move to commonsUI commons/chats/ui. Desktop keeps its older ChatBubbleLayout fork for now; the merge is the Desktop phase. Audit follow-ups on the previous commit: - PdfFetcher reads the disk cache from a provider on the IO dispatcher, so a cold-start PDF card no longer builds the app's lazy cache on main. - The Buzz DM lists resolve TimeAgoLabels once per list, not per row. - The iOS DateSkeletonFormatter rebuilds on a locale/time-zone change. - ScreenLayoutTest pins the inclusive 600dp breakpoint. - Stale KDoc links, a same-package import, and the baseline profile's stable-name entries for NowProvider and the moved theme functions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../baselineProfiles/baseline-prof.txt | 33 ++++++++--------- .../ui/components/pdf/PdfPreviewCard.kt | 2 +- .../ui/components/pdf/PdfViewerDialog.kt | 2 +- .../AccountSwitcherAndLeftDrawerLayout.kt | 2 +- .../screen/loggedIn/buzz/BuzzDmListScreen.kt | 10 ++++-- .../chats/cordnGroup/CordnGroupChatScreen.kt | 2 +- .../chats/cordnGroup/CordnMessageRow.kt | 10 +++--- .../loggedIn/chats/feed/ChatFeedView.kt | 4 ++- .../loggedIn/chats/feed/ChatMessageCompose.kt | 5 +-- .../chats/feed/types/RenderRegularTextNote.kt | 4 +-- .../relayGroup/RelayGroupChannelListScreen.kt | 8 ++++- .../cordn/CordnMessageGroupingTest.kt | 2 +- .../amethyst/ui/layouts/ScreenLayoutTest.kt | 4 +++ .../2026-08-30-commons-migration-sweep.md | 23 ++++++++++++ .../commons/chats/ui}/AutoScrollToNewest.kt | 4 +-- .../commons/chats/ui}/ChatBubbleLayout.kt | 3 +- .../commons/chats/ui}/ChatGroupPosition.kt | 4 +-- .../amethyst/commons/chats/ui}/JumboEmoji.kt | 2 +- .../chats/ui}/NewDateOrSubjectDivisor.kt | 3 +- .../ui/components/FileAttachmentCard.kt | 2 +- .../commons/ui/note/elements/TimeAgo.kt | 1 - .../commons/ui/note/PlatformDateFormat.ios.kt | 35 ++++++++++++++----- .../commons/service/pdf/PdfFetcher.kt | 8 +++-- 23 files changed, 116 insertions(+), 57 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/AutoScrollToNewest.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/ChatBubbleLayout.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/ChatGroupPosition.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/JumboEmoji.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/NewDateOrSubjectDivisor.kt (94%) diff --git a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt index d760efd883..016fe51292 100644 --- a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt +++ b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt @@ -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;->()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;->()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;->()V -Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->(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;->()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;->()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;->(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;->()V diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 4b72f34f30..7499c717c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -158,7 +158,7 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> + .fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) }.use { snapshot -> withContext(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index e063369bc7..20e6408a72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -196,7 +196,7 @@ private fun PdfViewerContent( try { withContext(Dispatchers.IO) { val snapshot = - PdfFetcher.fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> + PdfFetcher.fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) } try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 19d5be3a79..475ef9d787 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index 341b508ca2..bac94781ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.resources.cancel 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 @@ -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, rememberTimeAgoLabels()), + text = timeAgoShort(row.lastActivity, timeLabels), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index b884f56dbd..f51173a565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index 6d98298451..7c5696ca0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -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 @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index d58a1422a7..25519ffbb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index ccb19c4c2d..27c29984a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index eec5965811..ba2c0c0dfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 0b62d8571f..30efeb7f3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -99,6 +99,7 @@ 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 @@ -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, rememberTimeAgoLabels()), + text = timeAgoShort(row.lastActivity, timeLabels), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt index 8a6066509a..8eebd64a2e 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt @@ -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 diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt index d6600bd914..5c99632843 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt @@ -88,6 +88,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 diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 35693ab7e1..d24a6a3134 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1501,3 +1501,26 @@ A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after th - **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 or time-zone change, like the + other actuals. + - `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`), so **regenerate the profile** after these + moves. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt index d2d06805f7..9480580f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt index 67ae2a6d64..3583623b52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt index b710a72044..1ba16678a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt index 84d66d72b3..6f00e37c0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt index 355576e500..779bb759ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt @@ -18,11 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.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 diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt index 5960bb8db3..d0d1b13679 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt @@ -91,7 +91,7 @@ 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 fun FileAttachmentRow( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt index 1463dd1d88..636008baf9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.graphics.Color 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.ui.note.elements.LocalNowSeconds import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.timeAbsoluteWith diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt index 060ad318f9..e64f02c89d 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -30,22 +30,41 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle import platform.Foundation.NSLocale +import platform.Foundation.NSTimeZone import platform.Foundation.currentLocale import platform.Foundation.dateWithTimeIntervalSince1970 +import platform.Foundation.defaultTimeZone +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. */ +/** + * NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. + * Rebuilt when the current locale or time zone changes, like the other actuals. + */ actual class DateSkeletonFormatter actual constructor( - skeleton: String, + private val skeleton: String, ) { - private val formatter = - NSDateFormatter().apply { - locale = NSLocale.currentLocale - setLocalizedDateFormatFromTemplate(skeleton) - } + private class Cached( + val key: String, + val formatter: NSDateFormatter, + ) - actual fun format(epochMillis: Long): String = formatter.stringFromDate(dateOf(epochMillis)) + @Volatile private var cached: Cached? = null + + actual fun format(epochMillis: Long): String { + val locale = NSLocale.currentLocale + val key = locale.localeIdentifier + "|" + NSTimeZone.defaultTimeZone.name + 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 { diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index 6b562a668c..5af7e879e5 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -34,15 +34,17 @@ object PdfFetcher { * 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 [diskCache] so PDFs share the same LRU eviction and disk budget as - * images. + * 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, + diskCache: () -> DiskCache, okHttpClient: (String) -> OkHttpClient, ): DiskCache.Snapshot = withContext(Dispatchers.IO) { + val diskCache = 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 From 417f4060a7cb9d32e64bfedb8e3dcf0f73e86a82 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 23:40:07 +0000 Subject: [PATCH 3/3] fix: second audit of the commonsUI move - PDF snapshot close off main, shared formatters, tests - PdfPreviewCard closed its cache snapshot on the main thread (the use{} ran after withContext(IO) returned), taking Coil's global DiskLruCache lock there; pre-existing. PdfFetcher.useSnapshot fetches, runs the block and closes inside one IO block, with no suspension point for cancellation to leak through. The PDF viewer's onDispose 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 share one cached instance now (NSDateFormatter is costly to build). - timeAgoShort no longer allocates an unused fallback lambda per tick on Android: the expect returns null where the platform has no short span. - The iOS skeleton formatter keys on the locale only, like Android: iOS caches its system zone until reset, so keying on it cost lookups and never saw a change. KDocs now say which platforms follow what. - JumboEmojiTest covers emoji counting (ZWJ, skin tone, flag, keycap) and the bubble shapes; ScreenLayoutTest moves to commonsUI jvmTest beside its code. - The plan records the audit, and that the packaged Desktop runtime lacks jdk.localedata, which these formatters need before Desktop uses them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../ui/components/pdf/PdfPreviewCard.kt | 14 ++- .../ui/components/pdf/PdfViewerDialog.kt | 10 +- .../2026-08-30-commons-migration-sweep.md | 51 +++++++--- .../ui/note/PlatformDateFormat.android.kt | 5 +- .../commons/ui/note/PlatformDateFormat.kt | 16 ++-- .../commons/ui/note/TimeAgoFormatter.kt | 2 +- .../commons/ui/note/PlatformDateFormat.ios.kt | 36 ++++---- .../commons/service/pdf/PdfFetcher.kt | 84 +++++++++++------ .../commons/ui/note/PlatformDateFormat.jvm.kt | 35 ++++--- .../commons/chats/ui/JumboEmojiTest.kt | 92 +++++++++++++++++++ .../commons}/ui/layouts/ScreenLayoutTest.kt | 5 +- 11 files changed, 255 insertions(+), 95 deletions(-) create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons}/ui/layouts/ScreenLayoutTest.kt (93%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 7499c717c1..6e218e60c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -61,8 +61,6 @@ 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 @@ -158,12 +156,12 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index 20e6408a72..4376d12716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -82,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 @@ -223,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() } + } + } } } diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index d24a6a3134..bf9c67a45d 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1439,7 +1439,7 @@ functional bugs. The follow-ups: | 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`'s 17 cases pass against them. `LocalConfiguration` stays in the app, which asks the shared `rememberScreenLayoutSpec(widthDp, heightDp)`, so Desktop can supply its own window size. | +| `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 | @@ -1472,12 +1472,13 @@ Desktop still has its own `ToggleableTimeAgoText` and the older `commons/…/uti 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`: - - filter assemblers and `*LastRead`; - - `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`; - - the Tor status/dialog VM; - - `ChatBubbleLayout` + `ChatGroupPosition` + `JumboEmoji` + `NewDateOrSubjectDivisor` + `AutoScrollToNewest`. - - The chat bubble set is the next obvious batch: Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of it. +- **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. @@ -1515,12 +1516,40 @@ A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after th - `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 or time-zone change, like the - other actuals. + - 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`), so **regenerate the profile** after these - moves. + `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. diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt index 1aa0e916ea..385e7335e5 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -60,12 +60,11 @@ actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String { return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } } } -actual fun relativeTimeSpanShort( +actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String { +): String? { val humanReadable = DateUtils .getRelativeTimeSpanString( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt index 36bfd747a6..94711c83d9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -25,7 +25,8 @@ 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. + * 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, @@ -37,20 +38,19 @@ expect class DateSkeletonFormatter( expect fun calendarYearAndDay(epochMillis: Long): Int /** - * A time-of-day formatter ("14:32" / "2:32 PM") that follows the platform's own 12/24-hour - * preference, which on Android is a system setting rather than a locale property. + * 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, and [fallback] where the platform has no such - * formatter. + * instant that rounds to now or lies ahead, or null where the platform has no such formatter. */ -expect fun relativeTimeSpanShort( +expect fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String +): String? diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt index e0a8137456..649b149804 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt @@ -375,5 +375,5 @@ fun timeAgoShort( labels: TimeAgoLabels, ): String { if (time == null) return " " - return relativeTimeSpanShort(time * 1000, TimeUtils.nowMillis(), labels.now) { timeAgoWith(time, labels, prefix = "") } + return relativeTimeSpanShortOrNull(time * 1000, TimeUtils.nowMillis(), labels.now) ?: timeAgoWith(time, labels, prefix = "") } diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt index e64f02c89d..3a03a0d9e7 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarUnitDay import platform.Foundation.NSCalendarUnitYear @@ -30,10 +29,8 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle import platform.Foundation.NSLocale -import platform.Foundation.NSTimeZone import platform.Foundation.currentLocale import platform.Foundation.dateWithTimeIntervalSince1970 -import platform.Foundation.defaultTimeZone import platform.Foundation.localeIdentifier import kotlin.concurrent.Volatile @@ -41,7 +38,9 @@ private fun dateOf(epochMillis: Long) = NSDate.dateWithTimeIntervalSince1970(epo /** * NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. - * Rebuilt when the current locale or time zone changes, like the other actuals. + * 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, @@ -55,7 +54,7 @@ actual class DateSkeletonFormatter actual constructor( actual fun format(epochMillis: Long): String { val locale = NSLocale.currentLocale - val key = locale.localeIdentifier + "|" + NSTimeZone.defaultTimeZone.name + val key = locale.localeIdentifier val formatter = cached?.takeIf { it.key == key }?.formatter ?: NSDateFormatter() @@ -75,21 +74,22 @@ actual fun calendarYearAndDay(epochMillis: Long): Int { return year * 1000 + day } -@Composable -actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = - remember { - val formatter = - NSDateFormatter().apply { - dateStyle = NSDateFormatterNoStyle - timeStyle = NSDateFormatterShortStyle - } - val format: (Long) -> String = { epochMillis -> formatter.stringFromDate(dateOf(epochMillis)) } - format +// 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 } +} -actual fun relativeTimeSpanShort( +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, - fallback: () -> String, -): String = fallback() +): String? = null diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index 5af7e879e5..f211373bb8 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -42,39 +42,63 @@ object PdfFetcher { url: String, diskCache: () -> DiskCache, okHttpClient: (String) -> OkHttpClient, - ): DiskCache.Snapshot = - withContext(Dispatchers.IO) { - val diskCache = 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 } + ): DiskCache.Snapshot = withContext(Dispatchers.IO) { openOrDownload(url, diskCache, okHttpClient) } - val editor = diskCache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url") - try { - val request = - Request - .Builder() - .url(url) - .get() - .build() + /** 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 } - 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") - } + 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") } - - editor.commitAndOpenSnapshot() ?: throw IOException("Unable to commit cache editor for $url") - } catch (t: Throwable) { - runCatching { editor.abort() } - throw t } + + 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 useSnapshot( + url: String, + diskCache: () -> DiskCache, + okHttpClient: (String) -> OkHttpClient, + block: (DiskCache.Snapshot) -> T, + ): T = + withContext(Dispatchers.IO) { + openOrDownload(url, diskCache, okHttpClient).use(block) } } diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt index 18b5273f35..3ad02fe674 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -58,17 +57,31 @@ actual class DateSkeletonFormatter actual constructor( } } -@Composable -actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = - remember { - val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()) - val format: (Long) -> String = { epochMillis -> formatter.format(Instant.ofEpochMilli(epochMillis)) } - format - } +private class CachedTimeOfDay( + val locale: Locale, + val zone: ZoneId, + val formatter: DateTimeFormatter, +) -actual fun relativeTimeSpanShort( +@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, - fallback: () -> String, -): String = fallback() +): String? = null diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt new file mode 100644 index 0000000000..1872d94557 --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt @@ -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)) + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt similarity index 93% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt index 5c99632843..a52dd5f1eb 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt @@ -18,11 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.layouts +package com.vitorpamplona.amethyst.commons.ui.layouts -import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle -import com.vitorpamplona.amethyst.commons.ui.layouts.decideNavigationStyle -import com.vitorpamplona.amethyst.commons.ui.layouts.hasRoomForNotificationPanel import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue