android, desktop: add chat message tail and roundness settings (#4958)

* android, desktop: add roundness setting to chat items

* add tail setting

* use shape for clip

* wip tails

* shape style

* show tail only on last msg in group

* roundings

* padding for direct chats

* groups padding

* space between messages in settings preview

* refactor group paddings

* simplify

* simplify

* RcvDeleted handling

* revert uncessary

* import

* always maintain tail position

* rename

* reactions should not move

* short emoji shouldn't have tail

* remove invisible tail for voice without text

* better usage of gutters

* simplify

* rename

* simplify reactions

* linter happy

* exclude moderated items from shape

* uncessary diff

* func position

* fix chat view align on font resize (with image)

* fix tails moving bubble on max width

* fix big group names sometimes changing position

* small refactor

* fix top left corner end position

* rename

* sticky steps

* revert whitespace changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Diogo
2024-10-05 19:02:09 +01:00
committed by GitHub
parent 026a8022e0
commit bb2a6ec65d
9 changed files with 274 additions and 34 deletions

View File

@@ -115,6 +115,9 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced()
ThemesSection(systemDarkTheme)
SectionDividerSpaced()
MessageShapeSection()
SectionDividerSpaced()
ProfileImageSection()

View File

@@ -219,6 +219,8 @@ class AppPreferences {
}, settingsThemes)
val themeOverrides = mkThemeOverridesPreference()
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
val chatItemRoundness = mkFloatPreference(SHARED_PREFS_CHAT_ITEM_ROUNDNESS, 0.75f)
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
@@ -422,6 +424,8 @@ class AppPreferences {
private const val SHARED_PREFS_THEMES_OLD = "Themes"
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
private const val SHARED_PREFS_CHAT_ITEM_ROUNDNESS = "ChatItemRoundness"
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
private const val SHARED_PREFS_FONT_SCALE = "FontScale"
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
@@ -6334,6 +6338,8 @@ data class AppSettings(
var iosCallKitEnabled: Boolean? = null,
var iosCallKitCallsInRecents: Boolean? = null,
var uiProfileImageCornerRadius: Float? = null,
var uiChatItemRoundness: Float? = null,
var uiChatItemTail: Boolean? = null,
var uiColorScheme: String? = null,
var uiDarkColorScheme: String? = null,
var uiCurrentThemeIds: Map<String, String>? = null,
@@ -6366,6 +6372,8 @@ data class AppSettings(
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius }
if (uiChatItemRoundness != def.uiChatItemRoundness) { empty.uiChatItemRoundness = uiChatItemRoundness }
if (uiChatItemTail != def.uiChatItemTail) { empty.uiChatItemTail = uiChatItemTail }
if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme }
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
@@ -6409,6 +6417,8 @@ data class AppSettings(
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) }
uiChatItemRoundness?.let { def.chatItemRoundness.set(it) }
uiChatItemTail?.let { def.chatItemTail.set(it) }
uiColorScheme?.let { def.currentTheme.set(it) }
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
@@ -6442,6 +6452,8 @@ data class AppSettings(
iosCallKitEnabled = true,
iosCallKitCallsInRecents = false,
uiProfileImageCornerRadius = 22.5f,
uiChatItemRoundness = 0.75f,
uiChatItemTail = true,
uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME,
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
uiCurrentThemeIds = null,
@@ -6476,6 +6488,8 @@ data class AppSettings(
iosCallKitEnabled = def.iosCallKitEnabled.get(),
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
uiProfileImageCornerRadius = def.profileImageCornerRadius.get(),
uiChatItemRoundness = def.chatItemRoundness.get(),
uiChatItemTail = def.chatItemTail.get(),
uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME,
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
uiCurrentThemeIds = def.currentThemeIds.get(),

View File

@@ -24,11 +24,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.group.MemberProfileImage
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -75,7 +74,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
Box(
Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp)
Modifier.clipChatItem().background(itemColor).padding(bottom = 3.dp)
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
.onRightClick { showMenu.value = true }
) {
@@ -122,7 +121,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
Box(
Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp)
Modifier.clipChatItem().background(quoteColor).padding(bottom = 3.dp)
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
.onRightClick { showMenu.value = true }
) {

View File

@@ -1036,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) {
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, showTimestamp = itemSeparation.timestamp)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
}
}
@@ -1081,6 +1081,15 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
@Composable
fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
val chatItemTail = remember { appPreferences.chatItemTail.state }
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
}
Box {
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
@@ -1099,7 +1108,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
Column(
Modifier
.padding(top = 8.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.fillMaxWidth()
.then(swipeableModifier),
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -1111,7 +1120,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
Text(
memberNames(member, prevMember, memCount),
Modifier
.padding(start = MEMBER_IMAGE_SIZE + DEFAULT_PADDING_HALF)
.padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
.weight(1f, false),
fontSize = 13.5.sp,
color = MaterialTheme.colors.secondary,
@@ -1119,9 +1128,13 @@ fun BoxWithConstraintsScope.ChatItemsList(
maxLines = 1
)
if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
val chatItemTail = remember { appPreferences.chatItemTail.state }
val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
Text(
member.memberRole.text,
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF),
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
fontSize = 13.5.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.secondary,
@@ -1137,12 +1150,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
}
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() },
horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
}
Box(modifier = Modifier.padding(top = 2.dp)) {
Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
ChatItemViewShortHand(cItem, itemSeparation, range, false)
}
}
@@ -1164,7 +1176,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
Row(
Modifier
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
.then(swipeableOrSelectionModifier)
) {
ChatItemViewShortHand(cItem, itemSeparation, range)
@@ -1178,7 +1191,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
Box(
Modifier
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
.then(if (selectionVisible) Modifier else swipeableModifier)
) {
ChatItemViewShortHand(cItem, itemSeparation, range)
@@ -1190,11 +1204,14 @@ fun BoxWithConstraintsScope.ChatItemsList(
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
Box(
Modifier.padding(
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
)
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
.then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
) {
ChatItemViewShortHand(cItem, itemSeparation, range)
}

View File

@@ -3,13 +3,14 @@ package chat.simplex.common.views.chat.item
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
@@ -26,9 +27,16 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Clock
import kotlin.math.*
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
private val msgRectMaxRadius = 18.dp
private val msgBubbleMaxRadius = msgRectMaxRadius * 1.2f
val msgTailWidthDp = 9.dp
private val msgTailMinHeightDp = msgTailWidthDp * 1.254f // ~56deg
private val msgTailMaxHeightDp = msgTailWidthDp * 1.732f // 60deg
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
fun chatEventText(ci: ChatItem): AnnotatedString =
@@ -74,6 +82,7 @@ fun ChatItemView(
developerTools: Boolean,
showViaProxy: Boolean,
showTimestamp: Boolean,
itemSeparation: ItemSeparation,
preview: Boolean = false,
) {
val uriHandler = LocalUriHandler.current
@@ -100,7 +109,7 @@ fun ChatItemView(
@Composable
fun ChatItemReactions() {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
cItem.reactions.forEach { r ->
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) {
@@ -127,13 +136,13 @@ fun ChatItemView(
Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) {
Column(
Modifier
.clip(RoundedCornerShape(18.dp))
.clipChatItem(cItem, itemSeparation.largeGap, revealed.value)
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick)
.onRightClick { showMenu.value = true },
) {
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, receiveFile, onLinkLongClick, scrollToItem)
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem)
}
fun deleteMessageQuestionText(): String {
@@ -795,6 +804,154 @@ fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Un
}
}
@Composable
fun Modifier.chatItemOffset(cItem: ChatItem, tailVisible: Boolean, inverted: Boolean = false, revealed: Boolean): Modifier {
val chatItemTail = remember { appPreferences.chatItemTail.state }
val style = shapeStyle(cItem, chatItemTail.value, tailVisible, revealed)
val offset = if (style is ShapeStyle.Bubble) {
if (style.tailVisible) {
if (cItem.chatDir.sent) msgTailWidthDp else -msgTailWidthDp
} else {
0.dp
}
} else 0.dp
return this.offset(x = if (inverted) (-1f * offset) else offset)
}
@Composable
fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = false, revealed: Boolean = false): Modifier {
val chatItemRoundness = remember { appPreferences.chatItemRoundness.state }
val chatItemTail = remember { appPreferences.chatItemTail.state }
val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed)
val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f)
val shape = when (style) {
is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness)
}
return this.clip(shape)
}
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ ->
val (msgTailWidth, msgBubbleMaxRadius) = with(density) { Pair(msgTailWidthDp.toPx(), msgBubbleMaxRadius.toPx()) }
val width = if (sent && tailVisible) size.width - msgTailWidth else size.width
val height = size.height
val rxMax = min(msgBubbleMaxRadius, width / 2)
val ryMax = min(msgBubbleMaxRadius, height / 2)
val rx = roundness * rxMax
val ry = roundness * ryMax
val tailHeight = with(density) {
min(
msgTailMinHeightDp.toPx() + roundness * (msgTailMaxHeightDp.toPx() - msgTailMinHeightDp.toPx()),
height / 2
)
}
moveTo(rx, 0f)
lineTo(width - rx, 0f) // Top Line
if (roundness > 0) {
quadraticBezierTo(width, 0f, width, ry) // Top-right corner
}
if (height > 2 * ry) {
lineTo(width, height - ry) // Right side
}
if (roundness > 0) {
quadraticBezierTo(width, height, width - rx, height) // Bottom-right corner
}
if (tailVisible) {
lineTo(0f, height) // Bottom line
if (roundness > 0) {
val d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
val controlPoint = Offset(msgTailWidth, height - tailHeight + d * sqrt(roundness))
quadraticBezierTo(controlPoint.x, controlPoint.y, msgTailWidth, height - tailHeight)
} else {
lineTo(msgTailWidth, height - tailHeight)
}
if (height > ry + tailHeight) {
lineTo(msgTailWidth, ry)
}
} else {
lineTo(rx, height) // Bottom line
if (roundness > 0) {
quadraticBezierTo(0f, height, 0f, height - ry) // Bottom-left corner
}
if (height > 2 * ry) {
lineTo(0f, ry) // Left side
}
}
if (roundness > 0) {
val bubbleInitialX = if (tailVisible) msgTailWidth else 0f
quadraticBezierTo(bubbleInitialX, 0f, bubbleInitialX + rx, 0f) // Top-left corner
}
if (sent) {
val matrix = Matrix()
matrix.scale(-1f, 1f)
this.transform(matrix)
this.translate(Offset(size.width, 0f))
}
}
sealed class ShapeStyle {
data class Bubble(val tailVisible: Boolean, val startPadding: Boolean) : ShapeStyle()
data class RoundRect(val radius: Dp) : ShapeStyle()
}
fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean): ShapeStyle {
if (chatItem == null) {
return ShapeStyle.RoundRect(msgRectMaxRadius)
}
when (chatItem.content) {
is CIContent.SndMsgContent,
is CIContent.RcvMsgContent,
is CIContent.RcvDecryptionError,
is CIContent.SndDeleted,
is CIContent.RcvDeleted,
is CIContent.RcvIntegrityError,
is CIContent.SndModerated,
is CIContent.RcvModerated,
is CIContent.RcvBlocked,
is CIContent.InvalidJSON -> {
if (chatItem.meta.itemDeleted != null && (!revealed || chatItem.isDeletedContent)) {
return ShapeStyle.RoundRect(msgRectMaxRadius)
}
val tail = when (val content = chatItem.content.msgContent) {
is MsgContent.MCImage,
is MsgContent.MCVideo,
is MsgContent.MCVoice -> {
if (content.text.isEmpty()) {
false
} else {
tailVisible
}
}
is MsgContent.MCText -> {
if (isShortEmoji(content.text)) {
false
} else {
tailVisible
}
}
else -> tailVisible
}
return if (tailEnabled) {
ShapeStyle.Bubble(tail, !chatItem.chatDir.sent)
} else {
ShapeStyle.RoundRect(msgRectMaxRadius)
}
}
is CIContent.RcvGroupInvitation,
is CIContent.SndGroupInvitation -> return ShapeStyle.RoundRect(msgRectMaxRadius)
else -> return ShapeStyle.RoundRect(8.dp)
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),
@@ -931,6 +1088,7 @@ fun PreviewChatItemView(
showViaProxy = false,
showTimestamp = true,
preview = true,
itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null)
)
}
@@ -970,7 +1128,8 @@ fun PreviewChatItemViewDeletedContent() {
developerTools = false,
showViaProxy = false,
preview = true,
showTimestamp = true
showTimestamp = true,
itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null)
)
}
}

