From 969a7c433d7002822d0f65cc4b82665f72905470 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 22 Jan 2025 19:32:09 +0000 Subject: [PATCH] android, desktop: support chat item ttl per chat (#5546) * android, desktop: support chat item ttl per chat * disable all intereactions when reloading ttl * changes matching ios * changes * simplify * changes * divided apiLoadMessages * change * stacktrace * unneeded coroutineScope * only disable ttl * remove delay * look * look * Revert "look" This reverts commit 86a5d1d51191d2808a2a4d3549a199e8d5b02675. --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin --- .../chat/simplex/common/model/ChatModel.kt | 37 ++++-- .../chat/simplex/common/model/SimpleXAPI.kt | 22 +++- .../simplex/common/views/chat/ChatInfoView.kt | 109 +++++++++++++++++- .../common/views/chat/ChatItemsLoader.kt | 13 ++- .../views/chat/group/GroupChatInfoView.kt | 52 ++++++++- .../common/views/chat/group/GroupLinkView.kt | 3 +- .../common/views/database/DatabaseView.kt | 34 +++--- .../commonMain/resources/MR/base/strings.xml | 8 ++ 8 files changed, 242 insertions(+), 36 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 4eb0b350cb..c3bea3be90 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1484,6 +1484,7 @@ data class Contact( val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, val chatTags: List, + val chatItemTTL: Long?, override val chatDeleted: Boolean, val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { @@ -1567,7 +1568,8 @@ data class Contact( contactGrpInvSent = false, chatDeleted = false, uiThemes = null, - chatTags = emptyList() + chatTags = emptyList(), + chatItemTTL = null, ) } } @@ -1726,6 +1728,7 @@ data class GroupInfo ( val chatTs: Instant?, val uiThemes: ThemeModeOverrides? = null, val chatTags: List, + val chatItemTTL: Long?, override val localAlias: String, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group @@ -1774,7 +1777,8 @@ data class GroupInfo ( chatTs = Clock.System.now(), uiThemes = null, chatTags = emptyList(), - localAlias = "" + localAlias = "", + chatItemTTL = null ) } } @@ -4186,32 +4190,49 @@ enum class SwitchPhase { @SerialName("completed") Completed } -sealed class ChatItemTTL: Comparable { +sealed class ChatItemTTL: Comparable { object Day: ChatItemTTL() object Week: ChatItemTTL() object Month: ChatItemTTL() + object Year: ChatItemTTL() data class Seconds(val secs: Long): ChatItemTTL() object None: ChatItemTTL() - override fun compareTo(other: ChatItemTTL?): Int = (seconds ?: Long.MAX_VALUE).compareTo(other?.seconds ?: Long.MAX_VALUE) + override fun compareTo(other: ChatItemTTL): Int = + (seconds.takeIf { it != 0L } ?: Long.MAX_VALUE) + .compareTo(other.seconds.takeIf { it != 0L } ?: Long.MAX_VALUE) - val seconds: Long? + val seconds: Long get() = when (this) { - is None -> null + is None -> 0 is Day -> 86400L is Week -> 7 * 86400L is Month -> 30 * 86400L + is Year -> 365 * 86400L is Seconds -> secs } + val text: String + get() = when(this) { + is None -> generalGetString(MR.strings.chat_item_ttl_none) + is Day -> generalGetString(MR.strings.chat_item_ttl_day) + is Week -> generalGetString(MR.strings.chat_item_ttl_week) + is Month -> generalGetString(MR.strings.chat_item_ttl_month) + is Year -> generalGetString(MR.strings.chat_item_ttl_year) + is Seconds -> String.format(generalGetString(MR.strings.chat_item_ttl_seconds), secs) + } + + val neverExpires: Boolean get() = this is None + companion object { - fun fromSeconds(seconds: Long?): ChatItemTTL = + fun fromSeconds(seconds: Long): ChatItemTTL = when (seconds) { - null -> None + 0L -> None 86400L -> Day 7 * 86400L -> Week 30 * 86400L -> Month + 365 * 86400L -> Year else -> Seconds(seconds) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index f891d206a3..bf011c0bb7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1182,7 +1182,13 @@ object ChatController { suspend fun getChatItemTTL(rh: Long?): ChatItemTTL { val userId = currentUserId("getChatItemTTL") val r = sendCmd(rh, CC.APIGetChatItemTTL(userId)) - if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL) + if (r is CR.ChatItemTTL) { + return if (r.chatItemTTL != null) { + ChatItemTTL.fromSeconds(r.chatItemTTL) + } else { + ChatItemTTL.None + } + } throw Exception("failed to get chat item TTL: ${r.responseType} ${r.details}") } @@ -1193,6 +1199,13 @@ object ChatController { throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}") } + suspend fun setChatTTL(rh: Long?, chatType: ChatType, id: Long, chatItemTTL: ChatItemTTL?) { + val userId = currentUserId("setChatTTL") + val r = sendCmd(rh, CC.APISetChatTTL(userId, chatType, id, chatItemTTL?.seconds)) + if (r is CR.CmdOk) return + throw Exception("failed to set chat TTL: ${r.responseType} ${r.details}") + } + suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean { val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl) return when (r) { @@ -3383,8 +3396,9 @@ sealed class CC { class ApiGetUsageConditions(): CC() class ApiSetConditionsNotified(val conditionsId: Long): CC() class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List): CC() - class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC() + class APISetChatItemTTL(val userId: Long, val seconds: Long): CC() class APIGetChatItemTTL(val userId: Long): CC() + class APISetChatTTL(val userId: Long, val chatType: ChatType, val id: Long, val seconds: Long?): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() class APIGetNetworkConfig: CC() class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC() @@ -3567,6 +3581,7 @@ sealed class CC { is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" + is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}" @@ -3727,6 +3742,7 @@ sealed class CC { is ApiAcceptConditions -> "apiAcceptConditions" is APISetChatItemTTL -> "apiSetChatItemTTL" is APIGetChatItemTTL -> "apiGetChatItemTTL" + is APISetChatTTL -> "apiSetChatTTL" is APISetNetworkConfig -> "apiSetNetworkConfig" is APIGetNetworkConfig -> "apiGetNetworkConfig" is APISetNetworkInfo -> "apiSetNetworkInfo" @@ -3812,7 +3828,7 @@ sealed class CC { data class ItemRange(val from: Long, val to: Long) fun chatItemTTLStr(seconds: Long?): String { - if (seconds == null) return "none" + if (seconds == null) return "default" return seconds.toString() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 2b3cf773cc..7f5ed7b2a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -16,8 +16,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.* @@ -36,12 +38,14 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats -import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.group.ChatTTLSection +import chat.simplex.common.views.chat.group.ProgressIndicator import chat.simplex.common.views.chatlist.updateChatSettings +import chat.simplex.common.views.database.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -74,6 +78,9 @@ fun ChatInfoView( } val chatRh = chat.remoteHostId val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } + val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) } + val deletingItems = rememberSaveable(contact.id) { mutableStateOf(false) } + ChatInfoLayout( chat, contact, @@ -84,6 +91,16 @@ fun ChatInfoView( updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, + chatItemTTL = chatItemTTL, + setChatItemTTL = { + if (it == chatItemTTL.value) { + return@ChatInfoLayout + } + val previousChatTTL = chatItemTTL.value + chatItemTTL.value = it + + setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) + }, connStats = connStats, contactNetworkStatus.value, customUserProfile, @@ -173,7 +190,8 @@ fun ChatInfoView( } }, close = close, - onSearchClicked = onSearchClicked + onSearchClicked = onSearchClicked, + deletingItems = deletingItems ) } } @@ -504,6 +522,8 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State, setSendReceipts: (SendReceipts) -> Unit, + chatItemTTL: MutableState, + setChatItemTTL: (ChatItemTTL?) -> Unit, connStats: MutableState, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, @@ -520,7 +540,8 @@ fun ChatInfoLayout( syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, close: () -> Unit, - onSearchClicked: () -> Unit + onSearchClicked: () -> Unit, + deletingItems: State ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -597,6 +618,9 @@ fun ChatInfoLayout( } SectionDividerSpaced(maxBottomPadding = false) + ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + val conn = contact.activeConn if (conn != null) { SectionView { @@ -1308,6 +1332,80 @@ fun queueInfoText(info: Pair): String { return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo) } +fun setChatTTLAlert( + rhId: Long?, + chatInfo: ChatInfo, + selectedChatTTL: MutableState, + previousChatTTL: ChatItemTTL?, + progressIndicator: MutableState +) { + val defaultTTL = chatModel.chatItemTTL.value + val previouslyUsedTTL = previousChatTTL ?: defaultTTL + val newTTLToUse = selectedChatTTL.value ?: defaultTTL + + AlertManager.shared.showAlertDialog( + title = generalGetString( + if (newTTLToUse.neverExpires) { + MR.strings.disable_automatic_deletion_question + } else if (!previouslyUsedTTL.neverExpires || selectedChatTTL.value == null) { + MR.strings.change_automatic_deletion_question + } else MR.strings.enable_automatic_deletion_question), + text = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion_message else MR.strings.change_automatic_chat_deletion_message), + confirmText = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion else MR.strings.delete_messages), + onConfirm = { setChatTTL(rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) }, + onDismiss = { selectedChatTTL.value = previousChatTTL }, + onDismissRequest = { selectedChatTTL.value = previousChatTTL }, + destructive = true, + ) +} + +private fun setChatTTL( + rhId: Long?, + chatInfo: ChatInfo, + chatTTL: MutableState, + progressIndicator: MutableState, + previousChatTTL: ChatItemTTL? + ) { + progressIndicator.value = true + withBGApi { + try { + chatModel.controller.setChatTTL(rhId, chatInfo.chatType, chatInfo.apiId, chatTTL.value) + afterSetChatTTL(rhId, chatInfo, progressIndicator) + } catch (e: Exception) { + chatTTL.value = previousChatTTL + afterSetChatTTL(rhId, chatInfo, progressIndicator) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_message_deletion), e.stackTraceToString()) + } + } +} + +private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState) { + try { + val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return + if (chat.chatItems.isEmpty()) { + // replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview + withChats { + val oldChat = getChat(chat.id) + if (oldChat != null) { + replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList())) + } + } + } + if (chat.remoteHostId != chatModel.remoteHostId() || chat.id != chatModel.chatId.value) return + processLoadedChat( + chat, + navInfo, + contentTag = null, + pagination = pagination + ) + } catch (e: Exception) { + Log.e(TAG, "apiGetChat error: ${e.stackTraceToString()}") + } finally { + progressIndicator.value = false + } +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -1322,6 +1420,8 @@ fun PreviewChatInfoLayout() { User.sampleData, sendReceipts = remember { mutableStateOf(SendReceipts.Yes) }, setSendReceipts = {}, + chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) }, + setChatItemTTL = {}, localAlias = "", connectionCode = "123", developerTools = false, @@ -1338,7 +1438,8 @@ fun PreviewChatInfoLayout() { syncContactConnectionForce = {}, verifyClicked = {}, close = {}, - onSearchClicked = {} + onSearchClicked = {}, + deletingItems = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index 6419aa884d..3613ceaa7c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -34,7 +34,16 @@ suspend fun apiLoadMessages( // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last) || !isActive) return@coroutineScope + processLoadedChat(chat, navInfo, contentTag, pagination, visibleItemIndexesNonReversed) +} +suspend fun processLoadedChat( + chat: Chat, + navInfo: NavigationInfo, + contentTag: MsgContentTag?, + pagination: ChatPagination, + visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } +) { val chatState = chatModel.chatStateForContent(contentTag) val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState val oldItems = chatModel.chatItemsForContent(contentTag).value @@ -70,7 +79,7 @@ suspend fun apiLoadMessages( is ChatPagination.Before -> { newItems.addAll(oldItems) val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } - if (indexInCurrentItems == -1) return@coroutineScope + if (indexInCurrentItems == -1) return val (newIds, _) = mapItemsToIds(chat.chatItems) val wasSize = newItems.size val (oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) = removeDuplicatesAndModifySplitsOnBeforePagination( @@ -87,7 +96,7 @@ suspend fun apiLoadMessages( is ChatPagination.After -> { newItems.addAll(oldItems) val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } - if (indexInCurrentItems == -1) return@coroutineScope + if (indexInCurrentItems == -1) return val mappedItems = mapItemsToIds(chat.chatItems) val newIds = mappedItems.first diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 9b2986ef83..b2963c4d4a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString @@ -39,6 +40,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.database.TtlOptions import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* @@ -55,7 +57,10 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin 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)) } + val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) } + val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) } val scope = rememberCoroutineScope() + GroupChatInfoLayout( chat, groupInfo, @@ -66,6 +71,16 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, + chatItemTTL = chatItemTTL, + setChatItemTTL = { + if (it == chatItemTTL.value) { + return@GroupChatInfoLayout + } + val previousChatTTL = chatItemTTL.value + chatItemTTL.value = it + + setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) + }, members = remember { chatModel.groupMembers }.value .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } .sortedByDescending { it.memberRole }, @@ -125,7 +140,8 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin manageGroupLink = { ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } }, - onSearchClicked = onSearchClicked + onSearchClicked = onSearchClicked, + deletingItems = deletingItems ) } } @@ -285,6 +301,8 @@ fun ModalData.GroupChatInfoLayout( currentUser: User, sendReceipts: State, setSendReceipts: (SendReceipts) -> Unit, + chatItemTTL: MutableState, + setChatItemTTL: (ChatItemTTL?) -> Unit, members: List, developerTools: Boolean, onLocalAliasChanged: (String) -> Unit, @@ -300,7 +318,8 @@ fun ModalData.GroupChatInfoLayout( leaveGroup: () -> Unit, manageGroupLink: () -> Unit, close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, - onSearchClicked: () -> Unit + onSearchClicked: () -> Unit, + deletingItems: State ) { val listState = remember { appBarHandler.listState } val scope = rememberCoroutineScope() @@ -394,7 +413,10 @@ fun ModalData.GroupChatInfoLayout( } val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs SectionTextFooter(stringResource(footerId)) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + + ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true) SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { if (groupInfo.canAddMembers) { @@ -463,6 +485,26 @@ fun ModalData.GroupChatInfoLayout( } } +@Composable +fun ChatTTLSection(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { + Box { + SectionView { + TtlOptions( + chatItemTTL, + enabled = remember { derivedStateOf { !deletingItems.value } }, + onSelected = setChatItemTTL, + default = chatModel.chatItemTTL + ) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + } + if (deletingItems.value) { + Box(Modifier.matchParentSize()) { + ProgressIndicator() + } + } + } +} + @Composable private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { Column( @@ -770,12 +812,14 @@ fun PreviewGroupChatInfoLayout() { User.sampleData, sendReceipts = remember { mutableStateOf(SendReceipts.Yes) }, setSendReceipts = {}, + chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) }, + setChatItemTTL = {}, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, onLocalAliasChanged = {}, groupLink = null, scrollToItemId = remember { mutableStateOf(null) }, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 956ee575de..987a80e7c0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -1,9 +1,8 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index d951f1f812..4a3e1cda54 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* @@ -108,6 +109,9 @@ fun DatabaseView() { } }, onChatItemTTLSelected = { + if (it == null) { + return@DatabaseLayout + } val oldValue = chatItemTTL.value chatItemTTL.value = it if (it < oldValue) { @@ -158,7 +162,7 @@ fun DatabaseLayout( exportArchive: () -> Unit, deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, - onChatItemTTLSelected: (ChatItemTTL) -> Unit, + onChatItemTTLSelected: (ChatItemTTL?) -> Unit, disconnectAllHosts: () -> Unit, ) { val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote @@ -300,21 +304,25 @@ private fun setChatItemTTLAlert( } @Composable -private fun TtlOptions(current: State, enabled: State, onSelected: (ChatItemTTL) -> Unit) { +fun TtlOptions( + current: State, + enabled: State, + onSelected: (ChatItemTTL?) -> Unit, + default: State? = null +) { val values = remember { - val all: ArrayList = arrayListOf(ChatItemTTL.None, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day) - if (current.value is ChatItemTTL.Seconds) { - all.add(current.value) + val all: ArrayList = arrayListOf(ChatItemTTL.None, ChatItemTTL.Year, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day) + val currentValue = current.value + if (currentValue is ChatItemTTL.Seconds) { + all.add(currentValue) } - all.map { - when (it) { - is ChatItemTTL.None -> it to generalGetString(MR.strings.chat_item_ttl_none) - is ChatItemTTL.Day -> it to generalGetString(MR.strings.chat_item_ttl_day) - is ChatItemTTL.Week -> it to generalGetString(MR.strings.chat_item_ttl_week) - is ChatItemTTL.Month -> it to generalGetString(MR.strings.chat_item_ttl_month) - is ChatItemTTL.Seconds -> it to String.format(generalGetString(MR.strings.chat_item_ttl_seconds), it.secs) - } + val options: MutableList> = all.map { it to it.text }.toMutableList() + + if (default != null) { + options.add(null to String.format(generalGetString(MR.strings.chat_item_ttl_default), default.value.text)) } + + options } ExposedDropDownSettingRow( generalGetString(MR.strings.delete_messages_after), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 954c22abee..ded58ac9b3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -540,6 +540,12 @@ Notifications + Disable automatic message deletion? + Change automatic message deletion? + Messages in this chat will never be deleted. + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Disable delete messages + Delete chat messages from your device. connect @@ -1396,7 +1402,9 @@ 1 day 1 week 1 month + 1 year %s second(s) + default (%s) Messages This setting applies to messages in your current chat profile Delete messages after