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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY
This commit is contained in:
Claude
2026-09-26 23:10:42 +00:00
parent 5175d2ad01
commit 78e7561fb9
23 changed files with 116 additions and 57 deletions
@@ -24768,17 +24768,17 @@ SPLcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;->getActivityOrNull(La
Lcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;-><clinit>()V
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;-><clinit>()V
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V
Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
Lcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$0(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/ui/screen/AccountState;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$2(Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;Lcom/vitorpamplona/amethyst/ui/screen/AccountState;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
@@ -25632,6 +25632,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize40dp()F
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize50dp()F
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt;
Lcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;
@@ -25648,13 +25649,13 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9(Landroid
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/model/FontSizeType;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->accentContainer-DxMtmZc(JZ)J
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystDarkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->lightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystLightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccent-8_81llA(J)J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccentContainer-DxMtmZc(JZ)J
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;
@@ -25685,7 +25686,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpaci
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont14SP()J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont17SP()J
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
@@ -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) {
@@ -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 {
@@ -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
@@ -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,
)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
@@ -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,
)
@@ -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
@@ -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
@@ -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.
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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
@@ -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
@@ -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(
@@ -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
@@ -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 {
@@ -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