desktop: fix dark layering and reply line in flat layout

This commit is contained in:
sh committed 2026-10-14 07:43:26 +00:00
1 parent 8aa342061c
commit b3146d1e40
4 files changed
+76 -37

No files matched your search

@@ -454,7 +454,7 @@ private fun DesktopScreenLayout(userPickerState: MutableStateFlow<AnimatedViewSt
UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA)
}
}
if (LocalFlatLayout.current) FlatListSurface { StartPart() } else StartPart()
if (LocalFlatLayout.current) FlatSurface(flatListColor()) { StartPart() } else StartPart()
}
Box(Modifier.padding(start = railWidth).widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) {
ModalManager.start.showInView()
@@ -462,7 +462,7 @@ private fun DesktopScreenLayout(userPickerState: MutableStateFlow<AnimatedViewSt
}
Row(Modifier.padding(start = railWidth + DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) {
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
CenterPartOfScreen()
if (LocalFlatLayout.current) FlatSurface(flatChatColor()) { CenterPartOfScreen() } else CenterPartOfScreen()
}
if (ModalManager.end.hasModalsOpen()) {
VerticalDivider()
@@ -2,6 +2,8 @@ package chat.simplex.common.views.chat.item
import chat.simplex.common.views.chat.group.MemberProfileImage
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -221,23 +223,52 @@ fun FramedItemView(
}
}
fun openQuotedItem(qi: CIQuote) {
val itemId = qi.itemId
if (itemId != null) {
if (ci.isReport && chatsCtx.secondaryContextFilter != null) {
scrollToItemId.value = itemId
} else {
scrollToItem(itemId)
}
} else {
scrollToQuotedItemFromItem(ci.id)
}
}
@Composable
fun ciReplyAvatar(qi: CIQuote) {
when (val dir = qi.chatDir) {
is CIDirection.GroupRcv -> 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()
@@ -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<AnimatedViewState>) {
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) {
@@ -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)