View File

@@ -2,12 +2,10 @@ package chat.simplex.common.views.chat.item
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
@@ -36,6 +34,7 @@ fun FramedItemView(
showViaProxy: Boolean,
showMenu: MutableState<Boolean>,
showTimestamp: Boolean,
tailVisible: Boolean = false,
receiveFile: (Long) -> Unit,
onLinkLongClick: (link: String) -> Unit = {},
scrollToItem: (Long) -> Unit = {},
@@ -190,7 +189,7 @@ fun FramedItemView(
val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = MaterialTheme.appColors.receivedMessage
Box(Modifier
.clip(RoundedCornerShape(18.dp))
.clipChatItem(ci, tailVisible, revealed = true)
.background(
when {
transparentBackground -> Color.Transparent
@@ -200,7 +199,14 @@ fun FramedItemView(
)) {
var metaColor = MaterialTheme.colors.secondary
Box(contentAlignment = Alignment.BottomEnd) {
Column(Modifier.width(IntrinsicSize.Max)) {
val chatItemTail = remember { appPreferences.chatItemTail.state }
val style = shapeStyle(ci, chatItemTail.value, tailVisible, revealed = true)
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
Column(
Modifier
.width(IntrinsicSize.Max)
.padding(start = if (tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp)
) {
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted != null) {
when (ci.meta.itemDeleted) {
@@ -279,7 +285,13 @@ fun FramedItemView(
}
}
}
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
Box(
Modifier
.padding(
bottom = 6.dp,
end = 12.dp + if (tailRendered && sent) msgTailWidthDp else 0.dp,
)
) {
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}

View File

@@ -35,6 +35,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
import chat.simplex.common.views.chat.item.PreviewChatItemView
import chat.simplex.common.views.chat.item.msgTailWidthDp
import chat.simplex.res.MR
import com.godaddy.android.colorpicker.ClassicColorPicker
import com.godaddy.android.colorpicker.HsvColor
@@ -84,6 +85,31 @@ object AppearanceScope {
}
}
@Composable
fun MessageShapeSection() {
SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) {
Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground)
Spacer(Modifier.width(10.dp))
Slider(
remember { appPreferences.chatItemRoundness.state }.value,
valueRange = 0f..1f,
steps = 20,
onValueChange = {
val diff = it % 0.05f
appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff))
saveThemeToDatabase(null)
},
colors = SliderDefaults.colors(
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
)
)
}
SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail)
}
}
@Composable
fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
@@ -169,13 +195,17 @@ object AppearanceScope {
.padding(DEFAULT_PADDING_HALF)
) {
if (withMessages) {
val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) }
PreviewChatItemView(alice)
PreviewChatItemView(
ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice),
quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text))
)
)
val chatItemTail = remember { appPreferences.chatItemTail.state }
Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = if (chatItemTail.value) Modifier else Modifier.padding(horizontal = msgTailWidthDp)) {
val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) }
PreviewChatItemView(alice)
PreviewChatItemView(
ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice),
quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text))
)
)
}
} else {
Box(Modifier.fillMaxSize())
}

View File

@@ -1196,6 +1196,9 @@
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_profile_images">Profile images</string>
<string name="settings_section_title_message_shape">Message shape</string>
<string name="settings_message_shape_corner">Corner</string>
<string name="settings_message_shape_tail">Tail</string>
<string name="settings_section_title_chat_theme">Chat theme</string>
<string name="settings_section_title_user_theme">Profile theme</string>
<string name="settings_section_title_chat_colors">Chat colors</string>

View File

@@ -62,6 +62,9 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced()
ThemesSection(systemDarkTheme)
SectionDividerSpaced()
MessageShapeSection()
SectionDividerSpaced()
ProfileImageSection()