core: support chat unread stats (#5844)

This commit is contained in:
spaced4ndy authored and GitHub committed 2025-04-23 14:15:17 +00:00
1 parent b9adc7b345
commit 5262b8caca
9 files changed
+300 -77

No files matched your search

@@ -898,6 +898,28 @@ object ChatModel {
}
}
fun increaseGroupSupportChatsUnreadCounter(rhId: Long?, chatId: ChatId) {
changeGroupSupportChatsUnreadCounter(rhId, chatId, 1)
}
fun decreaseGroupSupportChatsUnreadCounter(rhId: Long?, chatId: ChatId, by: Int = 1) {
changeGroupSupportChatsUnreadCounter(rhId, chatId, -by)
}
private fun changeGroupSupportChatsUnreadCounter(rhId: Long?, chatId: ChatId, by: Int = 0) {
if (by == 0) return
val i = getChatIndex(rhId, chatId)
if (i >= 0) {
val chat = chats.value[i]
chats[i] = chat.copy(
chatStats = chat.chatStats.copy(
supportChatsUnreadCount = (chat.chatStats.supportChatsUnreadCount + by).coerceAtLeast(0),
)
)
}
}
fun increaseGroupReportsCounter(rhId: Long?, chatId: ChatId) {
changeGroupReportsCounter(rhId, chatId, 1)
}
@@ -1315,6 +1337,8 @@ data class Chat(
val unreadMentions: Int = 0,
// actual only via getChats() and getChat(.initial), otherwise, zero
val reportsCount: Int = 0,
// actual only via getChats() and getChat(.initial), otherwise, zero
val supportChatsUnreadCount: Int = 0,
val minUnreadItemId: Long = 0,
// actual only via getChats(), otherwise, false
val unreadChat: Boolean = false
@@ -2559,10 +2559,12 @@ object ChatController {
if (active(r.user)) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.addChatItem(rhId, cInfo, cItem)
// TODO [knocking] increase support chats unread count; move `isActiveReport` checks inside model functions?
if (cItem.isActiveReport) {
chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id)
}
if (cInfo.groupChatScope() != null && cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
chatModel.chatsContext.increaseGroupSupportChatsUnreadCounter(rhId, cInfo.id)
}
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
@@ -63,6 +63,7 @@ fun ChatView(
onComposed: suspend (chatId: String) -> Unit
) {
val showSearch = rememberSaveable { mutableStateOf(false) }
val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }
// They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..."
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
val activeChatInfo = remember { derivedStateOf {
@@ -78,7 +79,7 @@ fun ChatView(
} }
val user = chatModel.currentUser.value
val chatInfo = activeChatInfo.value
if (chatInfo == null || user == null) {
if (chat == null || chatInfo == null || user == null) {
LaunchedEffect(Unit) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@@ -139,6 +140,7 @@ fun ChatView(
chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0
}
}
val reportsCount = reportsCount(chatInfo.id)
val clipboard = LocalClipboardManager.current
CompositionLocalProvider(
LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false),
@@ -340,7 +342,7 @@ fun ChatView(
}
}
},
showGroupReports = {
showReportsOrSupportChats = {
val info = activeChatInfo.value ?: return@ChatLayout
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
@@ -348,7 +350,30 @@ fun ChatView(
}
hideKeyboard(view)
scope.launch {
showGroupReportsView(staleChatId, scrollToItemId, info)
if (reportsCount > 0) {
showGroupReportsView(staleChatId, scrollToItemId, info)
} else if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
ModalManager.end.showCustomModal { close ->
MemberSupportView(
chatRh,
chat,
info.groupInfo,
scrollToItemId,
close
)
}
} else if (info is ChatInfo.Group) {
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null)
val supportChatInfo = ChatInfo.Group(info.groupInfo, groupChatScope = scopeInfo)
scope.launch {
showMemberSupportChatView(
chatModel.chatId,
scrollToItemId = scrollToItemId,
supportChatInfo,
scopeInfo
)
}
}
}
},
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
@@ -599,6 +624,9 @@ fun ChatView(
withBGApi {
withContext(Dispatchers.Main) {
chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
if (chatsCtx.secondaryContextFilter != null) {
chatModel.chatsContext.decreaseGroupSupportChatsUnreadCounter(chatRh, chatInfo.id)
}
ntfManager.cancelNotificationsForChat(chatInfo.id)
chatModel.controller.apiChatItemsRead(
chatRh,
@@ -705,7 +733,7 @@ fun ChatLayout(
selectedChatItems: MutableState<Set<Long>?>,
back: () -> Unit,
info: () -> Unit,
showGroupReports: () -> Unit,
showReportsOrSupportChats: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
@@ -856,9 +884,10 @@ fun ChatLayout(
}
}
val reportsCount = reportsCount(chatInfo?.id)
val supportChatsUnreadCount = supportChatsUnreadCount(chatInfo?.id)
if (oneHandUI.value && chatBottomBar.value) {
if (chatsCtx.secondaryContextFilter == null && reportsCount > 0) { // TODO [knocking] support chats unread count toolbar
ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports)
if (chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportChatsUnreadCount > 0)) {
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = true, showReportsOrSupportChats)
} else {
StatusBarBackground()
}
@@ -914,8 +943,8 @@ fun ChatLayout(
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
}
}
if (reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports)
if ((reportsCount > 0 || supportChatsUnreadCount > 0) && (!oneHandUI.value || !chatBottomBar.value)) {
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = false, showReportsOrSupportChats)
}
}
}
@@ -1147,10 +1176,11 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
}
@Composable
private fun ReportedCountToolbar(
private fun SupportChatsCountToolbar(
reportsCount: Int,
supportChatsUnreadCount: Int,
withStatusBar: Boolean,
showGroupReports: () -> Unit
showReportsOrSupportChats: () -> Unit
) {
Box {
val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp
@@ -1159,18 +1189,25 @@ private fun ReportedCountToolbar(
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
.background(MaterialTheme.colors.background)
.clickable(onClick = showGroupReports)
.clickable(onClick = showReportsOrSupportChats)
.padding(top = statusBarPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
val iconColor = if (reportsCount == 0) MaterialTheme.colors.primary else MaterialTheme.colors.error
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = iconColor)
Spacer(Modifier.width(4.dp))
Text(
if (reportsCount == 1) {
stringResource(MR.strings.group_reports_active_one)
if (supportChatsUnreadCount == 0) {
if (reportsCount == 1) {
stringResource(MR.strings.group_reports_active_one)
} else {
stringResource(MR.strings.group_reports_active).format(reportsCount)
}
} else if (reportsCount == 0) {
stringResource(MR.strings.group_new_support_messages).format(supportChatsUnreadCount)
} else {
stringResource(MR.strings.group_reports_active).format(reportsCount)
String.format(generalGetString(MR.strings.group_reports_active_new_support_messages), reportsCount, supportChatsUnreadCount)
},
style = MaterialTheme.typography.button
)
@@ -1255,9 +1292,10 @@ fun BoxScope.ChatItemsList(
}
val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
val reportsCount = reportsCount(chatInfo.id)
val supportChatsUnreadCount = supportChatsUnreadCount(chatInfo.id)
val topPaddingToContent = topPaddingToContent(
chatView = chatsCtx.secondaryContextFilter == null,
additionalTopBar = chatsCtx.secondaryContextFilter == null && reportsCount > 0 // TODO [knocking] && support chats unread count > 0 ?
additionalTopBar = chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportChatsUnreadCount > 0)
)
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
val numberOfBottomAppBars = numberOfBottomAppBars()
@@ -1597,7 +1635,7 @@ fun BoxScope.ChatItemsList(
),
reverseLayout = true,
additionalBarOffset = composeViewHeight,
additionalTopBar = rememberUpdatedState(chatsCtx.secondaryContextFilter == null && reportsCount > 0), // TODO [knocking] && support chats unread count > 0 ?
additionalTopBar = rememberUpdatedState(chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportChatsUnreadCount > 0)),
chatBottomBar = remember { appPrefs.chatBottomBar.state }
) {
val mergedItemsValue = mergedItems.value
@@ -2282,6 +2320,15 @@ fun reportsCount(staleChatId: String?): Int {
}
}
@Composable
fun supportChatsUnreadCount(staleChatId: String?): Int {
return if (staleChatId?.startsWith("#") != true) {
0
} else {
remember(staleChatId) { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId }?.chatStats } }.value?.supportChatsUnreadCount ?: 0
}
}
private fun reversedChatItemsStatic(chatsCtx: ChatModel.ChatsContext): List<ChatItem> =
chatsCtx.chatItems.value.asReversed()
@@ -2980,7 +3027,7 @@ fun PreviewChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showGroupReports = {},
showReportsOrSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
@@ -3058,7 +3105,7 @@ fun PreviewGroupChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showGroupReports = {},
showReportsOrSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
@@ -27,7 +27,6 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.setGroupMembers
import chat.simplex.common.views.chatlist.unreadCountStr
import chat.simplex.common.views.newchat.AddContactLearnMore
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.launch
@@ -364,6 +364,8 @@ fun ChatPreviewView(
progressView()
} else if (chat.chatStats.reportsCount > 0) {
GroupReportsIcon()
} else if (chat.chatStats.supportChatsUnreadCount > 0) {
GroupSupportUnreadIcon()
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
@@ -559,6 +561,18 @@ fun GroupReportsIcon() {
)
}
@Composable
fun GroupSupportUnreadIcon() {
Icon(
painterResource(MR.images.ic_flag),
contentDescription = null,
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(21.sp.toDp())
.offset(x = 2.sp.toDp())
)
}
@Composable
private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String {
return if (groupInfo.membership.memberIncognito)
@@ -465,6 +465,8 @@
<string name="group_reports_active_one">1 report</string>
<string name="group_reports_active">%d reports</string>
<string name="group_reports_member_reports">Member reports</string>
<string name="group_new_support_messages">%d new support messages</string>
<string name="group_reports_active_new_support_messages">%1$d reports, %2$d new support messages</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>