android: ui for message details (#2454)

* android: ui for message details

* edit history, share text

* show info for deleted items, show reactions on revealed items

* unused imports

* revert swift changes

* update

* color

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin
2023-05-19 20:36:25 +04:00
committed by GitHub
co-authored by spaced4ndy
parent a0b3c0a5a4
commit a0c4726af3
8 changed files with 299 additions and 17 deletions
@@ -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<ChatItemTTL?> {
}
}
}
@Serializable
class ChatItemInfo(
val itemVersions: List<ChatItemVersion>,
)
@Serializable
data class ChatItemVersion(
val chatItemVersionId: Long,
val msgContent: MsgContent,
val formattedText: List<FormattedText>?,
val itemVersionTs: Instant,
val createdAt: Instant,
)
@@ -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<Chat>): 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))
@@ -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<String>(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")
}
@@ -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<Boolean>) -> 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 = { _, _ -> },
@@ -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
@@ -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<Boolean>
) {
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 = { _, _ -> },
)
}
}
@@ -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
)
}
@@ -83,6 +83,7 @@
<string name="connection_error">Connection error</string>
<string name="network_error_desc">Please check your network connection with <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> and try again.</string>
<string name="error_sending_message">Error sending message</string>
<string name="error_loading_details">Error loading details</string>
<string name="error_adding_members">Error adding member(s)</string>
<string name="error_joining_group">Error joining group</string>
<string name="cannot_receive_file">Cannot receive file</string>
@@ -212,6 +213,10 @@
<string name="copy_verb">Copy</string>
<string name="save_verb">Save</string>
<string name="edit_verb">Edit</string>
<string name="info_menu">Info</string>
<string name="sent_message">Sent message</string>
<string name="received_message">Received message</string>
<string name="edit_history">History</string>
<string name="delete_verb">Delete</string>
<string name="reveal_verb">Reveal</string>
<string name="hide_verb">Hide</string>
@@ -1107,10 +1112,25 @@
<string name="share_address">Share address</string>
<string name="you_can_share_this_address_with_your_contacts">You can share this address with your contacts to let them connect with %s.</string>
<!-- For Console chat info section -->
<!-- Chat / Chat item info -->
<string name="section_title_for_console">FOR CONSOLE</string>
<string name="info_row_local_name">Local name</string>
<string name="info_row_database_id">Database ID</string>
<string name="info_row_updated_at">Record updated at</string>
<string name="info_row_sent_at">Sent at</string>
<string name="info_row_received_at">Received at</string>
<string name="info_row_deleted_at">Deleted at</string>
<string name="info_row_moderated_at">Moderated at</string>
<string name="info_row_disappears_at">Disappears at</string>
<string name="share_text_database_id">Database ID: %d</string>
<string name="share_text_updated_at">Record updated at: %s</string>
<string name="share_text_sent_at">Sent at: %s</string>
<string name="share_text_received_at">Received at: %s</string>
<string name="share_text_deleted_at">Deleted at: %s</string>
<string name="share_text_moderated_at">Moderated at: %s</string>
<string name="share_text_disappears_at">Disappears at: %s</string>
<string name="item_info_current">(current)</string>
<string name="current_version_timestamp">"%s (current)"</string>
<!-- GroupMemberInfoView.kt -->
<string name="button_remove_member">Remove member</string>