From 7f9c4ede0282f0868a3d156a362ee0b68f1f0fcf Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 21 Oct 2022 15:29:08 +0300 Subject: [PATCH] android: Mark chat unread (#1235) * android: Mark chat unread * Fix * Fix2 * Fix3 * Refactoring * Icon * update icon Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 2 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 10 +++ .../chat/simplex/app/views/chat/ChatView.kt | 39 ++++++--- .../app/views/chatlist/ChatListNavLinkView.kt | 81 ++++++++++++++++--- .../app/views/chatlist/ChatPreviewView.kt | 4 +- .../app/src/main/res/values-de/strings.xml | 1 + .../app/src/main/res/values-ru/strings.xml | 1 + .../app/src/main/res/values/strings.xml | 1 + 8 files changed, 114 insertions(+), 25 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index c95c152b27..61edae444e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -387,7 +387,7 @@ data class Chat ( val id: String get() = chatInfo.id @Serializable - data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0) + data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false) @Serializable data class ServerInfo(val networkStatus: NetworkStatus) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 7c35c13947..fe1eab7be2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -714,6 +714,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return false } + suspend fun apiChatUnread(type: ChatType, id: Long, unreadChat: Boolean): Boolean { + val r = sendCmd(CC.ApiChatUnread(type, id, unreadChat)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiChatUnread bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiReceiveFile(fileId: Long): AChatItem? { val r = sendCmd(CC.ReceiveFile(fileId)) return when (r) { @@ -1425,6 +1432,7 @@ sealed class CC { class ApiAcceptContact(val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() + class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ReceiveFile(val fileId: Long): CC() val cmdString: String get() = when (this) { @@ -1486,6 +1494,7 @@ sealed class CC { is ApiEndCall -> "/_call end @${contact.apiId}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" + is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" } @@ -1548,6 +1557,7 @@ sealed class CC { is ApiEndCall -> "apiEndCall" is ApiCallStatus -> "apiCallStatus" is ApiChatRead -> "apiChatRead" + is ApiChatUnread -> "apiChatUnread" is ReceiveFile -> "receiveFile" } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 91547c2b87..51bfd329d7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -51,7 +51,7 @@ import kotlin.math.sign @Composable fun ChatView(chatModel: ChatModel) { - var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } + val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) } val searchText = rememberSaveable { mutableStateOf("") } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() @@ -68,20 +68,23 @@ fun ChatView(chatModel: ChatModel) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .collect { - activeChat = if (chatModel.chatId.value == null) { - null - } else { - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - chatModel.getChat(chatModel.chatId.value!!) + if (activeChat.value?.id != chatModel.chatId.value) { + activeChat.value = if (chatModel.chatId.value == null) { + null + } else { + // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly + // Also for situation when chatId changes after clicking in notification, etc + chatModel.getChat(chatModel.chatId.value!!) + } } + markUnreadChatAsRead(activeChat, chatModel) } } - if (activeChat == null || user == null) { + if (activeChat.value == null || user == null) { chatModel.chatId.value = null } else { - val chat = activeChat!! + val chat = activeChat.value!! BackHandler { chatModel.chatId.value = null } // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) @@ -119,7 +122,7 @@ fun ChatView(chatModel: ChatModel) { val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId) ModalManager.shared.showModalCloseable(true) { close -> ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) { - activeChat = it + activeChat.value = it } } } else if (cInfo is ChatInfo.Group) { @@ -789,6 +792,22 @@ private fun bottomEndFloatingButton( } } +private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: ChatModel) { + val chat = activeChat.value + if (chat?.chatStats?.unreadChat != true) return + withApi { + val success = chatModel.controller.apiChatUnread( + chat.chatInfo.chatType, + chat.chatInfo.apiId, + false + ) + if (success && chat.id == activeChat.value?.id) { + activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) + chatModel.replaceChat(chat.id, activeChat.value!!) + } + } +} + private fun providerForGallery( listStateIndex: Int, chatItems: List, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 1f93647ea2..9e17494a9a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -5,12 +5,13 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.TheaterComedy +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* 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.graphicsLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -29,12 +30,13 @@ import kotlinx.datetime.Clock @Composable fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { val showMenu = remember { mutableStateOf(false) } - var showMarkRead by remember { mutableStateOf(false) } + val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) { + chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat + } val stopped = chatModel.chatRunning.value == false - LaunchedEffect(chat.id, chat.chatStats.unreadCount > 0) { + LaunchedEffect(chat.id) { showMenu.value = false delay(500L) - showMarkRead = chat.chatStats.unreadCount > 0 } when (chat.chatInfo) { is ChatInfo.Direct -> @@ -123,6 +125,8 @@ suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) { fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { if (showMarkRead) { MarkReadChatAction(chat, chatModel, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) } ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) ClearChatAction(chat, chatModel, showMenu) @@ -141,6 +145,8 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM else -> { if (showMarkRead) { MarkReadChatAction(chat, chatModel, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) } ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) ClearChatAction(chat, chatModel, showMenu) @@ -167,6 +173,30 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState< ) } +@Composable +fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + DropdownMenuItem({ + markChatUnread(chat, chatModel) + showMenu.value = false + }) { + Row { + Text( + stringResource(R.string.mark_unread), + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = MaterialTheme.colors.onBackground + ) + Icon( + Icons.Outlined.MarkChatUnread, + stringResource(R.string.mark_unread), + tint = MaterialTheme.colors.onBackground + ) + } + } +} + @Composable fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState) { ItemAction( @@ -290,18 +320,45 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ) } -fun markChatRead(chat: Chat, chatModel: ChatModel) { - // Just to be sure - if (chat.chatStats.unreadCount == 0) return - - val minUnreadItemId = chat.chatStats.minUnreadItemId - chatModel.markChatItemsRead(chat.chatInfo) +fun markChatRead(c: Chat, chatModel: ChatModel) { + var chat = c withApi { - chatModel.controller.apiChatRead( + if (chat.chatStats.unreadCount > 0) { + val minUnreadItemId = chat.chatStats.minUnreadItemId + chatModel.markChatItemsRead(chat.chatInfo) + chatModel.controller.apiChatRead( + chat.chatInfo.chatType, + chat.chatInfo.apiId, + CC.ItemRange(minUnreadItemId, chat.chatItems.last().id) + ) + chat = chatModel.getChat(chat.id) ?: return@withApi + } + if (chat.chatStats.unreadChat) { + val success = chatModel.controller.apiChatUnread( + chat.chatInfo.chatType, + chat.chatInfo.apiId, + false + ) + if (success) { + chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } + } + } +} + +fun markChatUnread(chat: Chat, chatModel: ChatModel) { + // Just to be sure + if (chat.chatStats.unreadChat) return + + withApi { + val success = chatModel.controller.apiChatUnread( chat.chatInfo.chatType, chat.chatInfo.apiId, - CC.ItemRange(minUnreadItemId, chat.chatItems.last().id) + true ) + if (success) { + chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index ccde6d61ab..9a8e9f4762 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -136,13 +136,13 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD ) val n = chat.chatStats.unreadCount val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) - if (n > 0) { + if (n > 0 || chat.chatStats.unreadChat) { Box( Modifier.padding(top = 24.dp), contentAlignment = Alignment.Center ) { Text( - unreadCountStr(n), + if (n > 0) unreadCountStr(n) else "", color = MaterialTheme.colors.onPrimary, fontSize = 11.sp, modifier = Modifier diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index cbe65cca0f..98c0bd4a53 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -271,6 +271,7 @@ Kontakt löschen Gruppe löschen Als gelesen markieren + Mark unread Kontaktname festlegen diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 23da46379e..d7f8a27c67 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -271,6 +271,7 @@ Удалить Удалить Прочитано + Не прочитано Имя контакта diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 9e88a1cf3f..02bde2e1b5 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -271,6 +271,7 @@ Delete Delete Mark read + Mark unread Set contact name