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:
IanRDavies
2022-02-22 15:07:55 +00:00
committed by GitHub
parent 0d88fcc758
commit efa22715d5
6 changed files with 91 additions and 55 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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
) {

View File

@@ -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

View File

@@ -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)
)
}
}
}
}

View File

@@ -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)
}
}