android: unread message counter (#348)
* add unread counter to chats * run unread clear on message view for more than a second * track minUnreadItemId
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user