mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
android: group snd status (#2784)
This commit is contained in:
+65
-14
@@ -1625,18 +1625,12 @@ data class CIMeta (
|
||||
|
||||
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
|
||||
|
||||
fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<ImageResource, Color>? =
|
||||
when (itemStatus) {
|
||||
is CIStatus.SndSent -> MR.images.ic_check_filled to metaColor
|
||||
is CIStatus.SndRcvd -> when(itemStatus.msgRcptStatus) {
|
||||
MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor
|
||||
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
|
||||
}
|
||||
is CIStatus.SndErrorAuth -> MR.images.ic_close to Color.Red
|
||||
is CIStatus.SndError -> MR.images.ic_warning_filled to WarningYellow
|
||||
is CIStatus.RcvNew -> MR.images.ic_circle_filled to primaryColor
|
||||
else -> null
|
||||
}
|
||||
fun statusIcon(
|
||||
primaryColor: Color,
|
||||
metaColor: Color = CurrentColors.value.colors.secondary,
|
||||
paleMetaColor: Color = CurrentColors.value.colors.secondary
|
||||
): Pair<ImageResource, Color>? =
|
||||
itemStatus.statusIcon(primaryColor, metaColor, paleMetaColor)
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
@@ -1715,12 +1709,56 @@ fun localTimestamp(t: Instant): String {
|
||||
@Serializable
|
||||
sealed class CIStatus {
|
||||
@Serializable @SerialName("sndNew") class SndNew: CIStatus()
|
||||
@Serializable @SerialName("sndSent") class SndSent: CIStatus()
|
||||
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus): CIStatus()
|
||||
@Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus()
|
||||
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
|
||||
@Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus()
|
||||
@Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
|
||||
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
|
||||
|
||||
fun statusIcon(
|
||||
primaryColor: Color,
|
||||
metaColor: Color = CurrentColors.value.colors.secondary,
|
||||
paleMetaColor: Color = CurrentColors.value.colors.secondary
|
||||
): Pair<ImageResource, Color>? =
|
||||
when (this) {
|
||||
is SndNew -> null
|
||||
is SndSent -> when (this.sndProgress) {
|
||||
SndCIStatusProgress.Complete -> MR.images.ic_check_filled to metaColor
|
||||
SndCIStatusProgress.Partial -> MR.images.ic_check_filled to paleMetaColor
|
||||
}
|
||||
is SndRcvd -> when(this.msgRcptStatus) {
|
||||
MsgReceiptStatus.Ok -> when (this.sndProgress) {
|
||||
SndCIStatusProgress.Complete -> MR.images.ic_double_check to metaColor
|
||||
SndCIStatusProgress.Partial -> MR.images.ic_double_check to paleMetaColor
|
||||
}
|
||||
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
|
||||
}
|
||||
is SndErrorAuth -> MR.images.ic_close to Color.Red
|
||||
is SndError -> MR.images.ic_warning_filled to WarningYellow
|
||||
is RcvNew -> MR.images.ic_circle_filled to primaryColor
|
||||
is RcvRead -> null
|
||||
}
|
||||
|
||||
val statusText: String get() = when (this) {
|
||||
is SndNew -> generalGetString(MR.strings.item_status_snd_new_text)
|
||||
is SndSent -> generalGetString(MR.strings.item_status_snd_sent_text)
|
||||
is SndRcvd -> generalGetString(MR.strings.item_status_snd_rcvd_text)
|
||||
is SndErrorAuth -> generalGetString(MR.strings.item_status_snd_error_text)
|
||||
is SndError -> generalGetString(MR.strings.item_status_snd_error_text)
|
||||
is RcvNew -> generalGetString(MR.strings.item_status_rcv_new_text)
|
||||
is RcvRead -> generalGetString(MR.strings.item_status_rcv_read_text)
|
||||
}
|
||||
|
||||
val statusDescription: String get() = when (this) {
|
||||
is SndNew -> generalGetString(MR.strings.item_status_snd_new_desc)
|
||||
is SndSent -> generalGetString(MR.strings.item_status_snd_sent_desc)
|
||||
is SndRcvd -> generalGetString(MR.strings.item_status_snd_rcvd_desc)
|
||||
is SndErrorAuth -> generalGetString(MR.strings.item_status_snd_error_auth_desc)
|
||||
is SndError -> String.format(generalGetString(MR.strings.item_status_snd_error_unexpected_desc), this.agentError)
|
||||
is RcvNew -> generalGetString(MR.strings.item_status_rcv_new_desc)
|
||||
is RcvRead -> generalGetString(MR.strings.item_status_rcv_read_desc)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1729,6 +1767,12 @@ enum class MsgReceiptStatus {
|
||||
@SerialName("badMsgHash") BadMsgHash;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SndCIStatusProgress {
|
||||
@SerialName("partial") Partial,
|
||||
@SerialName("complete") Complete;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIDeleted {
|
||||
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()
|
||||
@@ -2489,6 +2533,7 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
|
||||
@Serializable
|
||||
class ChatItemInfo(
|
||||
val itemVersions: List<ChatItemVersion>,
|
||||
val memberDeliveryStatuses: List<MemberDeliveryStatus>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -2500,6 +2545,12 @@ data class ChatItemVersion(
|
||||
val createdAt: Instant,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemberDeliveryStatus(
|
||||
val groupMemberId: Long,
|
||||
val memberDeliveryStatus: CIStatus
|
||||
)
|
||||
|
||||
enum class NotificationPreviewMode {
|
||||
MESSAGE, CONTACT, HIDDEN;
|
||||
|
||||
|
||||
+14
-2
@@ -471,13 +471,19 @@ object ChatController {
|
||||
suspend fun apiSetAllContactReceipts(enable: Boolean) {
|
||||
val r = sendCmd(CC.SetAllContactReceipts(enable))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to enable receipts for all users ${r.responseType} ${r.details}")
|
||||
throw Exception("failed to set receipts for all users ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetUserContactReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) {
|
||||
val r = sendCmd(CC.ApiSetUserContactReceipts(userId, userMsgReceiptSettings))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to enable receipts for user contacts ${r.responseType} ${r.details}")
|
||||
throw Exception("failed to set receipts for user contacts ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetUserGroupReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) {
|
||||
val r = sendCmd(CC.ApiSetUserGroupReceipts(userId, userMsgReceiptSettings))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiHideUser(userId: Long, viewPwd: String): User =
|
||||
@@ -1785,6 +1791,7 @@ sealed class CC {
|
||||
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
|
||||
class SetAllContactReceipts(val enable: Boolean): CC()
|
||||
class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiUnhideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiMuteUser(val userId: Long): CC()
|
||||
@@ -1884,6 +1891,10 @@ sealed class CC {
|
||||
val mrs = userMsgReceiptSettings
|
||||
"/_set receipts contacts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiSetUserGroupReceipts -> {
|
||||
val mrs = userMsgReceiptSettings
|
||||
"/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiMuteUser -> "/_mute user $userId"
|
||||
@@ -1981,6 +1992,7 @@ sealed class CC {
|
||||
is ApiSetActiveUser -> "apiSetActiveUser"
|
||||
is SetAllContactReceipts -> "setAllContactReceipts"
|
||||
is ApiSetUserContactReceipts -> "apiSetUserContactReceipts"
|
||||
is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts"
|
||||
is ApiHideUser -> "apiHideUser"
|
||||
is ApiUnhideUser -> "apiUnhideUser"
|
||||
is ApiMuteUser -> "apiMuteUser"
|
||||
|
||||
+156
-26
@@ -3,6 +3,7 @@ package chat.simplex.common.views.chat
|
||||
import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -19,28 +20,30 @@ import androidx.compose.ui.text.AnnotatedString
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.platform.shareText
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
|
||||
enum class CIInfoTab {
|
||||
History, Quote
|
||||
sealed class CIInfoTab {
|
||||
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
|
||||
object History: CIInfoTab()
|
||||
class Quote(val quotedItem: CIQuote): CIInfoTab()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val appColors = CurrentColors.collectAsState().value.appColors
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val selection = remember { mutableStateOf(CIInfoTab.History) }
|
||||
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
|
||||
|
||||
@Composable
|
||||
fun TextBubble(text: String, formattedText: List<FormattedText>?, sender: String?, showMenu: MutableState<Boolean>) {
|
||||
@@ -160,10 +163,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
|
||||
@@ -214,55 +219,154 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) {
|
||||
SectionItemView(
|
||||
padding = PaddingValues(horizontal = 0.dp)
|
||||
) {
|
||||
ProfileImage(size = 36.dp, member.image)
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
Text(
|
||||
member.chatViewName,
|
||||
modifier = Modifier.weight(10f, fill = true),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
|
||||
Box(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.clickable {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = status.statusText,
|
||||
text = status.statusDescription
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = statusColor
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_more_horiz),
|
||||
contentDescription = null,
|
||||
tint = CurrentColors.value.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
|
||||
Details()
|
||||
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
|
||||
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
|
||||
if (mss.isNotEmpty()) {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
|
||||
mss.forEach { (member, status) ->
|
||||
MemberDeliveryStatusView(member, status)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
is CIInfoTab.Delivery -> stringResource(MR.strings.delivery)
|
||||
is CIInfoTab.History -> stringResource(MR.strings.edit_history)
|
||||
is 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
|
||||
is CIInfoTab.Delivery -> MR.images.ic_double_check
|
||||
is CIInfoTab.History -> MR.images.ic_history
|
||||
is CIInfoTab.Quote -> MR.images.ic_reply
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
fun numTabs(): Int {
|
||||
var numTabs = 1
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
numTabs += 1
|
||||
}
|
||||
if (ci.quotedItem != null) {
|
||||
numTabs += 1
|
||||
}
|
||||
return numTabs
|
||||
}
|
||||
|
||||
Column {
|
||||
if (numTabs() > 1) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
CIInfoTab.History -> {
|
||||
when (val sel = selection.value) {
|
||||
is CIInfoTab.Delivery -> {
|
||||
DeliveryTab(sel.memberDeliveryStatuses)
|
||||
}
|
||||
|
||||
is CIInfoTab.History -> {
|
||||
HistoryTab()
|
||||
}
|
||||
|
||||
CIInfoTab.Quote -> {
|
||||
QuoteTab(ci.quotedItem)
|
||||
is CIInfoTab.Quote -> {
|
||||
QuoteTab(sel.quotedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
val availableTabs = mutableListOf<CIInfoTab>()
|
||||
if (ciInfo.memberDeliveryStatuses != null) {
|
||||
availableTabs.add(CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses))
|
||||
}
|
||||
availableTabs.add(CIInfoTab.History)
|
||||
if (ci.quotedItem != null) {
|
||||
availableTabs.add(CIInfoTab.Quote(ci.quotedItem))
|
||||
}
|
||||
TabRow(
|
||||
selectedTabIndex = selection.value.ordinal,
|
||||
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
|
||||
backgroundColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
) {
|
||||
CIInfoTab.values().forEachIndexed { index, it ->
|
||||
availableTabs.forEach { ciInfoTab ->
|
||||
Tab(
|
||||
selected = selection.value.ordinal == index,
|
||||
selected = selection.value::class == ciInfoTab::class,
|
||||
onClick = {
|
||||
selection.value = CIInfoTab.values()[index]
|
||||
selection.value = ciInfoTab
|
||||
},
|
||||
text = { Text(tabTitle(it), fontSize = 13.sp) },
|
||||
text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) },
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(tabIcon(it)),
|
||||
tabTitle(it)
|
||||
painterResource(tabIcon(ciInfoTab)),
|
||||
tabTitle(ciInfoTab)
|
||||
)
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colors.primary,
|
||||
@@ -277,10 +381,18 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
|
||||
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> {
|
||||
return memberDeliveryStatuses.mapNotNull { mds ->
|
||||
chatModel.groupMembers.firstOrNull { it.groupMemberId == mds.groupMemberId }?.let { mem ->
|
||||
mem to mds.memberDeliveryStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
|
||||
val meta = ci.meta
|
||||
val sent = ci.chatDir.sent
|
||||
val shareText = mutableListOf<String>(generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
|
||||
val shareText = mutableListOf<String>("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "")
|
||||
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs)))
|
||||
if (!ci.chatDir.sent) {
|
||||
@@ -291,10 +403,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
|
||||
@@ -308,7 +422,7 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea
|
||||
val qi = ci.quotedItem
|
||||
if (qi != null) {
|
||||
shareText.add("")
|
||||
shareText.add(generalGetString(MR.strings.in_reply_to))
|
||||
shareText.add("## " + generalGetString(MR.strings.in_reply_to))
|
||||
shareText.add("")
|
||||
val ts = localTimestamp(qi.sentAt)
|
||||
val sender = qi.sender(null)
|
||||
@@ -320,10 +434,26 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea
|
||||
val t = qi.text
|
||||
shareText.add(if (t != "") t else generalGetString(MR.strings.item_info_no_text))
|
||||
}
|
||||
val mdss = chatItemInfo.memberDeliveryStatuses
|
||||
if (mdss != null) {
|
||||
val mss = membersStatuses(chatModel, mdss)
|
||||
if (mss.isNotEmpty()) {
|
||||
shareText.add("")
|
||||
shareText.add("## " + generalGetString(MR.strings.delivery))
|
||||
shareText.add("")
|
||||
mss.forEach { (member, status) ->
|
||||
shareText.add(String.format(
|
||||
generalGetString(MR.strings.recipient_colon_delivery_status),
|
||||
member.chatViewName,
|
||||
status.statusDescription
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
val versions = chatItemInfo.itemVersions
|
||||
if (versions.isNotEmpty()) {
|
||||
shareText.add("")
|
||||
shareText.add(generalGetString(MR.strings.edit_history))
|
||||
shareText.add("## " + generalGetString(MR.strings.edit_history))
|
||||
versions.forEachIndexed { index, itemVersion ->
|
||||
val ts = localTimestamp(itemVersion.itemVersionTs)
|
||||
shareText.add("")
|
||||
|
||||
+5
-2
@@ -321,11 +321,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = { ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
} }) {
|
||||
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+58
-5
@@ -26,26 +26,37 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.cantInviteIncognitoAlert
|
||||
import chat.simplex.common.views.chatlist.setGroupMembers
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.ClearChatButton
|
||||
import chat.simplex.common.views.chat.clearChatDialog
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group) {
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
|
||||
val groupInfo = chat.chatInfo.groupInfo
|
||||
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
|
||||
GroupChatInfoLayout(
|
||||
chat,
|
||||
groupInfo,
|
||||
currentUser,
|
||||
sendReceipts = sendReceipts,
|
||||
setSendReceipts = { sendRcpts ->
|
||||
withApi {
|
||||
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
|
||||
updateChatSettings(chat, chatSettings, chatModel)
|
||||
sendReceipts.value = sendRcpts
|
||||
}
|
||||
},
|
||||
members = chatModel.groupMembers
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedBy { it.displayName.lowercase() },
|
||||
@@ -150,6 +161,9 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
|
||||
fun GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
currentUser: User,
|
||||
sendReceipts: State<SendReceipts>,
|
||||
setSendReceipts: (SendReceipts) -> Unit,
|
||||
members: List<GroupMember>,
|
||||
developerTools: Boolean,
|
||||
groupLink: String?,
|
||||
@@ -184,6 +198,11 @@ fun GroupChatInfoLayout(
|
||||
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
|
||||
}
|
||||
GroupPreferencesButton(openPreferences)
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
@@ -269,6 +288,37 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
|
||||
val values = remember {
|
||||
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsSmallGroups)).map { it to it.text }
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.send_receipts),
|
||||
values,
|
||||
state,
|
||||
icon = painterResource(MR.images.ic_double_check),
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendReceiptsOptionDisabled() {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_double_check),
|
||||
text = generalGetString(MR.strings.send_receipts),
|
||||
click = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.send_receipts_disabled_alert_title),
|
||||
text = String.format(generalGetString(MR.strings.send_receipts_disabled_alert_msg), SMALL_GROUPS_RCPS_MEM_LIMIT)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(generalGetString(MR.strings.send_receipts_disabled), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -429,6 +479,9 @@ fun PreviewGroupChatInfoLayout() {
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
User.sampleData,
|
||||
sendReceipts = remember { mutableStateOf(SendReceipts.Yes) },
|
||||
setSendReceipts = {},
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
|
||||
+22
-6
@@ -14,11 +14,27 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = MaterialTheme.colors.secondary) {
|
||||
fun CIMetaView(
|
||||
chatItem: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
metaColor: Color = MaterialTheme.colors.secondary,
|
||||
paleMetaColor: Color = if (isInDarkTheme()) {
|
||||
metaColor.copy(
|
||||
red = metaColor.red * 0.67F,
|
||||
green = metaColor.green * 0.67F,
|
||||
blue = metaColor.red * 0.67F)
|
||||
} else {
|
||||
metaColor.copy(
|
||||
red = minOf(metaColor.red * 1.33F, 1F),
|
||||
green = minOf(metaColor.green * 1.33F, 1F),
|
||||
blue = minOf(metaColor.red * 1.33F, 1F))
|
||||
}
|
||||
) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
Text(
|
||||
@@ -28,14 +44,14 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor, paleMetaColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
// changing this function requires updating reserveSpaceForMeta
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color, paleColor: Color) {
|
||||
if (meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
@@ -48,7 +64,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) {
|
||||
@@ -138,7 +154,7 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
fun PreviewCIMetaViewSendSent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
)
|
||||
@@ -176,7 +192,7 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status= CIStatus.SndSent()
|
||||
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null
|
||||
)
|
||||
|
||||
+82
@@ -101,6 +101,29 @@ fun PrivacySettingsView(
|
||||
}
|
||||
}
|
||||
|
||||
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
|
||||
withApi {
|
||||
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
|
||||
chatModel.controller.apiSetUserGroupReceipts(currentUser.userId, mrs)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
|
||||
if (clearOverrides) {
|
||||
// For loop here is to prevent ConcurrentModificationException that happens with forEach
|
||||
for (i in 0 until chatModel.chats.size) {
|
||||
val chat = chatModel.chats[i]
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
var groupInfo = chat.chatInfo.groupInfo
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
@@ -117,6 +140,21 @@ fun PrivacySettingsView(
|
||||
} else {
|
||||
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (groupReceiptsOverrides == 0) {
|
||||
setSendReceiptsGroups(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -155,6 +193,7 @@ expect fun PrivacyDeviceSection(
|
||||
private fun DeliveryReceiptsSection(
|
||||
currentUser: User,
|
||||
setOrAskSendReceiptsContacts: (Boolean) -> Unit,
|
||||
setOrAskSendReceiptsGroups: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
|
||||
@@ -165,6 +204,14 @@ private fun DeliveryReceiptsSection(
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.sendRcptsSmallGroups ?: false,
|
||||
onCheckedChange = { enable ->
|
||||
setOrAskSendReceiptsGroups(enable)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser.displayName) {
|
||||
@@ -215,6 +262,41 @@ private fun showUserContactsReceiptsAlert(
|
||||
)
|
||||
}
|
||||
|
||||
private fun showUserGroupsReceiptsAlert(
|
||||
enable: Boolean,
|
||||
groupReceiptsOverrides: Int,
|
||||
setSendReceiptsGroups: (Boolean, Boolean) -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(if (enable) MR.strings.receipts_groups_title_enable else MR.strings.receipts_groups_title_disable),
|
||||
text = AnnotatedString(String.format(generalGetString(if (enable) MR.strings.receipts_groups_override_disabled else MR.strings.receipts_groups_override_enabled), groupReceiptsOverrides)),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
setSendReceiptsGroups(enable, false)
|
||||
}) {
|
||||
val t = stringResource(if (enable) MR.strings.receipts_groups_enable_keep_overrides else MR.strings.receipts_groups_disable_keep_overrides)
|
||||
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
setSendReceiptsGroups(enable, true)
|
||||
}
|
||||
) {
|
||||
val t = stringResource(if (enable) MR.strings.receipts_groups_enable_for_all else MR.strings.receipts_groups_disable_for_all)
|
||||
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val laDelays = listOf(10, 30, 60, 180, 0)
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -222,6 +222,8 @@
|
||||
<string name="edit_history">History</string>
|
||||
<string name="no_history">No history</string>
|
||||
<string name="in_reply_to">In reply to</string>
|
||||
<string name="delivery">Delivery</string>
|
||||
<string name="no_info_on_delivery">No info on delivery</string>
|
||||
<string name="delete_verb">Delete</string>
|
||||
<string name="reveal_verb">Reveal</string>
|
||||
<string name="hide_verb">Hide</string>
|
||||
@@ -869,7 +871,7 @@
|
||||
<string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string>
|
||||
<string name="set_passcode">Set passcode</string>
|
||||
<string name="receipts_section_description">These settings are for your current profile</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact settings</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact and group settings.</string>
|
||||
<string name="receipts_section_contacts">Contacts</string>
|
||||
<string name="receipts_contacts_title_enable">Enable receipts?</string>
|
||||
<string name="receipts_contacts_title_disable">Disable receipts?</string>
|
||||
@@ -879,6 +881,15 @@
|
||||
<string name="receipts_contacts_disable_keep_overrides">Disable (keep overrides)</string>
|
||||
<string name="receipts_contacts_enable_for_all">Enable for all</string>
|
||||
<string name="receipts_contacts_disable_for_all">Disable for all</string>
|
||||
<string name="receipts_section_groups">Small groups (max 20)</string>
|
||||
<string name="receipts_groups_title_enable">Enable receipts for groups?</string>
|
||||
<string name="receipts_groups_title_disable">Disable receipts for groups?</string>
|
||||
<string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string>
|
||||
<string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string>
|
||||
<string name="receipts_groups_enable_keep_overrides">Enable (keep group overrides)</string>
|
||||
<string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string>
|
||||
<string name="receipts_groups_enable_for_all">Enable for all groups</string>
|
||||
<string name="receipts_groups_disable_for_all">Disable for all groups</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
@@ -1161,6 +1172,9 @@
|
||||
<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>
|
||||
<string name="send_receipts">Send receipts</string>
|
||||
<string name="send_receipts_disabled">disabled</string>
|
||||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -1183,6 +1197,7 @@
|
||||
<string name="sender_at_ts">%s at %s</string>
|
||||
<string name="current_version_timestamp">%s (current)</string>
|
||||
<string name="item_info_no_text">no text</string>
|
||||
<string name="recipient_colon_delivery_status">%s: %s</string>
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
@@ -1527,6 +1542,22 @@
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- CIStatus texts -->
|
||||
<string name="item_status_snd_new_text">Sending message</string>
|
||||
<string name="item_status_snd_sent_text">Message sent</string>
|
||||
<string name="item_status_snd_rcvd_text">Sent message received</string>
|
||||
<string name="item_status_snd_error_text">Error sending message</string>
|
||||
<string name="item_status_rcv_new_text">Message received</string>
|
||||
<string name="item_status_rcv_read_text">Message read</string>
|
||||
|
||||
<string name="item_status_snd_new_desc">Sending message is in progress or pending.</string>
|
||||
<string name="item_status_snd_sent_desc">Message has been sent to the recipient\'s relay.</string>
|
||||
<string name="item_status_snd_rcvd_desc">Message has been received by the recipient.</string>
|
||||
<string name="item_status_snd_error_auth_desc">Message delivery error. Most likely this recipient has deleted the connection with you.</string>
|
||||
<string name="item_status_snd_error_unexpected_desc">Unexpected message delivery error: %1$s</string>
|
||||
<string name="item_status_rcv_new_desc">New message from this sender.</string>
|
||||
<string name="item_status_rcv_read_desc">You\'ve read this received message.</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
|
||||
|
||||
Reference in New Issue
Block a user