From 7254d14f26a83000079fb3fec7183fb9c6093e21 Mon Sep 17 00:00:00 2001 From: agent-p1p Date: Wed, 15 Jul 2026 20:53:53 +0200 Subject: [PATCH 1/2] refactor(ui): centralize scrims and dividers Replace ad-hoc media scrim alphas with semantic theme tokens and route all horizontal separators through AppDivider. Fixes #1384 --- .../whitenoise/android/ui/MarkdownRenderer.kt | 6 +- .../ui/account/AccountSelectorSheet.kt | 4 +- .../ui/conversation/ConversationRows.kt | 8 +-- .../ui/conversation/composer/MentionPicker.kt | 4 +- .../ui/conversation/media/MediaControls.kt | 3 +- .../conversation/media/MediaImageBubbles.kt | 7 ++- .../ui/conversation/media/MediaPending.kt | 7 ++- .../ui/conversation/media/MediaPreview.kt | 5 +- .../ui/conversation/media/MediaVideo.kt | 11 ++-- .../ui/conversation/media/MediaViewer.kt | 3 +- .../ui/conversation/media/RecentMedia.kt | 3 +- .../conversation/messages/MessageActions.kt | 4 +- .../messages/MessageBubbleFooter.kt | 3 +- .../android/ui/group/GroupDetailsScreen.kt | 15 +++-- .../android/ui/group/GroupEditScreen.kt | 3 +- .../android/ui/group/GroupImageSearch.kt | 3 +- .../android/ui/profile/ProfileEditScreen.kt | 3 +- .../android/ui/profile/ProfileSheet.kt | 7 +-- .../android/ui/qr/QrScannerSheet.kt | 3 +- .../ui/settings/NotificationsScreen.kt | 8 +-- .../ui/settings/SecurityPrivacyScreen.kt | 16 ++--- .../android/ui/theme/VisualTokens.kt | 30 +++++++++ .../android/ui/theme/VisualTokensTest.kt | 61 +++++++++++++++++++ 23 files changed, 158 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt create mode 100644 app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/MarkdownRenderer.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/MarkdownRenderer.kt index 06b2900cd..a3ea1dec2 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/MarkdownRenderer.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/MarkdownRenderer.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -68,6 +67,7 @@ import dev.ipf.marmotkit.MarkdownNostrHrpFfi import dev.ipf.marmotkit.MarkdownTableCellFfi import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.core.ProfileSanitizer +import dev.ipf.whitenoise.android.ui.theme.AppDivider import java.net.IDN import java.net.URI import java.util.Locale @@ -483,7 +483,7 @@ private fun MarkdownBlockView( onTextLayout = onTextLayout, ) MarkdownBlockFfi.ThematicBreak -> - HorizontalDivider(color = LocalContentColor.current.copy(alpha = 0.25f)) + AppDivider() is MarkdownBlockFfi.CodeBlock -> MarkdownCodeBlockView(block.content, ctx.useDecorativeBackgrounds) is MarkdownBlockFfi.BlockQuote -> MarkdownBlockQuoteView(block.blocks, ctx, depth) is MarkdownBlockFfi.ListBlock -> MarkdownListView(block, ctx, depth) @@ -619,7 +619,7 @@ private fun MarkdownTableView( val rowsElided = markdownSiblingsElided(block.rows) Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { MarkdownTableRowView(block.header, block.alignments, header = true, ctx) - HorizontalDivider(color = LocalContentColor.current.copy(alpha = 0.25f)) + AppDivider() visibleRows.forEach { row -> MarkdownTableRowView(row, block.alignments, header = false, ctx) } diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/account/AccountSelectorSheet.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/account/AccountSelectorSheet.kt index b9027e9de..7094bcea3 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/account/AccountSelectorSheet.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/account/AccountSelectorSheet.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -62,6 +61,7 @@ import dev.ipf.whitenoise.android.ui.common.UnreadCountBadge import dev.ipf.whitenoise.android.ui.profile.AvatarFullScreenViewer import dev.ipf.whitenoise.android.ui.profile.rememberAvatarImageAvailable import dev.ipf.whitenoise.android.ui.settings.settingsRowAmoledSurfaceBorder +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.amoledSheetContainerColor import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorder import kotlinx.coroutines.CancellationException @@ -245,7 +245,7 @@ internal fun AccountSelectorSheet( } }, ) - HorizontalDivider() + AppDivider() } } } diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationRows.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationRows.kt index 98086d927..e0d021bdc 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationRows.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationRows.kt @@ -28,7 +28,6 @@ import androidx.compose.material.icons.filled.Group import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults @@ -57,6 +56,7 @@ import dev.ipf.whitenoise.android.core.MessageDebugClassifier import dev.ipf.whitenoise.android.state.WhiteNoiseAppState import dev.ipf.whitenoise.android.ui.common.rememberGroupSystemCopy import dev.ipf.whitenoise.android.ui.group.disappearingMessagesLabel +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke import java.time.Instant import java.time.LocalDate @@ -76,9 +76,8 @@ internal fun UnreadMessagesDivider(count: Int) { .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { - HorizontalDivider( + AppDivider( modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outlineVariant, ) Text( text = text, @@ -92,9 +91,8 @@ internal fun UnreadMessagesDivider(count: Int) { shape = RoundedCornerShape(12.dp), ).padding(horizontal = 10.dp, vertical = 4.dp), ) - HorizontalDivider( + AppDivider( modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.outlineVariant, ) } } diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/MentionPicker.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/MentionPicker.kt index 60a055d36..9c28b5996 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/MentionPicker.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/MentionPicker.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -34,6 +33,7 @@ import androidx.compose.ui.unit.dp import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.core.MentionComposer import dev.ipf.whitenoise.android.ui.common.Avatar +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke /** @@ -64,7 +64,7 @@ internal fun MentionPicker( color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant) + AppDivider() LazyColumn(Modifier.fillMaxWidth()) { items(candidates, key = { it.accountIdHex }) { candidate -> val mentionLabel = stringResource(R.string.mention_picker_member, candidate.displayName) diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaControls.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaControls.kt index 5a34b2158..c81146446 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaControls.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaControls.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha internal enum class OpenAttachmentResult { Opened, NoHandler, Error } @@ -47,7 +48,7 @@ internal fun MediaCircleAction( Surface( onClick = onClick, shape = CircleShape, - color = Color.Black.copy(alpha = 0.55f), + color = Color.Black.copy(alpha = ScrimAlpha.Strong), contentColor = Color.White, modifier = modifier.size(52.dp), ) { diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaImageBubbles.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaImageBubbles.kt index 857d1fa95..427ad5b5a 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaImageBubbles.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaImageBubbles.kt @@ -57,6 +57,7 @@ import dev.ipf.whitenoise.android.state.ConversationController import dev.ipf.whitenoise.android.state.MediaAutoDownloadType import dev.ipf.whitenoise.android.state.TimelineMessage import dev.ipf.whitenoise.android.state.WhiteNoiseAppState +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -320,7 +321,7 @@ internal fun MediaImageBubble( } if (uploading) { Surface( - color = Color.Black.copy(alpha = 0.55f), + color = Color.Black.copy(alpha = ScrimAlpha.Strong), shape = CircleShape, modifier = Modifier.size(48.dp), ) { @@ -664,7 +665,7 @@ internal fun MediaImageGridTile( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)), + .background(Color.Black.copy(alpha = ScrimAlpha.MediumEmphasis)), contentAlignment = Alignment.Center, ) { Text( @@ -677,7 +678,7 @@ internal fun MediaImageGridTile( } if (uploading) { Box( - modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.35f)), + modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = ScrimAlpha.Light)), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPending.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPending.kt index 6d5be3a0e..7175ca876 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPending.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPending.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.media.MediaPipeline import dev.ipf.whitenoise.android.state.PendingAttachment +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -103,7 +104,7 @@ internal fun MediaPendingPlaceholder( contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize(), ) - Box(Modifier.matchParentSize().background(Color.Black.copy(alpha = 0.35f))) + Box(Modifier.matchParentSize().background(Color.Black.copy(alpha = ScrimAlpha.Light))) } PendingStatusOverlay( failed = failed, @@ -136,7 +137,7 @@ internal fun MediaPendingPlaceholder( ) } Box( - Modifier.matchParentSize().background(Color.Black.copy(alpha = 0.35f)), + Modifier.matchParentSize().background(Color.Black.copy(alpha = ScrimAlpha.Light)), ) PendingStatusOverlay( failed = failed, @@ -224,7 +225,7 @@ private fun PendingGridTile( } if (overflowCount > 0 && preview != null) { Box( - Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.5f)), + Modifier.fillMaxSize().background(Color.Black.copy(alpha = ScrimAlpha.MediumEmphasis)), contentAlignment = Alignment.Center, ) { Text( diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPreview.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPreview.kt index 053e367eb..dad9233b7 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPreview.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaPreview.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.media.MediaPipeline +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext @@ -453,7 +454,7 @@ private fun HeroMediaPreview( ) } if (metadata?.isVideo == true) { - Surface(shape = CircleShape, color = Color.Black.copy(alpha = 0.55f)) { + Surface(shape = CircleShape, color = Color.Black.copy(alpha = ScrimAlpha.Strong)) { Icon( Icons.Default.PlayArrow, contentDescription = stringResource(R.string.reply_media_video), @@ -550,7 +551,7 @@ private fun PreviewStripThumb( } if (metadata?.isVideo == true) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Surface(shape = CircleShape, color = Color.Black.copy(alpha = 0.55f)) { + Surface(shape = CircleShape, color = Color.Black.copy(alpha = ScrimAlpha.Strong)) { Icon( Icons.Default.PlayArrow, contentDescription = null, diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaVideo.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaVideo.kt index 16ef32fb0..e745c0194 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaVideo.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaVideo.kt @@ -62,6 +62,7 @@ import dev.ipf.whitenoise.android.media.playbackErrorInvalidatesAttachmentCache import dev.ipf.whitenoise.android.state.ConversationController import dev.ipf.whitenoise.android.state.MediaAutoDownloadType import dev.ipf.whitenoise.android.state.WhiteNoiseAppState +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -256,7 +257,7 @@ internal fun MediaVideoGridTile( Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)) } Surface( - color = Color.Black.copy(alpha = 0.55f), + color = Color.Black.copy(alpha = ScrimAlpha.Strong), shape = CircleShape, modifier = Modifier @@ -300,7 +301,7 @@ internal fun MediaVideoGridTile( modifier = Modifier .matchParentSize() - .background(Color.Black.copy(alpha = 0.45f)), + .background(Color.Black.copy(alpha = ScrimAlpha.Medium)), contentAlignment = Alignment.Center, ) { Text( @@ -312,7 +313,7 @@ internal fun MediaVideoGridTile( } if (uploading) { Box( - modifier = Modifier.matchParentSize().background(Color.Black.copy(alpha = 0.35f)), + modifier = Modifier.matchParentSize().background(Color.Black.copy(alpha = ScrimAlpha.Light)), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -502,7 +503,7 @@ internal fun MediaVideoBubble( // When startDownload is gated off (policy says no auto-fetch), the // triangle becomes a download icon and tap consents to the fetch. Surface( - color = Color.Black.copy(alpha = 0.55f), + color = Color.Black.copy(alpha = ScrimAlpha.Strong), shape = CircleShape, modifier = Modifier @@ -636,7 +637,7 @@ internal fun MediaVideoBubble( // Duration pill bottom-start. Only shown once duration is known. if (durationMs > 0L) { Surface( - color = Color.Black.copy(alpha = 0.55f), + color = Color.Black.copy(alpha = ScrimAlpha.Strong), shape = RoundedCornerShape(6.dp), modifier = Modifier diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaViewer.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaViewer.kt index b18ae2638..e223b860c 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaViewer.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/MediaViewer.kt @@ -74,6 +74,7 @@ import dev.ipf.whitenoise.android.ui.common.applyViewerTransformGesture import dev.ipf.whitenoise.android.ui.common.clampViewerPageIndex import dev.ipf.whitenoise.android.ui.common.resetViewerTransform import dev.ipf.whitenoise.android.ui.common.viewerPagerScrollEnabled +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map @@ -336,7 +337,7 @@ internal fun MediaViewerFrame( .align(Alignment.BottomCenter) .background( Brush.verticalGradient( - listOf(Color.Transparent, Color.Black.copy(alpha = 0.6f)), + listOf(Color.Transparent, Color.Black.copy(alpha = ScrimAlpha.Gradient)), ), ).navigationBarsPadding() .padding(horizontal = 16.dp, vertical = 12.dp), diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/RecentMedia.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/RecentMedia.kt index adc37a32c..eb89a476c 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/RecentMedia.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/media/RecentMedia.kt @@ -52,6 +52,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import dev.ipf.whitenoise.android.R +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -271,7 +272,7 @@ private fun RecentThumb( .align(Alignment.Center) .size(24.dp) .clip(CircleShape) - .background(Color.Black.copy(alpha = 0.5f)), + .background(Color.Black.copy(alpha = ScrimAlpha.MediumEmphasis)), contentAlignment = Alignment.Center, ) { Icon( diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageActions.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageActions.kt index 2c676afd4..cfaf428d2 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageActions.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageActions.kt @@ -34,7 +34,6 @@ import androidx.compose.material.icons.filled.TextFields import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -83,6 +82,7 @@ import dev.ipf.whitenoise.android.ui.chats.newchat.SelectionIndicator import dev.ipf.whitenoise.android.ui.common.rememberGroupTitleCopy import dev.ipf.whitenoise.android.ui.design.KeyboardSafePopup import dev.ipf.whitenoise.android.ui.resolveMentionsInPlaintext +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.Dimens import dev.ipf.whitenoise.android.ui.theme.amoledSheetContainerColor import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke @@ -246,7 +246,7 @@ internal fun MessageActionMenu( ) } } - HorizontalDivider() + AppDivider() } Column( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubbleFooter.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubbleFooter.kt index f1214289e..89c32d076 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubbleFooter.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubbleFooter.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import dev.ipf.whitenoise.android.state.MessageStatus +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha // Gap between a bubble's text and its trailing inline footer. private val BubbleFooterGap = 8.dp @@ -30,7 +31,7 @@ private fun MediaScrimFooter( ) { Box( modifier - .background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(percent = 50)) + .background(Color.Black.copy(alpha = ScrimAlpha.LightEmphasis), RoundedCornerShape(percent = 50)) .padding(horizontal = 8.dp, vertical = 3.dp), ) { content() diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupDetailsScreen.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupDetailsScreen.kt index 23c710ec1..f0d3cea75 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupDetailsScreen.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupDetailsScreen.kt @@ -56,7 +56,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -133,6 +132,7 @@ import dev.ipf.whitenoise.android.ui.profile.AvatarFullScreenViewer import dev.ipf.whitenoise.android.ui.profile.rememberAvatarImageAvailable import dev.ipf.whitenoise.android.ui.settings.ChatBubbleColorsScreen import dev.ipf.whitenoise.android.ui.settings.DiagnosticRow +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.Dimens import dev.ipf.whitenoise.android.ui.theme.PillShape import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorderStroke @@ -694,7 +694,7 @@ internal fun GroupDetailsScreen( ) } - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + AppDivider() SettingsActionRow( icon = Icons.Default.Schedule, title = stringResource(R.string.disappearing_messages), @@ -721,7 +721,7 @@ internal fun GroupDetailsScreen( appState.updateCollapseLongMessagesInGroup(controller.group.groupIdHex, it) }, ) - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + AppDivider() SectionHeader(stringResource(R.string.notifications)) SettingsActionRow( icon = Icons.Default.Notifications, @@ -805,7 +805,7 @@ internal fun GroupDetailsScreen( ) } - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + AppDivider() Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -961,7 +961,7 @@ internal fun GroupDetailsScreen( } } - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + AppDivider() SectionHeader(stringResource(R.string.info)) Column( Modifier @@ -1035,7 +1035,7 @@ internal fun GroupDetailsScreen( // sole-admin and sole-member cases get their own confirm copy. On // failure the controller's lastMutationError surfaces inline here // (in addition to the snackbar) so the user can retry in place. - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + AppDivider() val selfMember = controller.members.firstOrNull { GroupProjector.isActiveAccountMember(it, activeAccountIdHex) } if (canEdit && selfMember != null) { @@ -1631,9 +1631,8 @@ private fun PushDeliveryDebugSection( ) info.tokens.forEachIndexed { index, token -> if (index > 0) { - HorizontalDivider( + AppDivider( modifier = Modifier.padding(vertical = 8.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), ) } PushTokenDebugRows(token, appState, yesNo) diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupEditScreen.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupEditScreen.kt index 3e817e8b6..3c70fcb49 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupEditScreen.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupEditScreen.kt @@ -55,6 +55,7 @@ import dev.ipf.whitenoise.android.ui.common.rememberGroupTitleCopy import dev.ipf.whitenoise.android.ui.profile.AvatarFullScreenViewer import dev.ipf.whitenoise.android.ui.profile.rememberAvatarImageAvailable import dev.ipf.whitenoise.android.ui.theme.Dimens +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -192,7 +193,7 @@ internal fun GroupEditScreen( .offset(x = 6.dp, y = 6.dp) .size(36.dp) .clip(CircleShape) - .background(Color.Black.copy(alpha = 0.62f)) + .background(Color.Black.copy(alpha = ScrimAlpha.Heavy)) .clickable( onClickLabel = editImageLabel, role = Role.Button, diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupImageSearch.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupImageSearch.kt index d70752a00..fb8e76893 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupImageSearch.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/group/GroupImageSearch.kt @@ -79,6 +79,7 @@ import dev.ipf.whitenoise.android.media.ImageSearchResult import dev.ipf.whitenoise.android.media.sanitizeHttpsAvatarUrl import dev.ipf.whitenoise.android.ui.common.Avatar import dev.ipf.whitenoise.android.ui.settings.subtitleRes +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSheetContainerColor import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -450,7 +451,7 @@ private fun GroupImageSearchTile( Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .background(Color.Black.copy(alpha = 0.5f)) + .background(Color.Black.copy(alpha = ScrimAlpha.MediumEmphasis)) .padding(horizontal = 6.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileEditScreen.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileEditScreen.kt index 2c4901672..23c7550d7 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileEditScreen.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileEditScreen.kt @@ -68,6 +68,7 @@ import dev.ipf.whitenoise.android.ui.common.SectionCard import dev.ipf.whitenoise.android.ui.common.StickyFormActionBar import dev.ipf.whitenoise.android.ui.group.ImageSearchSheet import dev.ipf.whitenoise.android.ui.theme.Dimens +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -256,7 +257,7 @@ internal fun ProfileEditScreen( .offset(x = 6.dp, y = 6.dp) .size(36.dp) .clip(CircleShape) - .background(Color.Black.copy(alpha = 0.62f)) + .background(Color.Black.copy(alpha = ScrimAlpha.Heavy)) .clickable( onClickLabel = editPictureLabel, role = Role.Button, diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileSheet.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileSheet.kt index 2e54ba49f..5ec4db1da 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileSheet.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/profile/ProfileSheet.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults @@ -53,7 +52,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.key @@ -91,6 +89,7 @@ import dev.ipf.whitenoise.android.ui.common.SectionCard import dev.ipf.whitenoise.android.ui.common.rememberGroupTitleCopy import dev.ipf.whitenoise.android.ui.group.GroupMemberMenuAction import dev.ipf.whitenoise.android.ui.group.groupMemberMenuActions +import dev.ipf.whitenoise.android.ui.theme.AppDivider import dev.ipf.whitenoise.android.ui.theme.Dimens import dev.ipf.whitenoise.android.ui.theme.amoledSheetContainerColor import dev.ipf.whitenoise.android.ui.theme.amoledSurfaceBorder @@ -402,7 +401,7 @@ internal fun ProfileSheet( onOpen = { onOpenGroup(group, false) }, ) if (index != sharedGroups.lastIndex) { - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)) + AppDivider() } } } @@ -726,7 +725,7 @@ private fun ProfileSheetAdminActions( } } - HorizontalDivider(color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)) + AppDivider() Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/qr/QrScannerSheet.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/qr/QrScannerSheet.kt index 7f1fc9a82..30f0fdee3 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/qr/QrScannerSheet.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/qr/QrScannerSheet.kt @@ -61,6 +61,7 @@ import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.ui.common.lifecycleOwner +import dev.ipf.whitenoise.android.ui.theme.ScrimAlpha import dev.ipf.whitenoise.android.ui.theme.amoledSheetContainerColor import kotlinx.coroutines.launch import java.util.concurrent.Executor @@ -146,7 +147,7 @@ internal fun QrScannerSheetContent( Modifier .padding( 16.dp, - ).background(Color.Black.copy(alpha = 0.55f), RoundedCornerShape(24.dp)) + ).background(Color.Black.copy(alpha = ScrimAlpha.Strong), RoundedCornerShape(24.dp)) .padding(horizontal = 14.dp, vertical = 8.dp), ) } diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/NotificationsScreen.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/NotificationsScreen.kt index 2bdba4fcc..cc927ba8b 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/NotificationsScreen.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/NotificationsScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Forward import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -43,6 +42,7 @@ import androidx.compose.ui.unit.dp import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.state.WhiteNoiseAppState import dev.ipf.whitenoise.android.ui.common.SectionCard +import dev.ipf.whitenoise.android.ui.theme.AppDivider import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -113,7 +113,7 @@ internal fun NotificationsScreen( }, ) } - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { Text(stringResource(R.string.keep_connected), style = MaterialTheme.typography.bodyLarge) @@ -133,7 +133,7 @@ internal fun NotificationsScreen( ) } val nativePushAvailable = appState.isNativePushAvailable() - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { Text(stringResource(R.string.native_push), style = MaterialTheme.typography.bodyLarge) @@ -161,7 +161,7 @@ internal fun NotificationsScreen( }, ) } - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) // Per-type controls (sound, vibration, importance, lockscreen, // DND bypass) live in the OS notification details — Android's // native per-channel UI. We deep-link there instead of diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SecurityPrivacyScreen.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SecurityPrivacyScreen.kt index 743df7478..5fbc9ec2f 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SecurityPrivacyScreen.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SecurityPrivacyScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -39,6 +38,7 @@ import dev.ipf.whitenoise.android.R import dev.ipf.whitenoise.android.state.AppLockDelay import dev.ipf.whitenoise.android.state.WhiteNoiseAppState import dev.ipf.whitenoise.android.ui.common.SectionCard +import dev.ipf.whitenoise.android.ui.theme.AppDivider import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @@ -103,21 +103,21 @@ internal fun SecurityPrivacyScreen( } } } - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsSwitchRow( title = stringResource(R.string.force_incognito_keyboard), subtitle = stringResource(R.string.force_incognito_keyboard_subtitle), checked = appState.forceIncognitoKeyboard, onCheckedChange = { appState.updateForceIncognitoKeyboard(it) }, ) - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsSwitchRow( title = stringResource(R.string.allow_chat_screenshots), subtitle = stringResource(R.string.allow_chat_screenshots_subtitle), checked = !appState.allowChatScreenshotsInChats, onCheckedChange = { appState.updateAllowChatScreenshotsInChats(!it) }, ) - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsSwitchRow( title = stringResource(R.string.telemetry), subtitle = stringResource(R.string.telemetry_settings_subtitle), @@ -135,7 +135,7 @@ internal fun SecurityPrivacyScreen( } }, ) - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsSwitchRow( title = stringResource(R.string.audit_logs), subtitle = stringResource(R.string.audit_logs_settings_subtitle), @@ -153,7 +153,7 @@ internal fun SecurityPrivacyScreen( } }, ) - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) var deleteAuditLogsConfirmOpen by remember { mutableStateOf(false) } Row( Modifier @@ -217,9 +217,9 @@ internal fun SecurityPrivacyScreen( onCheckedChange = { appState.updateDeveloperMode(it) }, ) if (appState.developerMode) { - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsRow(stringResource(R.string.diagnostics), stringResource(R.string.diagnostics_settings_subtitle)) { onOpenDiagnostics() } - HorizontalDivider(Modifier.padding(vertical = 12.dp)) + AppDivider(Modifier.padding(vertical = 12.dp)) SettingsSwitchRow( title = stringResource(R.string.streaming_debug), subtitle = stringResource(R.string.streaming_debug_subtitle), diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt new file mode 100644 index 000000000..0f3d7a335 --- /dev/null +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt @@ -0,0 +1,30 @@ +package dev.ipf.whitenoise.android.ui.theme + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** Semantic opacity tokens for black scrims drawn over media. */ +object ScrimAlpha { + const val Light = 0.35f + const val LightEmphasis = 0.4f + const val Medium = 0.45f + const val MediumEmphasis = 0.5f + const val Strong = 0.55f + const val Gradient = 0.6f + const val Heavy = 0.62f +} + +/** App-wide horizontal separator using Material's divider-specific color role. */ +@Composable +internal fun appDividerColor(): Color = MaterialTheme.colorScheme.outlineVariant + +@Composable +fun AppDivider(modifier: Modifier = Modifier) { + HorizontalDivider( + modifier = modifier, + color = appDividerColor(), + ) +} diff --git a/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt b/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt new file mode 100644 index 000000000..cca706de1 --- /dev/null +++ b/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt @@ -0,0 +1,61 @@ +package dev.ipf.whitenoise.android.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.v2.createComposeRule +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [36]) +class VisualTokensTest { + @get:Rule + val composeRule = createComposeRule() + + @Test + fun scrimAlphaTokensPreserveExistingVisualLevels() { + assertEquals( + listOf(0.35f, 0.4f, 0.45f, 0.5f, 0.55f, 0.6f, 0.62f), + listOf( + ScrimAlpha.Light, + ScrimAlpha.LightEmphasis, + ScrimAlpha.Medium, + ScrimAlpha.MediumEmphasis, + ScrimAlpha.Strong, + ScrimAlpha.Gradient, + ScrimAlpha.Heavy, + ), + ) + } + + @Test + fun appDividerUsesVisibleAmoledOutlineToken() { + var expectedColor = Color.Unspecified + var dividerColor = Color.Unspecified + + composeRule.setContent { + WhiteNoiseTheme(darkTheme = true, amoled = true) { + val outlineVariant = MaterialTheme.colorScheme.outlineVariant + val appDividerColor = appDividerColor() + SideEffect { + expectedColor = outlineVariant + dividerColor = appDividerColor + } + AppDivider() + } + } + + composeRule.runOnIdle { + assertNotEquals(Color.Black, expectedColor) + assertEquals(expectedColor, dividerColor) + } + } +} From 8b2a57187b83dcf4c56c9134d84bfdc5224f5b4a Mon Sep 17 00:00:00 2001 From: agent-p1p Date: Wed, 15 Jul 2026 21:57:12 +0200 Subject: [PATCH 2/2] test(ui): cover markdown divider contrast Render thematic and table dividers in sent and received bubbles across light, dark, and AMOLED screenshot baselines. Remove the self-referential divider color helper so AppDivider wiring is exercised directly by the snapshots. --- .../android/ui/theme/VisualTokens.kt | 6 +- .../MessageBubbleChromeScreenshotTest.kt | 95 ++++++++++++++++++ .../android/ui/theme/VisualTokensTest.kt | 39 ------- ...essage_bubble_markdown_dividers_amoled.png | Bin 0 -> 12394 bytes .../message_bubble_markdown_dividers_dark.png | Bin 0 -> 11446 bytes ...message_bubble_markdown_dividers_light.png | Bin 0 -> 11426 bytes 6 files changed, 96 insertions(+), 44 deletions(-) create mode 100644 app/src/test/snapshots/message_bubble_markdown_dividers_amoled.png create mode 100644 app/src/test/snapshots/message_bubble_markdown_dividers_dark.png create mode 100644 app/src/test/snapshots/message_bubble_markdown_dividers_light.png diff --git a/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt b/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt index 0f3d7a335..4101060fc 100644 --- a/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt +++ b/app/src/main/java/dev/ipf/whitenoise/android/ui/theme/VisualTokens.kt @@ -4,7 +4,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color /** Semantic opacity tokens for black scrims drawn over media. */ object ScrimAlpha { @@ -18,13 +17,10 @@ object ScrimAlpha { } /** App-wide horizontal separator using Material's divider-specific color role. */ -@Composable -internal fun appDividerColor(): Color = MaterialTheme.colorScheme.outlineVariant - @Composable fun AppDivider(modifier: Modifier = Modifier) { HorizontalDivider( modifier = modifier, - color = appDividerColor(), + color = MaterialTheme.colorScheme.outlineVariant, ) } diff --git a/app/src/test/java/dev/ipf/whitenoise/android/ui/screenshot/MessageBubbleChromeScreenshotTest.kt b/app/src/test/java/dev/ipf/whitenoise/android/ui/screenshot/MessageBubbleChromeScreenshotTest.kt index e193fa398..84f134f57 100644 --- a/app/src/test/java/dev/ipf/whitenoise/android/ui/screenshot/MessageBubbleChromeScreenshotTest.kt +++ b/app/src/test/java/dev/ipf/whitenoise/android/ui/screenshot/MessageBubbleChromeScreenshotTest.kt @@ -18,8 +18,14 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.unit.dp import com.github.takahirom.roborazzi.captureRoboImage +import dev.ipf.marmotkit.MarkdownAlignmentFfi +import dev.ipf.marmotkit.MarkdownBlockFfi +import dev.ipf.marmotkit.MarkdownDocumentFfi +import dev.ipf.marmotkit.MarkdownInlineFfi +import dev.ipf.marmotkit.MarkdownTableCellFfi import dev.ipf.whitenoise.android.core.ReplyMediaKind import dev.ipf.whitenoise.android.state.MessageStatus +import dev.ipf.whitenoise.android.ui.MarkdownMessageBody import dev.ipf.whitenoise.android.ui.conversation.messages.MessageInlineFooter import dev.ipf.whitenoise.android.ui.conversation.messages.messageBubbleBorder import dev.ipf.whitenoise.android.ui.conversation.messages.messageBubbleTimestampColor @@ -80,6 +86,30 @@ class MessageBubbleChromeScreenshotTest { .captureRoboImage("src/test/snapshots/message_bubble_chrome_amoled.png") } + @Test + fun bubbleMarkdownDividersLight() { + renderMarkdownDividers(darkTheme = false, amoled = false) + composeRule + .onNodeWithTag(MARKDOWN_TAG) + .captureRoboImage("src/test/snapshots/message_bubble_markdown_dividers_light.png") + } + + @Test + fun bubbleMarkdownDividersDark() { + renderMarkdownDividers(darkTheme = true, amoled = false) + composeRule + .onNodeWithTag(MARKDOWN_TAG) + .captureRoboImage("src/test/snapshots/message_bubble_markdown_dividers_dark.png") + } + + @Test + fun bubbleMarkdownDividersAmoled() { + renderMarkdownDividers(darkTheme = true, amoled = true) + composeRule + .onNodeWithTag(MARKDOWN_TAG) + .captureRoboImage("src/test/snapshots/message_bubble_markdown_dividers_amoled.png") + } + private fun render(darkTheme: Boolean) { composeRule.setContent { WhiteNoiseTheme(darkTheme = darkTheme) { @@ -125,11 +155,76 @@ class MessageBubbleChromeScreenshotTest { } } + private fun renderMarkdownDividers( + darkTheme: Boolean, + amoled: Boolean, + ) { + composeRule.setContent { + WhiteNoiseTheme(darkTheme = darkTheme, amoled = amoled) { + Surface { + Column( + modifier = Modifier.width(360.dp).padding(16.dp).testTag(MARKDOWN_TAG), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + MarkdownDividerBubble(mine = false, amoled = amoled) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + MarkdownDividerBubble(mine = true, amoled = amoled) + } + } + } + } + } + } + private companion object { const val TAG = "bubble-chrome" + const val MARKDOWN_TAG = "bubble-markdown-dividers" + } +} + +@Composable +private fun MarkdownDividerBubble( + mine: Boolean, + amoled: Boolean, +) { + val bubbleColor = + when { + amoled -> Color.Black + mine -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.surfaceVariant + } + val label = if (mine) "Sent" else "Received" + + Surface( + color = bubbleColor, + shape = RoundedCornerShape(18.dp), + border = messageBubbleBorder(highlighted = false, mine = mine), + tonalElevation = if (mine) 1.dp else 0.dp, + ) { + MarkdownMessageBody( + document = markdownDividerDocument(label), + modifier = Modifier.width(260.dp).padding(horizontal = 14.dp, vertical = 10.dp), + ) } } +private fun markdownDividerDocument(label: String): MarkdownDocumentFfi = + MarkdownDocumentFfi( + blocks = + listOf( + MarkdownBlockFfi.Paragraph(listOf(MarkdownInlineFfi.Text("$label markdown rule"))), + MarkdownBlockFfi.ThematicBreak, + MarkdownBlockFfi.Table( + alignments = listOf(MarkdownAlignmentFfi.NONE, MarkdownAlignmentFfi.NONE), + header = listOf(markdownTableCell("Token"), markdownTableCell("Value")), + rows = listOf(listOf(markdownTableCell("Divider"), markdownTableCell("Visible"))), + ), + ), + truncated = false, + ) + +private fun markdownTableCell(text: String): MarkdownTableCellFfi = MarkdownTableCellFfi(listOf(MarkdownInlineFfi.Text(text))) + @Composable private fun DirectionalBubble( text: String, diff --git a/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt b/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt index cca706de1..eacb80117 100644 --- a/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt +++ b/app/src/test/java/dev/ipf/whitenoise/android/ui/theme/VisualTokensTest.kt @@ -1,25 +1,9 @@ package dev.ipf.whitenoise.android.ui.theme -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.test.junit4.v2.createComposeRule import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.annotation.GraphicsMode -@RunWith(RobolectricTestRunner::class) -@GraphicsMode(GraphicsMode.Mode.NATIVE) -@Config(sdk = [36]) class VisualTokensTest { - @get:Rule - val composeRule = createComposeRule() - @Test fun scrimAlphaTokensPreserveExistingVisualLevels() { assertEquals( @@ -35,27 +19,4 @@ class VisualTokensTest { ), ) } - - @Test - fun appDividerUsesVisibleAmoledOutlineToken() { - var expectedColor = Color.Unspecified - var dividerColor = Color.Unspecified - - composeRule.setContent { - WhiteNoiseTheme(darkTheme = true, amoled = true) { - val outlineVariant = MaterialTheme.colorScheme.outlineVariant - val appDividerColor = appDividerColor() - SideEffect { - expectedColor = outlineVariant - dividerColor = appDividerColor - } - AppDivider() - } - } - - composeRule.runOnIdle { - assertNotEquals(Color.Black, expectedColor) - assertEquals(expectedColor, dividerColor) - } - } } diff --git a/app/src/test/snapshots/message_bubble_markdown_dividers_amoled.png b/app/src/test/snapshots/message_bubble_markdown_dividers_amoled.png new file mode 100644 index 0000000000000000000000000000000000000000..64fd131e8fc8f225661cb7cca6befb8e8d385c67 GIT binary patch literal 12394 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 GzyASdciq_l literal 0 HcmV?d00001 diff --git a/app/src/test/snapshots/message_bubble_markdown_dividers_dark.png b/app/src/test/snapshots/message_bubble_markdown_dividers_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..db32b8f0649853e018262f92437c22d3ae90b4f8 GIT binary patch literal 11446 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 literal 0 HcmV?d00001 diff --git a/app/src/test/snapshots/message_bubble_markdown_dividers_light.png b/app/src/test/snapshots/message_bubble_markdown_dividers_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f2af19720c3ed1284c22bd9a68b62c48a1286bef GIT binary patch literal 11426 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 Q?=hCLg1USm#N^%o0gTP{H~;_u literal 0 HcmV?d00001