mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 05:24:44 +00:00
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:
co-authored by
spaced4ndy
parent
a0b3c0a5a4
commit
a0c4726af3
@@ -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 = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user