From faea5e90acd21983fbf9037a72a5887429dc7ff5 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:56:43 +0400 Subject: [PATCH] android: members connected aggregated item; group layout (#2934) --- apps/ios/Shared/Model/ChatModel.swift | 2 +- .../Chat/ChatItem/CIRcvDecryptionError.swift | 21 ++--- apps/ios/SimpleXChat/ChatTypes.swift | 5 -- .../chat/simplex/common/model/ChatModel.kt | 25 ++++++ .../simplex/common/views/chat/ChatView.kt | 88 ++++++++++++++----- .../simplex/common/views/chat/ComposeView.kt | 7 ++ .../common/views/chat/ContextItemView.kt | 34 +++++-- .../simplex/common/views/chat/SendMsgView.kt | 8 +- .../common/views/chat/item/CIEventView.kt | 32 +------ .../views/chat/item/CIRcvDecryptionError.kt | 10 --- .../common/views/chat/item/ChatItemView.kt | 79 ++++++++++++++--- .../common/views/chat/item/DeletedItemView.kt | 3 +- .../common/views/chat/item/FramedItemView.kt | 50 +++++++---- .../views/chat/item/IntegrityErrorItemView.kt | 7 +- .../views/chat/item/MarkedDeletedItemView.kt | 3 +- .../common/views/chat/item/TextItemView.kt | 9 -- .../commonMain/resources/MR/base/strings.xml | 4 + 17 files changed, 252 insertions(+), 135 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 390e25bed..229408a4e 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -487,7 +487,7 @@ final class ChatModel: ObservableObject { guard var i = getChatItemIndex(ci) else { return [] } var ns: [String] = [] while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected { - ns.append(m.chatViewName) + ns.append(m.displayName) i += 1 } return ns diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index f4f8a52eb..2e0a19ead 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -16,7 +16,6 @@ struct CIRcvDecryptionError: View { var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem - var showMember = false @State private var alert: CIRcvDecryptionErrorAlert? enum CIRcvDecryptionErrorAlert: Identifiable { @@ -106,9 +105,6 @@ struct CIRcvDecryptionError: View { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 2) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") - } Text(chatItem.content.text) .foregroundColor(.red) .italic() @@ -137,20 +133,13 @@ struct CIRcvDecryptionError: View { } private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View { - func text() -> Text { - Text(chatItem.content.text) - .foregroundColor(.red) - .italic() - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) - } return ZStack(alignment: .bottomTrailing) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") + text() - } else { - text() - } + Text(chatItem.content.text) + .foregroundColor(.red) + .italic() + + Text(" ") + + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) } .padding(.horizontal, 12) CIMetaView(chatItem: chatItem) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5bdd03ace..de52328ca 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2533,17 +2533,12 @@ public enum CIContent: Decodable, ItemContent { public var showMemberName: Bool { switch self { - case .sndMsgContent: return true case .rcvMsgContent: return true - case .sndDeleted: return true case .rcvDeleted: return true - case .sndCall: return true case .rcvCall: return true case .rcvIntegrityError: return true case .rcvDecryptionError: return true case .rcvGroupInvitation: return true - case .sndChatPreference: return true - case .sndModerated: return true case .rcvModerated: return true case .invalidJSON: return true default: return false 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 9762ec72a..a0c31496e 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 @@ -1385,6 +1385,18 @@ data class ChatItem ( else -> false } + val memberConnected: GroupMember? get() = + when (chatDir) { + is CIDirection.GroupRcv -> when (content) { + is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> chatDir.groupMember + else -> null + } + else -> null + } + else -> null + } + fun memberToModerate(chatInfo: ChatInfo): Pair? { return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { val m = chatInfo.groupInfo.membership @@ -1838,6 +1850,19 @@ sealed class CIContent: ItemContent { is InvalidJSON -> "invalid data" } + val showMemberName: Boolean get() = + when (this) { + is RcvMsgContent -> true + is RcvDeleted -> true + is RcvCall -> true + is RcvIntegrityError -> true + is RcvDecryptionError -> true + is RcvGroupInvitation -> true + is RcvModerated -> true + is InvalidJSON -> true + else -> false + } + companion object { fun featureText(feature: Feature, enabled: String, param: Int?): String = if (feature.hasParam) { 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 749afe8fa..78b379df7 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 @@ -17,11 +17,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.* import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* @@ -718,7 +718,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } ) LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem -> + itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem -> CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view @@ -760,27 +760,73 @@ fun BoxWithConstraintsScope.ChatItemsList( if (chat.chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null - val member = cItem.chatDir.groupMember - val showMember = showMemberImage(member, prevItem) - Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) { - if (showMember) { - Box( - Modifier - .clip(CircleShape) - .clickable { - showMemberInfo(chat.chatInfo.groupInfo, member) - } - ) { - MemberImage(member) + val nextItem = if (i - 1 >= 0) reversedChatItems[i - 1] else null + fun getConnectedMemberNames(): List { + val ns = mutableListOf() + var idx = i + while (idx < reversedChatItems.size) { + val m = reversedChatItems[idx].memberConnected + if (m != null) { + ns.add(m.displayName) + } else { + break + } + idx++ + } + return ns + } + if (cItem.memberConnected != null && nextItem?.memberConnected != null) { + // memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView + Box(Modifier.size(0.dp)) {} + } else { + val member = cItem.chatDir.groupMember + if (showMemberImage(member, prevItem)) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start + ) { + if (cItem.content.showMemberName) { + Text( + member.displayName, + Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + } + Row( + swipeableModifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box( + Modifier + .clip(CircleShape) + .clickable { + showMemberInfo(chat.chatInfo.groupInfo, member) + } + ) { + MemberImage(member) + } + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames) + } + } + } else { + Row( + Modifier + .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) + .then(swipeableModifier) + ) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames) } - Spacer(Modifier.size(4.dp)) - } else { - Spacer(Modifier.size(42.dp)) } - ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } else { - Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) { + Box( + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) + .then(swipeableModifier) + ) { ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } @@ -973,9 +1019,11 @@ fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean { (prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId) } +val MEMBER_IMAGE_SIZE: Dp = 38.dp + @Composable fun MemberImage(member: GroupMember) { - ProfileImage(38.dp, member.memberProfile.image) + ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 9be98525a..234880a41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -16,6 +16,8 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.Indigo +import chat.simplex.common.ui.theme.isSystemInDarkTheme import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -753,6 +755,10 @@ fun ComposeView( } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } + val sendButtonColor = + if (chat.chatInfo.incognito) + if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) + else MaterialTheme.colors.primary SendMsgView( composeState, showVoiceRecordIcon = true, @@ -764,6 +770,7 @@ fun ComposeView( allowVoiceToContact = ::allowVoiceToContact, userIsObserver = userIsObserver.value, userCanSend = userCanSend.value, + sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, sendMessage = { ttl -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index 9a4d4c377..49203c7cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* import chat.simplex.common.model.* @@ -27,6 +29,17 @@ fun ContextItemView( val sent = contextItem.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + + @Composable + fun msgContentView(lines: Int) { + MarkdownText( + contextItem.text, contextItem.formattedText, + maxLines = lines, + linkMode = SimplexLinkMode.DESCRIPTION, + modifier = Modifier.fillMaxWidth(), + ) + } + Row( Modifier .padding(top = 8.dp) @@ -49,12 +62,21 @@ fun ContextItemView( contentDescription = stringResource(MR.strings.icon_descr_context), tint = MaterialTheme.colors.secondary, ) - MarkdownText( - contextItem.text, contextItem.formattedText, - sender = contextItem.memberDisplayName, senderBold = true, maxLines = 3, - linkMode = SimplexLinkMode.DESCRIPTION, - modifier = Modifier.fillMaxWidth(), - ) + val sender = contextItem.memberDisplayName + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + msgContentView(lines = 2) + } + } else { + msgContentView(lines = 3) + } } IconButton(onClick = cancelContextItem) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 0d07fe626..205f18c46 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -41,6 +41,7 @@ fun SendMsgView( allowedVoiceByPrefs: Boolean, userIsObserver: Boolean, userCanSend: Boolean, + sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference? = null, @@ -194,12 +195,12 @@ fun SendMsgView( val menuItems = MenuItems() if (menuItems.isNotEmpty()) { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true } + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) { showDropdown.value = true } DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } } else { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) } } } @@ -449,6 +450,7 @@ private fun SendMsgButton( icon: Painter, sizeDp: Animatable, alpha: Animatable, + sendButtonColor: Color, enabled: Boolean, sendMessage: (Int?) -> Unit, onLongClick: (() -> Unit)? = null @@ -476,7 +478,7 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) + .background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary) .padding(3.dp) ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt index 09d0bd4d0..508116f7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt @@ -8,43 +8,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun CIEventView(ci: ChatItem) { - @Composable - fun chatEventTextView(text: AnnotatedString) { - Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) - } +fun CIEventView(text: AnnotatedString) { Row( Modifier.padding(horizontal = 6.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically ) { - val memberDisplayName = ci.memberDisplayName - if (memberDisplayName != null) { - chatEventTextView( - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(ci)) - ) - } else { - chatEventTextView(chatEventText(ci)) - } + Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) } } - -val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) - -fun chatEventText(ci: ChatItem): AnnotatedString = - buildAnnotatedString { - withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) } - } - @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" @@ -52,8 +28,6 @@ fun chatEventText(ci: ChatItem): AnnotatedString = @Composable fun CIEventViewPreview() { SimpleXTheme { - CIEventView( - ChatItem.getGroupEventSample() - ) + CIEventView(buildAnnotatedString { append("event happened") }) } } 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 2ce52f971..2918d885b 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 @@ -32,7 +32,6 @@ fun CIRcvDecryptionError( syncMemberConnection: (GroupInfo, GroupMember) -> Unit, findModelChat: (String) -> Chat?, findModelMember: (String) -> GroupMember?, - showMember: Boolean ) { LaunchedEffect(Unit) { if (cInfo is ChatInfo.Direct) { @@ -46,7 +45,6 @@ fun CIRcvDecryptionError( fun BasicDecryptionErrorItem() { DecryptionErrorItem( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.decryption_error), @@ -64,7 +62,6 @@ fun CIRcvDecryptionError( if (modelContactStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -78,7 +75,6 @@ fun CIRcvDecryptionError( } else if (!modelContactStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_contact), @@ -103,7 +99,6 @@ fun CIRcvDecryptionError( if (modelMemberStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -117,7 +112,6 @@ fun CIRcvDecryptionError( } else if (!modelMemberStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member), @@ -140,7 +134,6 @@ fun CIRcvDecryptionError( @Composable fun DecryptionErrorItemFixButton( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit, syncSupported: Boolean ) { @@ -159,7 +152,6 @@ fun DecryptionErrorItemFixButton( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) @@ -189,7 +181,6 @@ fun DecryptionErrorItemFixButton( @Composable fun DecryptionErrorItem( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit ) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -204,7 +195,6 @@ fun DecryptionErrorItem( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } }, 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 0597e2d13..2857d6acc 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 @@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight @@ -29,13 +29,22 @@ import kotlinx.datetime.Clock // TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) +val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) + +fun chatEventText(ci: ChatItem): AnnotatedString = + chatEventText(ci.content.text, ci.timestampText) + +fun chatEventText(eventText: String, ts: String): AnnotatedString = + buildAnnotatedString { + withStyle(chatEventStyle) { append("$eventText $ts") } + } + @Composable fun ChatItemView( cInfo: ChatInfo, cItem: ChatItem, composeState: MutableState, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -53,6 +62,7 @@ fun ChatItemView( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, + getConnectedMemberNames: (() -> List)? = null, ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -95,7 +105,8 @@ fun ChatItemView( ReactionIcon(r.reaction.text, fontSize = 12.sp) if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) - Text("${r.totalReacted}", + Text( + "${r.totalReacted}", fontSize = 11.5.sp, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, @@ -116,7 +127,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -246,7 +257,7 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && !revealed.value) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { @@ -265,7 +276,7 @@ fun ChatItemView( } @Composable fun DeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + DeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) @@ -276,9 +287,48 @@ fun ChatItemView( CICallItemView(cInfo, cItem, status, duration, acceptCall) } + fun eventItemViewText(): AnnotatedString { + val memberDisplayName = cItem.memberDisplayName + return if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(cItem)) + } else { + chatEventText(cItem) + } + } + + @Composable fun EventItemView() { + CIEventView(eventItemViewText()) + } + + fun membersConnectedText(): String? { + return if (getConnectedMemberNames != null) { + val ns = getConnectedMemberNames() + when { + ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) + ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) + ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) + else -> null + } + } else { + null + } + } + + fun membersConnectedItemText(): AnnotatedString { + val t = membersConnectedText() + return if (t != null) { + chatEventText(t, cItem.timestampText) + } else { + eventItemViewText() + } + } + @Composable fun ModeratedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage) @@ -292,14 +342,17 @@ fun ChatItemView( is CIContent.RcvDeleted -> DeletedItem() is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) - is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember) - is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember) + is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL) + is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) - is CIContent.RcvGroupEventContent -> CIEventView(cItem) - is CIContent.SndGroupEventContent -> CIEventView(cItem) - is CIContent.RcvConnEventContent -> CIEventView(cItem) - is CIContent.SndConnEventContent -> CIEventView(cItem) + is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText()) + else -> EventItemView() + } + is CIContent.SndGroupEventContent -> EventItemView() + is CIContent.RcvConnEventContent -> EventItemView() + is CIContent.SndConnEventContent -> EventItemView() is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.RcvChatPreference -> { 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 3d8d9f794..2d949e173 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?, showMember: Boolean = false) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sent = ci.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -30,7 +30,6 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), 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 faa7dfdbf..0767862a4 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 @@ -23,6 +23,7 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.base64ToBitmap +import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE import chat.simplex.res.MR import kotlin.math.min @@ -32,7 +33,6 @@ fun FramedItemView( ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, linkMode: SimplexLinkMode, showMenu: MutableState, receiveFile: (Long) -> Unit, @@ -49,17 +49,38 @@ fun FramedItemView( @Composable fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) + @Composable + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { + MarkdownText( + qi.text, + qi.formattedText, + maxLines = lines, + overflow = TextOverflow.Ellipsis, + style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), + linkMode = linkMode + ) + } + @Composable fun ciQuotedMsgView(qi: CIQuote) { Box( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.TopStart ) { - MarkdownText( - qi.text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3, - style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), - linkMode = linkMode - ) + val sender = qi.sender(membership()) + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + ciQuotedMsgTextView(qi, lines = 2) + } + } else { + ciQuotedMsgTextView(qi, lines = 3) + } } } @@ -156,7 +177,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) } } @@ -207,7 +228,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVideo -> { @@ -215,29 +236,29 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } } - else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } } } @@ -253,7 +274,6 @@ fun FramedItemView( fun CIMarkdownText( ci: ChatItem, chatTTL: Int?, - showMember: Boolean, linkMode: SimplexLinkMode, uriHandler: UriHandler?, onLinkLongClick: (link: String) -> Unit = {} @@ -261,7 +281,7 @@ fun CIMarkdownText( Box(Modifier.padding(vertical = 6.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, if (showMember) ci.memberDisplayName else null, + text, if (text.isEmpty()) emptyList() else ci.formattedText, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick ) 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 7cd996ec2..582730b8f 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 @@ -24,8 +24,8 @@ import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable -fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { - CIMsgError(ci, timedMessagesTTL, showMember) { +fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) { + CIMsgError(ci, timedMessagesTTL) { when (msgError) { is MsgErrorType.MsgSkipped -> AlertManager.shared.showAlertMsg( @@ -50,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT } @Composable -fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) { +fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), @@ -63,7 +63,6 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), 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 8ced77289..84675a09b 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 chat.simplex.res.MR import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( @@ -47,7 +47,6 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool private fun MarkedDeletedText(text: String) { Text( buildAnnotatedString { - // appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), 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 24aad7fdd..af7d32d52 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 @@ -25,15 +25,6 @@ import kotlinx.coroutines.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) -fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMemberBold: Boolean) { - if (chatItem.chatDir is CIDirection.GroupRcv) { - val name = chatItem.chatDir.groupMember.memberProfile.displayName - if (groupMemberBold) b.withStyle(boldFont) { append(name) } - else b.append(name) - b.append(": ") - } -} - fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolean) { if (sender != null) { if (senderBold) b.withStyle(boldFont) { append(sender) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index dcc421182..5449dd6a8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1103,6 +1103,10 @@ you left group profile updated + %s and %s connected + %s, %s and %s connected + %s, %s and %d other members connected + changed address for you changing address…