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 ff4d78a42..63c36affa 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 @@ -27,7 +27,7 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale } } - fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null + fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) @@ -66,13 +66,16 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) { // update previews val i = getChatIndex(cInfo.id) + val chat: Chat if (i >= 0) { - val chat = chats[i] + chat = chats[i] chats[i] = chat.copy( chatItems = arrayListOf(cItem), chatStats = - if (cItem.meta.itemStatus is CIStatus.RcvNew) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1) + if (cItem.meta.itemStatus is CIStatus.RcvNew) { + val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId) + } else chat.chatStats ) @@ -85,16 +88,29 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale // add to current chat if (chatId.value == cInfo.id) { chatItems.add(cItem) - if (cItem.meta.itemStatus is CIStatus.RcvNew) { - // TODO mark item read via api and model -// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { -// if self.chatId == cInfo.id { -// SimpleX.markChatItemRead(cInfo, cItem) -// } -// } - } } } + + fun markChatItemsRead(cInfo: ChatInfo) { + val chatIdx = getChatIndex(cInfo.id) + // update current chat + if (chatId.value == cInfo.id) { + var i = 0 + while (i < chatItems.count()) { + val item = chatItems[i] + if (item.meta.itemStatus is CIStatus.RcvNew) { + chatItems[i] = item.copy(meta=item.meta.copy(itemStatus = CIStatus.RcvRead())) + } + i += 1 + } + val chat = chats[chatIdx] + chats[chatIdx] = chat.copy( + chatItems = chatItems, + chatStats = chat.chatStats.copy(unreadCount = 0, minUnreadItemId = chat.chatItems.last().id + 1) + ) + } + + } // // func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { // // update previews @@ -124,33 +140,6 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale // } // } // -// func markChatItemsRead(_ cInfo: ChatInfo) { -// // update preview -// if let chat = getChat(cInfo.id) { -// chat.chatStats = ChatStats() -// } -// // update current chat -// if chatId == cInfo.id { -// var i = 0 -// while i < chatItems.count { -// if case .rcvNew = chatItems[i].meta.itemStatus { -// chatItems[i].meta.itemStatus = .rcvRead -// } -// i = i + 1 -// } -// } -// } -// -// func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { -// // update preview -// if let i = getChatIndex(cInfo.id) { -// chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 -// } -// // update current chat -// if chatId == cInfo.id, let j = chatItems.firstIndex(where: { $0.id == cItem.id }) { -// chatItems[j].meta.itemStatus = .rcvRead -// } -// } // // func popChat(_ id: String) { // if let i = getChatIndex(id) { @@ -443,7 +432,7 @@ class AChatItem ( ) @Serializable -class ChatItem ( +data class ChatItem ( val chatDir: CIDirection, val meta: CIMeta, val content: CIContent @@ -488,7 +477,7 @@ sealed class CIDirection { } @Serializable -class CIMeta ( +data class CIMeta ( val itemId: Long, val itemTs: Instant, val itemText: String, 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 a5b5a0e9a..bfb255dad 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 @@ -4,7 +4,8 @@ import android.util.Log import androidx.compose.runtime.mutableStateOf import chat.simplex.app.* import kotlinx.coroutines.* -import kotlinx.datetime.* +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.serialization.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject 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 b775054b8..f5369afd1 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 @@ -8,8 +8,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -24,9 +23,9 @@ import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.helpers.ChatInfoImage import chat.simplex.app.views.helpers.withApi import com.google.accompanist.insets.* -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.datetime.Clock +import java.util.* @ExperimentalAnimatedInsets @DelicateCoroutinesApi @@ -35,6 +34,21 @@ fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } if (chat != null) { + + // TODO a more advanced version would mark as read only if in view + LaunchedEffect(chat.chatItems) { + delay(1000L) + if (chat.chatItems.count() > 0) { + chatModel.markChatItemsRead(chat.chatInfo) + withApi { + chatModel.controller.apiChatRead( + chat.chatInfo.chatType, + chat.chatInfo.apiId, + CC.ItemRange(chat.chatStats.minUnreadItemId, chat.chatItems.last().id) + ) + } + } + } ChatLayout(chat, chatModel.chatItems, back = { nav.popBackStack() }, info = { nav.navigate(Pages.ChatInfo.route) }, @@ -96,10 +110,11 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) { modifier = Modifier.padding(10.dp) ) } - Row(Modifier - .padding(horizontal = 68.dp) - .fillMaxWidth() - .clickable(onClick = info), + Row( + Modifier + .padding(horizontal = 68.dp) + .fillMaxWidth() + .clickable(onClick = info), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 087703775..bd8c40a0f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import androidx.navigation.NavOptions import chat.simplex.app.Pages import chat.simplex.app.model.Chat import chat.simplex.app.model.ChatModel 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 87b6b0815..c59558085 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 @@ -1,13 +1,12 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -16,6 +15,7 @@ import chat.simplex.app.model.* import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.ChatInfoImage +import chat.simplex.app.views.helpers.badgeLayout import kotlinx.datetime.Clock @Composable @@ -59,9 +59,22 @@ fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { verticalArrangement = Arrangement.Top) { Text(ts, color = HighOrLowlight, - style = MaterialTheme.typography.body2 + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(bottom=5.dp) ) - // TODO unread count + + if (chat.chatStats.unreadCount > 0) { + Text( + chat.chatStats.unreadCount.toString(), + color = MaterialTheme.colors.onPrimary, + style = MaterialTheme.typography.body2, + modifier = Modifier + .background(MaterialTheme.colors.primary, shape = CircleShape) + .align(Alignment.End) + .badgeLayout() + .padding(2.dp) + ) + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt new file mode 100644 index 000000000..a8d9f0e43 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt @@ -0,0 +1,17 @@ +package chat.simplex.app.views.helpers + +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout + +fun Modifier.badgeLayout() = + layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + + // based on the expectation of only one line of text + val minPadding = placeable.height / 4 + + val width = maxOf(placeable.width + minPadding, placeable.height) + layout(width, placeable.height) { + placeable.place((width - placeable.width) / 2, 0) + } + }