From b3146d1e40e581d67bfe81bfa8ce50cc4feb1b68 Mon Sep 17 00:00:00 2001 From: sh Date: Tue, 13 Oct 2026 14:43:01 +0000 Subject: [PATCH] desktop: fix dark layering and reply line in flat layout --- .../kotlin/chat/simplex/common/App.kt | 4 +- .../common/views/chat/item/FramedItemView.kt | 68 +++++++++++++------ .../common/views/chatlist/DesktopRail.kt | 37 ++++++---- .../chatlist/ChatListNavLinkView.desktop.kt | 4 +- 4 files changed, 76 insertions(+), 37 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index a9caa2f04b..2b519c80b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -454,7 +454,7 @@ private fun DesktopScreenLayout(userPickerState: MutableStateFlow MemberProfileImage(FLAT_REPLY_AVATAR_SIZE, dir.groupMember) + is CIDirection.GroupSnd -> membership()?.let { MemberProfileImage(FLAT_REPLY_AVATAR_SIZE, it) } + is CIDirection.ChannelRcv, + is CIDirection.DirectRcv -> ChatInfoImage(chatInfo, FLAT_REPLY_AVATAR_SIZE) + is CIDirection.DirectSnd, + is CIDirection.LocalSnd, + is CIDirection.LocalRcv -> ProfileImage(FLAT_REPLY_AVATAR_SIZE, chatModel.currentUser.value?.image) + null -> {} + } + } + @Composable fun ciReplyLine(qi: CIQuote) { + val interactionSource = remember { MutableInteractionSource() } + val color = if (interactionSource.collectIsHoveredAsState().value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary Row( - Modifier.padding(start = FLAT_TEXT_START_PADDING, top = FLAT_TEXT_VERTICAL_PADDING, end = 12.dp), + Modifier + .padding(start = FLAT_TEXT_START_PADDING, top = FLAT_TEXT_VERTICAL_PADDING, end = 12.dp) + .combinedClickable(interactionSource = interactionSource, indication = null, onLongClick = { showMenu.value = true }, onClick = { openQuotedItem(qi) }) + .onRightClick { showMenu.value = true }, horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(painterResource(MR.images.ic_reply), null, Modifier.size(FLAT_REPLY_ICON_SIZE), tint = MaterialTheme.colors.secondary) - val quotedDir = qi.chatDir - if (quotedDir is CIDirection.GroupRcv) { - MemberProfileImage(FLAT_REPLY_AVATAR_SIZE, quotedDir.groupMember) - } + Icon(painterResource(MR.images.ic_reply), null, Modifier.size(FLAT_REPLY_ICON_SIZE), tint = color) + ciReplyAvatar(qi) val sender = qi.sender(membership()) if (sender != null) { - Text(sender, fontSize = FLAT_REPLY_FONT_SIZE, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colors.secondary, maxLines = 1) + Text(sender, fontSize = FLAT_REPLY_FONT_SIZE, fontWeight = FontWeight.SemiBold, color = color, maxLines = 1) } - val replyStyle = TextStyle(fontSize = FLAT_REPLY_FONT_SIZE, color = MaterialTheme.colors.secondary) + val replyStyle = TextStyle(fontSize = FLAT_REPLY_FONT_SIZE, color = color) Box(Modifier.weight(1f, fill = false)) { when { qi.text.isNotEmpty() -> ciQuotedMsgTextView(qi, lines = 1, showTimestamp = false, style = replyStyle) @@ -263,7 +294,7 @@ fun FramedItemView( val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage - Box(Modifier + Column(Modifier .clipChatItem(ci, tailVisible, revealed = true) .background( when { @@ -273,6 +304,9 @@ fun FramedItemView( } )) { var metaColor = MaterialTheme.colors.secondary + if (flat && ci.quotedItem != null) { + ciReplyLine(ci.quotedItem) + } Box(contentAlignment = Alignment.BottomEnd) { val style = shapeStyle(ci, chatItemTailEnabled(), tailVisible, true) val tailRendered = style is ShapeStyle.Bubble && style.tailVisible @@ -319,27 +353,17 @@ fun FramedItemView( FramedItemHeader(stringResource(MR.strings.live), false) } } - if (ci.quotedItem != null) { + if (ci.quotedItem != null && !flat) { Column( Modifier .combinedClickable( onLongClick = { showMenu.value = true }, - onClick = { - if (ci.quotedItem.itemId != null) { - if (ci.isReport && chatsCtx.secondaryContextFilter != null) { - scrollToItemId.value = ci.quotedItem.itemId - } else { - scrollToItem(ci.quotedItem.itemId) - } - } else { - scrollToQuotedItemFromItem(ci.id) - } - } + onClick = { openQuotedItem(ci.quotedItem) } ) .onRightClick { showMenu.value = true } ) { Header() - if (flat) ciReplyLine(ci.quotedItem) else ciQuoteView(ci.quotedItem) + ciQuoteView(ci.quotedItem) } } else { Header() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt index 4c959db08f..8a1428e2ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/DesktopRail.kt @@ -40,31 +40,43 @@ private val RAIL_ACTIVE_MARK_WIDTH = 4.dp private val RAIL_ACTIVE_BACKGROUND_WIDTH = 40.dp private val RAIL_ACTIVE_BACKGROUND_HEIGHT = 32.dp private val RAIL_UNREAD_DOT_SIZE = 6.dp -private const val RAIL_ACTIVE_BACKGROUND_ALPHA = 0.12f +private const val RAIL_ACTIVE_BACKGROUND_ALPHA = 0.1f private val RAIL_ITEM_INSET = 6.dp private const val RAIL_TILE_CORNER_PERCENT = 30 private const val RAIL_TILE_CIRCLE_PERCENT = 50 private const val RAIL_TILE_ALPHA = 0.15f private const val RAIL_MARK_ACTIVE_FRACTION = 0.8f private const val RAIL_MARK_HOVER_FRACTION = 0.4f -// background darkening of the rail and the chat list; dark themes need more for a visible step +// surfaces step from the darkest rail to the lightest chat: light themes darken the rail and the list, +// dark themes darken the rail and lift the chat, since their background is already near black private const val FLAT_RAIL_SHADE_LIGHT = 0.07f -private const val FLAT_RAIL_SHADE_DARK = 0.4f private const val FLAT_LIST_SHADE_LIGHT = 0.035f -private const val FLAT_LIST_SHADE_DARK = 0.2f +private const val FLAT_RAIL_SHADE_DARK = 0.5f +private const val FLAT_CHAT_LIFT_DARK = 0.045f +// raises placeholder avatar contrast on the shaded surfaces while keeping the theme color +private const val FLAT_PLACEHOLDER_CONTRAST = 0.15f @Composable -fun flatRailColor(): Color = flatShade(if (MaterialTheme.colors.isLight) FLAT_RAIL_SHADE_LIGHT else FLAT_RAIL_SHADE_DARK) +fun flatRailColor(): Color = + MaterialTheme.colors.background.mixWith(Color.Black, 1 - if (MaterialTheme.colors.isLight) FLAT_RAIL_SHADE_LIGHT else FLAT_RAIL_SHADE_DARK) @Composable -fun flatListColor(): Color = flatShade(if (MaterialTheme.colors.isLight) FLAT_LIST_SHADE_LIGHT else FLAT_LIST_SHADE_DARK) +fun flatListColor(): Color = + if (MaterialTheme.colors.isLight) MaterialTheme.colors.background.mixWith(Color.Black, 1 - FLAT_LIST_SHADE_LIGHT) else MaterialTheme.colors.background @Composable -private fun flatShade(amount: Float): Color = MaterialTheme.colors.background.mixWith(Color.Black, 1 - amount) +fun flatChatColor(): Color = + if (MaterialTheme.colors.isLight) MaterialTheme.colors.background else MaterialTheme.colors.background.mixWith(Color.White, 1 - FLAT_CHAT_LIFT_DARK) @Composable -fun FlatListSurface(content: @Composable () -> Unit) { - MaterialTheme(colors = MaterialTheme.colors.copy(background = flatListColor()), typography = MaterialTheme.typography, shapes = MaterialTheme.shapes, content = content) +fun FlatSurface(background: Color, content: @Composable () -> Unit) { + val colors = MaterialTheme.colors + MaterialTheme( + colors = colors.copy(background = background, secondaryVariant = colors.secondaryVariant.mixWith(colors.onBackground, 1 - FLAT_PLACEHOLDER_CONTRAST)), + typography = MaterialTheme.typography, + shapes = MaterialTheme.shapes, + content = content + ) } // Spec: spec/client/chat-list.md#DesktopRail @@ -130,7 +142,8 @@ private fun RailUsers(userPickerState: MutableStateFlow) { else -> 0.dp } ) - val corner by animateIntAsState(if (user.activeUser || hovered) RAIL_TILE_CORNER_PERCENT else RAIL_TILE_CIRCLE_PERCENT) + val baseCorner = remember { appPreferences.profileImageCornerRadius.state }.value.toInt().coerceIn(0, RAIL_TILE_CIRCLE_PERCENT) + val corner by animateIntAsState(if (user.activeUser || hovered) minOf(baseCorner, RAIL_TILE_CORNER_PERCENT) else baseCorner) Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Box( Modifier @@ -238,7 +251,7 @@ private fun RailItem( enabled: Boolean = true, onClick: () -> Unit ) { - val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + val color = if (active) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary Column( Modifier .fillMaxWidth() @@ -252,7 +265,7 @@ private fun RailItem( Box( Modifier .size(RAIL_ACTIVE_BACKGROUND_WIDTH * fontSizeSqrtMultiplier, RAIL_ACTIVE_BACKGROUND_HEIGHT * fontSizeSqrtMultiplier) - .background(if (active) MaterialTheme.colors.primary.copy(alpha = RAIL_ACTIVE_BACKGROUND_ALPHA) else Color.Transparent, RoundedCornerShape(DEFAULT_PADDING_HALF)), + .background(if (active) MaterialTheme.colors.onBackground.copy(alpha = RAIL_ACTIVE_BACKGROUND_ALPHA) else Color.Transparent, RoundedCornerShape(DEFAULT_PADDING_HALF)), contentAlignment = Alignment.Center ) { if (emoji != null) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index c96ee987c6..25a72a4b66 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -30,6 +30,8 @@ object NoIndication : IndicationNodeFactory { private val FLAT_ROW_INSET = 6.dp private val FLAT_ROW_GAP = 1.dp private val FLAT_ROW_CORNER = 6.dp +// twice the hover tint, so the selected chat stands out from the hovered one +private const val FLAT_SELECTED_ALPHA = 0.1f @Composable actual fun ChatListNavLinkLayout( @@ -59,7 +61,7 @@ actual fun ChatListNavLinkLayout( chatLinkPreview() } if (selectedChat.value) { - Box(Modifier.matchParentSize().background(MaterialTheme.colors.onBackground.copy(0.05f))) + Box(Modifier.matchParentSize().background(MaterialTheme.colors.onBackground.copy(if (flat) FLAT_SELECTED_ALPHA else 0.05f))) } if (dropdownMenuItems != null) { DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)