diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 725377fcd8..9390c9e639 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -4,8 +4,6 @@ import android.net.Uri import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration @@ -19,12 +17,16 @@ import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.app.views.usersettings.NotificationsMode import kotlinx.coroutines.* import kotlinx.datetime.* +import kotlinx.datetime.TimeZone import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.io.File +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.* import kotlin.random.Random import kotlin.time.* @@ -1624,8 +1626,28 @@ fun getTimestampText(t: Instant): String { val time: LocalDateTime = t.toLocalDateTime(tz) val recent = now.date == time.date || (now.date.minus(time.date).days == 1 && now.hour < 12 && time.hour >= 18 ) - return if (recent) String.format("%02d:%02d", time.hour, time.minute) - else String.format("%02d/%02d", time.dayOfMonth, time.monthNumber) + val dateFormatter = + if (recent) { + DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) + } else { + DateTimeFormatter.ofPattern( + when (Locale.getDefault().country) { + "US" -> "M/dd" + "DE" -> "dd.MM" + "RU" -> "dd.MM" + else -> "dd/MM" + } + ) +// DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + } + return time.toJavaLocalDateTime().format(dateFormatter) +} + +fun localTimestamp(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val ts: LocalDateTime = t.toLocalDateTime(tz) + val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + return ts.toJavaLocalDateTime().format(dateFormatter) } @Serializable @@ -1640,8 +1662,8 @@ sealed class CIStatus { @Serializable sealed class CIDeleted { - @Serializable @SerialName("deleted") class Deleted: CIDeleted() - @Serializable @SerialName("moderated") class Moderated(val byGroupMember: GroupMember): CIDeleted() + @Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted() + @Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted() } @Serializable @@ -2380,3 +2402,17 @@ sealed class ChatItemTTL: Comparable { } } } + +@Serializable +class ChatItemInfo( + val itemVersions: List, +) + +@Serializable +data class ChatItemVersion( + val chatItemVersionId: Long, + val msgContent: MsgContent, + val formattedText: List?, + val itemVersionTs: Instant, + val createdAt: Instant, +) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 1a66d6d44b..ff80579dff 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -595,6 +595,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + suspend fun apiGetChatItemInfo(type: ChatType, id: Long, itemId: Long): ChatItemInfo? { + return when (val r = sendCmd(CC.ApiGetChatItemInfo(type, id, itemId))) { + is CR.ApiChatItemInfo -> r.chatItemInfo + else -> { + apiErrorAlert("apiGetChatItemInfo", generalGetString(R.string.error_loading_details), r) + null + } + } + } + suspend fun apiUpdateChatItem(type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? { val r = sendCmd(CC.ApiUpdateChatItem(type, id, itemId, mc, live)) if (r is CR.ChatItemUpdated) return r.chatItem @@ -1895,6 +1905,7 @@ sealed class CC { class ApiStorageEncryption(val config: DBEncryptionConfig): CC() class ApiGetChats(val userId: Long): CC() class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() @@ -1984,6 +1995,7 @@ sealed class CC { is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}" is ApiGetChats -> "/_get chats $userId pcc=on" is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") + is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" is ApiSendMessage -> { val ttlStr = if (ttl != null) "$ttl" else "default" "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" @@ -2074,6 +2086,7 @@ sealed class CC { is ApiStorageEncryption -> "apiStorageEncryption" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" + is ApiGetChatItemInfo -> "apiGetChatItemInfo" is ApiSendMessage -> "apiSendMessage" is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" @@ -3254,6 +3267,7 @@ sealed class CR { @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR() + @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: User, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR() @@ -3366,6 +3380,7 @@ sealed class CR { is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" + is ApiChatItemInfo -> "chatItemInfo" is UserProtoServers -> "userProtoServers" is ServerTestResult -> "serverTestResult" is ChatItemTTL -> "chatItemTTL" @@ -3475,6 +3490,7 @@ sealed class CR { is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) is ApiChat -> withUser(user, json.encodeToString(chat)) + is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(AChatItem)}\n${json.encodeToString(chatItemInfo)}") is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt new file mode 100644 index 0000000000..1b69f5c4cd --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt @@ -0,0 +1,169 @@ +package chat.simplex.app.views.chat + +import InfoRow +import SectionBottomSpacer +import SectionDividerSpaced +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.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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 +import chat.simplex.app.views.chat.item.ItemAction +import chat.simplex.app.views.chat.item.MarkdownText +import chat.simplex.app.views.helpers.* + +@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 context = LocalContext.current + val uriHandler = LocalUriHandler.current + + @Composable + fun ItemVersionView(ciVersion: ChatItemVersion, current: Boolean) { + val showMenu = remember { mutableStateOf(false) } + val text = ciVersion.msgContent.text + Column { + Box( + Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp) + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) + ) { + Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { + MarkdownText( + text, if (text.isEmpty()) emptyList() else ciVersion.formattedText, + linkMode = SimplexLinkMode.DESCRIPTION, uriHandler = uriHandler, + onLinkLongClick = { showMenu.value = true } + ) + } + } + Row(Modifier.padding(start = 12.dp, top = 3.dp, bottom = 16.dp)) { + Text( + localTimestamp(ciVersion.itemVersionTs), + fontSize = 12.sp, + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(end = 6.dp) + ) + if (current && ci.meta.itemDeleted == null) { + Text( + stringResource(R.string.item_info_current), + fontSize = 12.sp, + color = MaterialTheme.colors.secondary + ) + } + } + DefaultDropdownMenu(showMenu) { + ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = { + shareText(context, text) + showMenu.value = false + }) + ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = { + copyText(context, text) + showMenu.value = false + }) + } + } + } + + Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + AppBarTitle(stringResource(if (sent) R.string.sent_message else R.string.received_message)) + SectionView { + InfoRow(stringResource(R.string.info_row_sent_at), localTimestamp(ci.meta.itemTs)) + if (!sent) { + InfoRow(stringResource(R.string.info_row_received_at), localTimestamp(ci.meta.createdAt)) + } + when (val itemDeleted = ci.meta.itemDeleted) { + is CIDeleted.Deleted -> + if (itemDeleted.deletedTs != null) { + InfoRow(stringResource(R.string.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs)) + } + is CIDeleted.Moderated -> + if (itemDeleted.deletedTs != null) { + InfoRow(stringResource(R.string.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs)) + } + else -> {} + } + val deleteAt = ci.meta.itemTimed?.deleteAt + if (deleteAt != null) { + InfoRow(stringResource(R.string.info_row_disappears_at), localTimestamp(deleteAt)) + } + if (devTools) { + InfoRow(stringResource(R.string.info_row_database_id), ci.meta.itemId.toString()) + InfoRow(stringResource(R.string.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) + } + } + val versions = ciInfo.itemVersions + if (versions.isNotEmpty()) { + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text(stringResource(R.string.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + versions.forEachIndexed { i, ciVersion -> + ItemVersionView(ciVersion, current = i == 0) + } + } + } + SectionBottomSpacer() + } +} + +fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String { + val meta = ci.meta + val sent = ci.chatDir.sent + val shareText = mutableListOf(generalGetString(if (sent) R.string.sent_message else R.string.received_message), "") + + shareText.add(String.format(generalGetString(R.string.share_text_sent_at), localTimestamp(meta.itemTs))) + if (!ci.chatDir.sent) { + shareText.add(String.format(generalGetString(R.string.share_text_received_at), localTimestamp(meta.createdAt))) + } + when (val itemDeleted = ci.meta.itemDeleted) { + is CIDeleted.Deleted -> + if (itemDeleted.deletedTs != null) { + shareText.add(String.format(generalGetString(R.string.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs))) + } + is CIDeleted.Moderated -> + if (itemDeleted.deletedTs != null) { + shareText.add(String.format(generalGetString(R.string.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs))) + } + else -> {} + } + val deleteAt = ci.meta.itemTimed?.deleteAt + if (deleteAt != null) { + shareText.add(String.format(generalGetString(R.string.share_text_disappears_at), localTimestamp(deleteAt))) + } + if (devTools) { + shareText.add(String.format(generalGetString(R.string.share_text_database_id), meta.itemId)) + shareText.add(String.format(generalGetString(R.string.share_text_updated_at), meta.updatedAt)) + } + val versions = chatItemInfo.itemVersions + if (versions.isNotEmpty()) { + shareText.add("") + shareText.add(generalGetString(R.string.edit_history)) + versions.forEachIndexed { index, itemVersion -> + val ts = localTimestamp(itemVersion.itemVersionTs) + shareText.add("") + shareText.add( + if (index == 0 && ci.meta.itemDeleted == null) { + String.format(generalGetString(R.string.current_version_timestamp), ts) + } else { + localTimestamp(itemVersion.itemVersionTs) + } + ) + shareText.add(itemVersion.msgContent.text) + } + } + return shareText.joinToString(separator = "\n") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 80472372c3..ed26b2d761 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -101,6 +101,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } } val view = LocalView.current + val context = LocalContext.current if (activeChat.value == null || user == null) { chatModel.chatId.value = null } else { @@ -272,6 +273,18 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } } }, + showItemDetails = { cInfo, cItem -> + withApi { + val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id) + if (ciInfo != null) { + ModalManager.shared.showModal(endButtons = { ShareButton { + shareText(context, itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) + } }) { + ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + } + } + } + }, addMembers = { groupInfo -> hideKeyboard(view) withApi { @@ -331,6 +344,7 @@ fun ChatLayout( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, + showItemDetails: (ChatInfo, ChatItem) -> Unit, addMembers: (GroupInfo) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, changeNtfsState: (Boolean, currentValue: MutableState) -> Unit, @@ -373,7 +387,7 @@ fun ChatLayout( ChatItemsList( chat, unreadCount, composeState, chatItems, searchValue, useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, markRead, setFloatingButton, onComposed, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, showItemDetails, markRead, setFloatingButton, onComposed, ) } } @@ -547,6 +561,7 @@ fun BoxWithConstraintsScope.ChatItemsList( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, + showItemDetails: (ChatInfo, ChatItem) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit, onComposed: () -> Unit, @@ -660,11 +675,11 @@ fun BoxWithConstraintsScope.ChatItemsList( } 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, scrollToItem = scrollToItem, setReaction = setReaction) + ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } else { 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, scrollToItem = scrollToItem, setReaction = setReaction) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } } else { // direct message @@ -675,7 +690,7 @@ fun BoxWithConstraintsScope.ChatItemsList( end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, ).then(swipeableModifier) ) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } @@ -1099,6 +1114,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, addMembers = { _ -> }, markRead = { _, _ -> }, changeNtfsState = { _, _ -> }, @@ -1160,6 +1176,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, addMembers = { _ -> }, markRead = { _, _ -> }, changeNtfsState = { _, _ -> }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index c9e460d03d..eace6bed88 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -44,7 +44,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { StatusIconText(painterResource(R.drawable.ic_timer), color) val ttl = meta.itemTimed?.ttl if (ttl != chatTTL) { - Text(shortTimeText(ttl), color = color, fontSize = 13.sp) + Text(shortTimeText(ttl), color = color, fontSize = 12.sp) } Spacer(Modifier.width(4.dp)) } @@ -57,7 +57,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { StatusIconText(painterResource(R.drawable.ic_circle_filled), Color.Transparent) Spacer(Modifier.width(4.dp)) } - Text(meta.timestampText, color = color, fontSize = 13.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 diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 531fb16851..1643490138 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -25,9 +25,9 @@ import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.ComposeContextItem -import chat.simplex.app.views.chat.ComposeState +import chat.simplex.app.views.chat.* import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.IncognitoView import com.google.accompanist.permissions.rememberPermissionState import kotlinx.datetime.Clock @@ -50,6 +50,7 @@ fun ChatItemView( scrollToItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, + showItemDetails: (ChatInfo, ChatItem) -> Unit, ) { val context = LocalContext.current val uriHandler = LocalUriHandler.current @@ -225,6 +226,7 @@ fun ChatItemView( if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) { CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) if (!(live && cItem.meta.isLive)) { DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) } @@ -247,6 +249,7 @@ fun ChatItemView( showMenu.value = false } ) + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) } DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) } @@ -317,7 +320,7 @@ fun ChatItemView( } } - if (cItem.content.msgContent != null && cItem.meta.itemDeleted == null && cItem.reactions.isNotEmpty()) { + if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) { ChatItemReactions() } } @@ -342,6 +345,24 @@ fun CancelFileItemAction( ) } +@Composable +fun ItemInfoAction( + cInfo: ChatInfo, + cItem: ChatItem, + showItemDetails: (ChatInfo, ChatItem) -> Unit, + showMenu: MutableState +) { + ItemAction( + stringResource(R.string.info_menu), + painterResource(R.drawable.ic_info), + onClick = { + showItemDetails(cInfo, cItem) + showMenu.value = false + } + ) +} + + @Composable fun DeleteItemAction( cItem: ChatItem, @@ -496,6 +517,7 @@ fun PreviewChatItemView() { scrollToItem = {}, acceptFeature = { _, _, _ -> }, setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, ) } } @@ -518,6 +540,7 @@ fun PreviewChatItemViewDeletedContent() { scrollToItem = {}, acceptFeature = { _, _, _ -> }, setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt index 4bb56b60fa..937d007517 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt @@ -19,6 +19,7 @@ import chat.simplex.app.model.CIDeleted import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.generalGetString +import kotlinx.datetime.Clock @Composable fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { @@ -67,7 +68,7 @@ private fun MarkedDeletedText(text: String) { fun PreviewMarkedDeletedItemView() { SimpleXTheme { DeletedItemView( - ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()), + ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), null ) } diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 690e841378..4d616fc5ee 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -83,6 +83,7 @@ Connection error Please check your network connection with %1$s and try again. Error sending message + Error loading details Error adding member(s) Error joining group Cannot receive file @@ -212,6 +213,10 @@ Copy Save Edit + Info + Sent message + Received message + History Delete Reveal Hide @@ -1107,10 +1112,25 @@ Share address You can share this address with your contacts to let them connect with %s. - + FOR CONSOLE Local name Database ID + Record updated at + Sent at + Received at + Deleted at + Moderated at + Disappears at + Database ID: %d + Record updated at: %s + Sent at: %s + Received at: %s + Deleted at: %s + Moderated at: %s + Disappears at: %s + (current) + "%s (current)" Remove member