From 43ceb184c4493d7585f5227657876c253ea6d93e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 13 Jul 2023 11:10:34 +0400 Subject: [PATCH] android: add quoted message to chat item info (#2688) --- .../java/chat/simplex/app/model/ChatModel.kt | 2 +- .../app/views/chat/ChatItemInfoView.kt | 214 +++++++++++++++--- .../commonMain/resources/MR/base/strings.xml | 5 +- .../resources/MR/images/ic_history.svg | 1 + 4 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt index 0851f32e28..5221572876 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1832,7 +1832,7 @@ class CIQuote ( fun sender(membership: GroupMember?): String? = when (chatDir) { is CIDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun) is CIDirection.DirectRcv -> null - is CIDirection.GroupSnd -> membership?.displayName + is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun) is CIDirection.GroupRcv -> chatDir.groupMember.displayName null -> null } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt index 6eebb7901d..4d7cc4b8ea 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt @@ -7,18 +7,18 @@ import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +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.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.CurrentColors import chat.simplex.app.ui.theme.DEFAULT_PADDING @@ -26,34 +26,42 @@ import chat.simplex.app.views.chat.item.ItemAction import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.app.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource + +enum class CIInfoTab { + History, Quote +} @Composable fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { val sent = ci.chatDir.sent val appColors = CurrentColors.collectAsState().value.appColors - val itemColor = if (sent) appColors.sentMessage else appColors.receivedMessage val uriHandler = LocalUriHandler.current + val selection = remember { mutableStateOf(CIInfoTab.History) } + + @Composable + fun TextBubble(text: String, formattedText: List?, sender: String?, showMenu: MutableState) { + if (text != "") { + MarkdownText( + text, if (text.isEmpty()) emptyList() else formattedText, + sender = sender, + senderBold = true, + linkMode = SimplexLinkMode.DESCRIPTION, uriHandler = uriHandler, + onLinkLongClick = { showMenu.value = true } + ) + } else { + Text( + generalGetString(MR.strings.item_info_no_text), + style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp, fontStyle = FontStyle.Italic) + ) + } + } @Composable fun ItemVersionView(ciVersion: ChatItemVersion, current: Boolean) { val showMenu = remember { mutableStateOf(false) } val text = ciVersion.msgContent.text - - @Composable - fun VersionText() { - if (text != "") { - MarkdownText( - text, if (text.isEmpty()) emptyList() else ciVersion.formattedText, - linkMode = SimplexLinkMode.DESCRIPTION, uriHandler = uriHandler, - onLinkLongClick = { showMenu.value = true } - ) - } else { - Text( - generalGetString(MR.strings.item_info_no_text), - style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp, fontStyle = FontStyle.Italic) - ) - } - } + val itemColor = if (sent) appColors.sentMessage else appColors.receivedMessage Column { Box( @@ -61,7 +69,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { - VersionText() + TextBubble(text, ciVersion.formattedText, sender = null, showMenu) } } Row(Modifier.padding(start = 12.dp, top = 3.dp, bottom = 16.dp)) { @@ -94,7 +102,46 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { } } - Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + @Composable + fun QuotedMsgView(qi: CIQuote) { + val showMenu = remember { mutableStateOf(false) } + val text = qi.text + val quoteColor = if (qi.chatDir?.sent == true) appColors.sentMessage else appColors.receivedMessage + + Column { + Box( + Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp) + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) + ) { + Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { + TextBubble(text, qi.formattedText, sender = qi.sender(null), showMenu) + } + } + Row(Modifier.padding(start = 12.dp, top = 3.dp, bottom = 16.dp)) { + Text( + localTimestamp(qi.sentAt), + fontSize = 12.sp, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(end = 6.dp) + ) + } + if (text != "") { + DefaultDropdownMenu(showMenu) { + ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { + shareText(text) + showMenu.value = false + }) + ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { + copyText(text) + showMenu.value = false + }) + } + } + } + } + + @Composable + fun Details() { AppBarTitle(stringResource(if (sent) MR.strings.sent_message else MR.strings.received_message)) SectionView { InfoRow(stringResource(MR.strings.info_row_sent_at), localTimestamp(ci.meta.itemTs)) @@ -106,10 +153,12 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { if (itemDeleted.deletedTs != null) { InfoRow(stringResource(MR.strings.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs)) } + is CIDeleted.Moderated -> if (itemDeleted.deletedTs != null) { InfoRow(stringResource(MR.strings.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs)) } + else -> {} } val deleteAt = ci.meta.itemTimed?.deleteAt @@ -121,17 +170,105 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) } } - val versions = ciInfo.itemVersions - if (versions.isNotEmpty()) { + } + + @Composable + fun HistoryTab() { + Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) - versions.forEachIndexed { i, ciVersion -> - ItemVersionView(ciVersion, current = i == 0) + val versions = ciInfo.itemVersions + if (versions.isNotEmpty()) { + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + versions.forEachIndexed { i, ciVersion -> + ItemVersionView(ciVersion, current = i == 0) + } + } + } else { + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary) + } } } + SectionBottomSpacer() + } + } + + @Composable + fun QuoteTab(qi: CIQuote) { + Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Details() + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + QuotedMsgView(qi) + } + SectionBottomSpacer() + } + } + + @Composable + fun tabTitle(tab: CIInfoTab): String { + return when (tab) { + CIInfoTab.History -> stringResource(MR.strings.edit_history) + CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to) + } + } + + fun tabIcon(tab: CIInfoTab): ImageResource { + return when (tab) { + CIInfoTab.History -> MR.images.ic_history + CIInfoTab.Quote -> MR.images.ic_reply + } + } + + Column { + if (ci.quotedItem != null) { + Column( + Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column(Modifier.weight(1f)) { + when (selection.value) { + CIInfoTab.History -> { + HistoryTab() + } + + CIInfoTab.Quote -> { + QuoteTab(ci.quotedItem) + } + } + } + TabRow( + selectedTabIndex = selection.value.ordinal, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + CIInfoTab.values().forEachIndexed { index, it -> + Tab( + selected = selection.value.ordinal == index, + onClick = { + selection.value = CIInfoTab.values()[index] + }, + text = { Text(tabTitle(it), fontSize = 13.sp) }, + icon = { + Icon( + painterResource(tabIcon(it)), + tabTitle(it) + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + } + } else { + HistoryTab() } - SectionBottomSpacer() } } @@ -149,10 +286,12 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea if (itemDeleted.deletedTs != null) { shareText.add(String.format(generalGetString(MR.strings.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs))) } + is CIDeleted.Moderated -> if (itemDeleted.deletedTs != null) { shareText.add(String.format(generalGetString(MR.strings.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs))) } + else -> {} } val deleteAt = ci.meta.itemTimed?.deleteAt @@ -163,6 +302,21 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) } + val qi = ci.quotedItem + if (qi != null) { + shareText.add("") + shareText.add(generalGetString(MR.strings.in_reply_to)) + shareText.add("") + val ts = localTimestamp(qi.sentAt) + val sender = qi.sender(null) + if (sender != null) { + shareText.add(String.format(generalGetString(MR.strings.sender_at_ts), sender, ts)) + } else { + shareText.add(ts) + } + val t = qi.text + shareText.add(if (t != "") t else generalGetString(MR.strings.item_info_no_text)) + } val versions = chatItemInfo.itemVersions if (versions.isNotEmpty()) { shareText.add("") @@ -174,7 +328,7 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea if (index == 0 && ci.meta.itemDeleted == null) { String.format(generalGetString(MR.strings.current_version_timestamp), ts) } else { - localTimestamp(itemVersion.itemVersionTs) + ts } ) val t = itemVersion.msgContent.text 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 1797890271..5da9bd973c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -220,6 +220,8 @@ Sent message Received message History + No history + In reply to Delete Reveal Hide @@ -1161,7 +1163,8 @@ Moderated at: %s Disappears at: %s (current) - "%s (current)" + %s at %s + %s (current) no text diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg new file mode 100644 index 0000000000..f98f4db386 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg @@ -0,0 +1 @@ + \ No newline at end of file