From 53f0fe9ca477c48b6ddcb26fa6f4ce4a9d5be07b Mon Sep 17 00:00:00 2001 From: Diogo Date: Thu, 26 Sep 2024 20:26:33 +0100 Subject: [PATCH] android, desktop: time based message grouping and day separators (#4914) * android, desktop: message grouping * short format on chat * separator for dates * simplify * show on separator when not current year * default for showing date on markdown text * remove unused code * refactor * refactor * remove default locally * fixed build * fix * show first date in chat * apply padding to selectable area * fix date on chats for previous days * add year formatting * fixed message grouping and time show * remove log * fixed reserved space for meta * align first chat bubble with image * metadata correct space * remove log * simplify item separation logic * cleanuo * icon tweaks * without unneeded element * match ios logic * CIMetaText fix * split selectable area * Revert "split selectable area" This reverts commit 1c6001ba3d936f32f5a72fabfa0ba81d6a7146ce. * reserve space similar to ios * split spacing for chat item selection * less repeated code * format * increase padding --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin --- .../chat/simplex/common/model/ChatModel.kt | 37 +++++-- .../simplex/common/views/chat/ChatView.kt | 99 ++++++++++++++++--- .../common/views/chat/item/CICallItemView.kt | 3 +- .../views/chat/item/CIGroupInvitationView.kt | 20 ++-- .../common/views/chat/item/CIMetaView.kt | 90 ++++++++++++----- .../views/chat/item/CIRcvDecryptionError.kt | 8 +- .../common/views/chat/item/CIVoiceView.kt | 8 +- .../common/views/chat/item/ChatItemView.kt | 23 +++-- .../common/views/chat/item/DeletedItemView.kt | 7 +- .../common/views/chat/item/EmojiItemView.kt | 5 +- .../common/views/chat/item/FramedItemView.kt | 35 ++++--- .../views/chat/item/IntegrityErrorItemView.kt | 11 ++- .../views/chat/item/MarkedDeletedItemView.kt | 7 +- .../common/views/chat/item/TextItemView.kt | 5 +- .../common/views/chatlist/ChatPreviewView.kt | 4 +- .../common/views/helpers/ChatInfoImage.kt | 5 +- 16 files changed, 257 insertions(+), 110 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 8d942222c1..7234209577 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2356,7 +2356,8 @@ data class CIMeta ( val deletable: Boolean, val editable: Boolean ) { - val timestampText: String get() = getTimestampText(itemTs) + val timestampText: String get() = getTimestampText(itemTs, true) + val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now() val isLive: Boolean get() = itemLive == true val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null @@ -2420,7 +2421,18 @@ data class CITimed( val deleteAt: Instant? ) -fun getTimestampText(t: Instant): String { +fun getTimestampDateText(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val time = t.toLocalDateTime(tz).toJavaLocalDateTime() + val weekday = time.format(DateTimeFormatter.ofPattern("EEE")) + val dayMonthYear = time.format(DateTimeFormatter.ofPattern( + if (Clock.System.now().toLocalDateTime(tz).year == time.year) "d MMM" else "d MMM YYYY") + ) + + return "$weekday, $dayMonthYear" +} + +fun getTimestampText(t: Instant, shortFormat: Boolean = false): String { val tz = TimeZone.currentSystemDefault() val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz) val time: LocalDateTime = t.toLocalDateTime(tz) @@ -2428,16 +2440,23 @@ fun getTimestampText(t: Instant): String { val recent = now.date == time.date || (period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 ) val dateFormatter = - if (recent) { + if (recent || shortFormat) { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) } else { + val dayMonthFormat = when (Locale.getDefault().country) { + "US" -> "M/dd" + "DE" -> "dd.MM" + "RU" -> "dd.MM" + else -> "dd/MM" + } + val dayMonthYearFormat = when (Locale.getDefault().country) { + "US" -> "M/dd/yy" + "DE" -> "dd.MM.yy" + "RU" -> "dd.MM.yy" + else -> "dd/MM/yy" + } DateTimeFormatter.ofPattern( - when (Locale.getDefault().country) { - "US" -> "M/dd" - "DE" -> "dd.MM" - "RU" -> "dd.MM" - else -> "dd/MM" - } + if (now.year == time.year) dayMonthFormat else dayMonthYearFormat ) // DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 48387f4abb..1cc81a351f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.CIDirection.GroupRcv import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats @@ -41,11 +42,14 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.datetime.Clock +import kotlinx.datetime.* import java.io.File import java.net.URI +import kotlin.math.abs import kotlin.math.sign +data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?) + @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts @@ -1048,16 +1052,16 @@ fun BoxWithConstraintsScope.ChatItemsList( val revealed = remember { mutableStateOf(false) } @Composable - fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?, fillMaxWidth: Boolean = true) { + fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) { 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) + 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) } } @Composable - fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) { + fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) { if (it == DismissValue.DismissedToStart) { scope.launch { @@ -1078,7 +1082,26 @@ fun BoxWithConstraintsScope.ChatItemsList( swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, ) val sent = cItem.chatDir.sent - Box(Modifier.padding(bottom = 4.dp)) { + + @Composable + fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) { + Box( + modifier = modifier.padding( + bottom = if (itemSeparation.largeGap) { + if (i == 0) { + 8.dp + } else { + 4.dp + } + } else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp + ), + contentAlignment = Alignment.CenterStart + ) { + content() + } + } + + 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 val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp) @@ -1130,7 +1153,7 @@ fun BoxWithConstraintsScope.ChatItemsList( @Composable fun Item() { - Box(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID), contentAlignment = Alignment.CenterStart) { + ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) { androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedChatItem(Modifier, cItem.id, selectedChatItems) } @@ -1139,7 +1162,9 @@ fun BoxWithConstraintsScope.ChatItemsList( Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { MemberImage(member) } - ChatItemViewShortHand(cItem, range, false) + Box(modifier = Modifier.padding(top = 2.dp)) { + ChatItemViewShortHand(cItem, itemSeparation, range, false) + } } } } @@ -1153,7 +1178,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } else { - Box(contentAlignment = Alignment.CenterStart) { + ChatItemBox { AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } @@ -1162,12 +1187,12 @@ fun BoxWithConstraintsScope.ChatItemsList( .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) .then(swipeableOrSelectionModifier) ) { - ChatItemViewShortHand(cItem, range) + ChatItemViewShortHand(cItem, itemSeparation, range) } } } } else { - Box(contentAlignment = Alignment.CenterStart) { + ChatItemBox { AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } @@ -1176,12 +1201,12 @@ fun BoxWithConstraintsScope.ChatItemsList( .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) .then(if (selectionVisible) Modifier else swipeableModifier) ) { - ChatItemViewShortHand(cItem, range) + ChatItemViewShortHand(cItem, itemSeparation, range) } } } } else { // direct message - Box(contentAlignment = Alignment.CenterStart) { + ChatItemBox { AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } @@ -1191,7 +1216,7 @@ fun BoxWithConstraintsScope.ChatItemsList( end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, ).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier) ) { - ChatItemViewShortHand(cItem, range) + ChatItemViewShortHand(cItem, itemSeparation, range) } } } @@ -1210,17 +1235,30 @@ fun BoxWithConstraintsScope.ChatItemsList( // memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView } else { val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory) + + val itemSeparation = getItemSeparation(cItem, nextItem) + val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null + + if (itemSeparation.date != null) { + DateSeparator(itemSeparation.date) + } + val range = chatViewItemsRange(currIndex, prevHidden) if (revealed.value && range != null) { reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] - ChatItemView(ci, null, prev) + ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) } } else { - ChatItemView(cItem, range, prevItem) + ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) + } + + if (i == reversedChatItems.lastIndex) { + DateSeparator(cItem.meta.itemTs) } } + if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { scope.launch { @@ -1424,7 +1462,7 @@ private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean = else -> false } -val MEMBER_IMAGE_SIZE: Dp = 38.dp +val MEMBER_IMAGE_SIZE: Dp = 37.dp @Composable fun MemberImage(member: GroupMember) { @@ -1518,6 +1556,18 @@ private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @C } } +@Composable +private fun DateSeparator(date: Instant) { + Text( + text = getTimestampDateText(date), + Modifier.padding(DEFAULT_PADDING).fillMaxWidth(), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) +} + val chatViewScrollState = MutableStateFlow(false) fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { @@ -1891,6 +1941,23 @@ private fun handleForwardConfirmation( ) } +private fun getItemSeparation(chatItem: ChatItem, nextItem: ChatItem?): ItemSeparation { + if (nextItem == null) { + return ItemSeparation(timestamp = true, largeGap = true, date = null) + } + + val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) { + chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId + } else chatItem.chatDir.sent == nextItem.chatDir.sent + val largeGap = !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) + + return ItemSeparation( + timestamp = largeGap || nextItem.meta.timestampText != chatItem.meta.timestampText, + largeGap = largeGap, + date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(nextItem.meta.itemTs)) null else nextItem.meta.itemTs + ) +} + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt index 74c6e38566..744bcf7b66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt @@ -20,6 +20,7 @@ fun CICallItemView( cItem: ChatItem, status: CICallStatus, duration: Int, + showTimestamp: Boolean, acceptCall: (Contact) -> Unit, timedMessagesTTL: Int? ) { @@ -47,7 +48,7 @@ fun CICallItemView( CICallStatus.Error -> {} } - CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) + CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index 577327c159..2bcbbe29e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -26,6 +26,7 @@ fun CIGroupInvitationView( ci: ChatItem, groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole, + showTimestamp: Boolean, chatIncognito: Boolean = false, joinGroup: (Long, () -> Unit) -> Unit, timedMessagesTTL: Int? @@ -118,7 +119,7 @@ fun CIGroupInvitationView( Text( buildAnnotatedString { append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } }, color = if (inProgress.value) MaterialTheme.colors.secondary @@ -129,7 +130,7 @@ fun CIGroupInvitationView( Text( buildAnnotatedString { append(groupInvitationStr()) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } } ) } @@ -145,7 +146,7 @@ fun CIGroupInvitationView( } } - CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) + CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp) } } } @@ -162,7 +163,8 @@ fun PendingCIGroupInvitationViewPreview() { groupInvitation = CIGroupInvitation.getSample(), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null + timedMessagesTTL = null, + showTimestamp = true, ) } } @@ -179,8 +181,9 @@ fun CIGroupInvitationViewAcceptedPreview() { groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null - ) + timedMessagesTTL = null, + showTimestamp = true, + ) } } @@ -196,7 +199,8 @@ fun CIGroupInvitationViewLongNamePreview() { ), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null - ) + timedMessagesTTL = null, + showTimestamp = true, + ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index def3b14ebc..68077d31f8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -35,7 +35,8 @@ fun CIMetaView( }, showStatus: Boolean = true, showEdited: Boolean = true, - showViaProxy: Boolean + showTimestamp: Boolean, + showViaProxy: Boolean, ) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { @@ -54,7 +55,8 @@ fun CIMetaView( paleMetaColor, showStatus = showStatus, showEdited = showEdited, - showViaProxy = showViaProxy + showViaProxy = showViaProxy, + showTimestamp = showTimestamp ) } } @@ -70,11 +72,11 @@ private fun CIMetaText( paleColor: Color, showStatus: Boolean = true, showEdited: Boolean = true, - showViaProxy: Boolean + showTimestamp: Boolean, + showViaProxy: Boolean, ) { if (showEdited && meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) - Spacer(Modifier.width(3.dp)) } if (meta.disappearing) { StatusIconText(painterResource(MR.images.ic_timer), color) @@ -82,12 +84,13 @@ private fun CIMetaText( if (ttl != chatTTL) { Text(shortTimeText(ttl), color = color, fontSize = 12.sp) } - Spacer(Modifier.width(4.dp)) } if (showViaProxy && meta.sentViaProxy == true) { + Spacer(Modifier.width(4.dp)) Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary) } if (showStatus) { + Spacer(Modifier.width(4.dp)) val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) if (statusIcon != null) { val (icon, statusColor) = statusIcon @@ -96,17 +99,19 @@ private fun CIMetaText( } else { StatusIconText(painterResource(icon), statusColor) } - Spacer(Modifier.width(4.dp)) } else if (!meta.disappearing) { StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) - Spacer(Modifier.width(4.dp)) } } if (encrypted != null) { - StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) Spacer(Modifier.width(4.dp)) + StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) + } + + if (showTimestamp) { + Spacer(Modifier.width(4.dp)) + Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } - Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } // the conditions in this function should match CIMetaText @@ -117,28 +122,56 @@ fun reserveSpaceForMeta( secondaryColor: Color, showStatus: Boolean = true, showEdited: Boolean = true, - showViaProxy: Boolean = false + showViaProxy: Boolean = false, + showTimestamp: Boolean ): String { val iconSpace = " " - var res = "" - if (showEdited && meta.itemEdited) res += iconSpace + val whiteSpace = " " + var res = iconSpace + var space: String? = null + + fun appendSpace() { + if (space != null) { + res += space + space = null + } + } + + if (showEdited && meta.itemEdited) { + res += iconSpace + } if (meta.itemTimed != null) { res += iconSpace val ttl = meta.itemTimed.ttl if (ttl != chatTTL) { res += shortTimeText(ttl) } + space = whiteSpace } if (showViaProxy && meta.sentViaProxy == true) { + appendSpace() res += iconSpace } - if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) { - res += iconSpace + if (showStatus) { + appendSpace() + if (meta.statusIcon(secondaryColor) != null) { + res += iconSpace + } else if (!meta.disappearing) { + res += iconSpace + } + space = whiteSpace } + if (encrypted != null) { + appendSpace() res += iconSpace + space = whiteSpace } - return res + meta.timestampText + if (showTimestamp) { + appendSpace() + res += meta.timestampText + } + return res } @Composable @@ -154,7 +187,8 @@ fun PreviewCIMetaView() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -167,7 +201,8 @@ fun PreviewCIMetaViewUnread() { status = CIStatus.RcvNew() ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -180,7 +215,8 @@ fun PreviewCIMetaViewSendFailed() { status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX")) ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -192,7 +228,8 @@ fun PreviewCIMetaViewSendNoAuth() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -204,7 +241,8 @@ fun PreviewCIMetaViewSendSent() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -217,7 +255,8 @@ fun PreviewCIMetaViewEdited() { itemEdited = true ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -231,7 +270,8 @@ fun PreviewCIMetaViewEditedUnread() { status= CIStatus.RcvNew() ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -245,7 +285,8 @@ fun PreviewCIMetaViewEditedSent() { status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } @@ -255,6 +296,7 @@ fun PreviewCIMetaViewDeletedContent() { CIMetaView( chatItem = ChatItem.getDeletedContentSampleData(), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index dd0e9cf1a2..d58fd7553f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -169,14 +169,14 @@ fun DecryptionErrorItemFixButton( Text( buildAnnotatedString { append(generalGetString(MR.strings.fix_connection)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) } withStyle(reserveTimestampStyle) { append(" ") } // for icon }, color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } - CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true) } } } @@ -201,11 +201,11 @@ fun DecryptionErrorItem( Text( buildAnnotatedString { withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) - CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 5ae46ef4e7..4aedcc013a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -38,6 +38,7 @@ fun CIVoiceView( ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, + showTimestamp: Boolean, smallView: Boolean = false, longClick: () -> Unit, receiveFile: (Long) -> Unit, @@ -86,7 +87,7 @@ fun CIVoiceView( durationText(time / 1000) } } - VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) { + VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) { AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) } if (smallView) { @@ -120,6 +121,7 @@ private fun VoiceLayout( hasText: Boolean, timedMessagesTTL: Int?, showViaProxy: Boolean, + showTimestamp: Boolean, sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, @@ -200,7 +202,7 @@ private fun VoiceLayout( VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) } Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) { - CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -215,7 +217,7 @@ private fun VoiceLayout( } } Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) { - CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index df30e85161..6e59e38d02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -73,6 +73,7 @@ fun ChatItemView( showItemDetails: (ChatInfo, ChatItem) -> Unit, developerTools: Boolean, showViaProxy: Boolean, + showTimestamp: Boolean, preview: Boolean = false, ) { val uriHandler = LocalUriHandler.current @@ -132,7 +133,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -355,14 +356,14 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) + EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) } else { framedItemView() } @@ -374,7 +375,7 @@ fun ChatItemView( } @Composable fun LegacyDeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) + DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) @@ -386,7 +387,7 @@ fun ChatItemView( } @Composable fun CallItem(status: CICallStatus, duration: Int) { - CICallItemView(cInfo, cItem, status, duration, acceptCall, cInfo.timedMessagesTTL) + CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL) DeleteItemMenu() } @@ -431,7 +432,7 @@ fun ChatItemView( @Composable fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) @@ -474,7 +475,7 @@ fun ChatItemView( is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) is CIContent.RcvIntegrityError -> if (developerTools) { - IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL) + IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL) DeleteItemMenu() } else { Box(Modifier.size(0.dp)) {} @@ -484,11 +485,11 @@ fun ChatItemView( DeleteItemMenu() } is CIContent.RcvGroupInvitation -> { - CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL) + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) DeleteItemMenu() } is CIContent.SndGroupInvitation -> { - CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL) + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) DeleteItemMenu() } is CIContent.RcvDirectEventContent -> { @@ -928,6 +929,7 @@ fun PreviewChatItemView( showItemDetails = { _, _ -> }, developerTools = false, showViaProxy = false, + showTimestamp = true, preview = true, ) } @@ -968,6 +970,7 @@ fun PreviewChatItemViewDeletedContent() { developerTools = false, showViaProxy = false, preview = true, + showTimestamp = true ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 9b7db099b6..17245c4e75 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) { val sent = ci.chatDir.sent val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage @@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -51,7 +51,8 @@ fun PreviewDeletedItemView() { DeletedItemView( ChatItem.getDeletedContentSampleData(), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt index 4969eccbb6..7aca0466f9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt @@ -12,18 +12,19 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MREmojiChar import chat.simplex.common.ui.theme.EmojiFont +import java.sql.Timestamp val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont) val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) @Composable -fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { +fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) { Column( Modifier.padding(vertical = 8.dp, horizontal = 12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(chatItem.content.text) - CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy) + CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index ddcf7c340b..1542012136 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -35,6 +35,7 @@ fun FramedItemView( linkMode: SimplexLinkMode, showViaProxy: Boolean, showMenu: MutableState, + showTimestamp: Boolean, receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, @@ -47,7 +48,7 @@ fun FramedItemView( } @Composable - fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) { MarkdownText( qi.text, qi.formattedText, @@ -56,7 +57,8 @@ fun FramedItemView( overflow = TextOverflow.Ellipsis, style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), linkMode = linkMode, - uriHandler = if (appPlatform.isDesktop) uriHandler else null + uriHandler = if (appPlatform.isDesktop) uriHandler else null, + showTimestamp = showTimestamp ) } @@ -76,10 +78,10 @@ fun FramedItemView( style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary), maxLines = 1 ) - ciQuotedMsgTextView(qi, lines = 2) + ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp) } } else { - ciQuotedMsgTextView(qi, lines = 3) + ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp) } } } @@ -178,7 +180,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } @@ -242,7 +244,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVideo -> { @@ -250,35 +252,35 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVoice -> { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } - else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } } Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { - CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy) + CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -291,14 +293,15 @@ fun CIMarkdownText( linkMode: SimplexLinkMode, uriHandler: UriHandler?, onLinkLongClick: (link: String) -> Unit = {}, - showViaProxy: Boolean + showViaProxy: Boolean, + showTimestamp: Boolean, ) { - Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { + Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, - uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy + uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index dc585358c4..d528396193 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -23,8 +23,8 @@ import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable -fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) { - CIMsgError(ci, timedMessagesTTL) { +fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) { + CIMsgError(ci, showTimestamp, timedMessagesTTL) { when (msgError) { is MsgErrorType.MsgSkipped -> AlertManager.shared.showAlertMsg( @@ -49,7 +49,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT } @Composable -fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { +fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onClick: () -> Unit) { val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), @@ -68,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL, showViaProxy = false) + CIMetaView(ci, timedMessagesTTL, showViaProxy = false, showTimestamp = showTimestamp) } } } @@ -83,7 +83,8 @@ fun IntegrityErrorItemViewPreview() { IntegrityErrorItemView( MsgErrorType.MsgBadHash(), ChatItem.getDeletedContentSampleData(), - null + showTimestamp = true, + null, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 5b5438d76f..ea71895ce5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState, showViaProxy: Boolean) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState, showViaProxy: Boolean, showTimestamp: Boolean) { val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage Surface( @@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl Box(Modifier.weight(1f, false)) { MergedMarkedDeletedText(ci, revealed) } - CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -113,7 +113,8 @@ fun PreviewMarkedDeletedItemView() { DeletedItemView( ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), null, - showViaProxy = false + showViaProxy = false, + showTimestamp = true ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index c0e222d7d1..434cde608a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -70,7 +70,8 @@ fun MarkdownText ( linkMode: SimplexLinkMode, inlineContent: Pair Unit, Map>? = null, onLinkLongClick: (link: String) -> Unit = {}, - showViaProxy: Boolean = false + showViaProxy: Boolean = false, + showTimestamp: Boolean = true ) { val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr @@ -78,7 +79,7 @@ fun MarkdownText ( val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy) + reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { " " } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 0edaf89974..d63e47bcdd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -256,7 +256,7 @@ fun ChatPreviewView( } } is MsgContent.MCVoice -> SmallContentPreviewVoice() { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) { + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, smallView = true, longClick = {}) { val user = chatModel.currentUser.value ?: return@CIVoiceView withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } } @@ -332,7 +332,7 @@ fun ChatPreviewView( chatPreviewTitle() } Spacer(Modifier.width(8.sp.toDp())) - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) + val ts = getTimestampText(chat.chatItems.lastOrNull()?.meta?.itemTs ?: chat.chatInfo.chatTs) ChatListTimestampView(ts) } Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index 1289687601..d338c57e61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -137,9 +137,10 @@ fun ProfileImageForActiveCall( size: Dp, image: String? = null, color: Color = MaterialTheme.colors.secondaryVariant, -) { + backgroundColor: Color? = null, + ) { if (image == null) { - Box(Modifier.requiredSize(size).clip(CircleShape)) { + Box(Modifier.requiredSize(size).clip(CircleShape).then(if (backgroundColor != null) Modifier.background(backgroundColor) else Modifier)) { Icon( AccountCircleFilled, contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),