From a124dc4cc4b212e9c0ddcae87df1dd2eb2ca17a4 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 21 Jul 2026 03:44:53 +0000 Subject: [PATCH] feat: add support for unread message and room highlighting (#49) Reviewed-on: https://git.reya.su/reya/coop-mobile/pulls/49 --- .../kotlin/su/reya/coop/screens/HomeScreen.kt | 77 ++++++++++++++----- .../commonMain/kotlin/su/reya/coop/Room.kt | 3 +- .../su/reya/coop/repository/ChatRepository.kt | 29 ++++++- .../coop/viewmodel/ChatScreenViewModel.kt | 3 + 4 files changed, 86 insertions(+), 26 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt index 91fe388..7f29360 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -655,27 +657,39 @@ fun NewRequests(requests: List) { else -> "" } + val totalUnread = requests.sumOf { it.unreadCount } + ListItem( modifier = Modifier.clickable { navigator.navigate(Screen.RequestList) }, leadingContent = { - Box( - modifier = Modifier - .size(48.dp) - .clip(MaterialShapes.Clover4Leaf.toShape()), - contentAlignment = Alignment.Center + BadgedBox( + badge = { + if (totalUnread > 0) { + Badge { + Text(totalUnread.toString()) + } + } + } ) { - Surface( - modifier = Modifier.size(48.dp), - color = MaterialTheme.colorScheme.tertiaryContainer, + Box( + modifier = Modifier + .size(48.dp) + .clip(MaterialShapes.Clover4Leaf.toShape()), + contentAlignment = Alignment.Center ) { - Box(contentAlignment = Alignment.Center) { - Icon( - painter = painterResource(Res.drawable.ic_request), - contentDescription = "Requests", - tint = MaterialTheme.colorScheme.onTertiaryFixed - ) + Surface( + modifier = Modifier.size(48.dp), + color = MaterialTheme.colorScheme.tertiaryContainer, + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(Res.drawable.ic_request), + contentDescription = "Requests", + tint = MaterialTheme.colorScheme.onTertiaryFixed + ) + } } } } @@ -683,14 +697,19 @@ fun NewRequests(requests: List) { headlineContent = { Text( text = "Requests", - style = MaterialTheme.typography.titleMediumEmphasized + style = MaterialTheme.typography.titleMediumEmphasized.copy( + fontWeight = if (totalUnread > 0) FontWeight.SemiBold else FontWeight.Normal + ) ) }, supportingContent = { if (supportingText.isNotEmpty()) { Text( text = supportingText, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = if (totalUnread > 0) FontWeight.SemiBold else FontWeight.Normal, + color = if (totalUnread > 0) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline + ), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -712,22 +731,35 @@ fun ChatRoom(room: Room, onClick: () -> Unit) { ListItem( modifier = Modifier.clickable(onClick = onClick), leadingContent = { - Avatar(picture = roomState.picture, description = roomState.picture) + BadgedBox( + badge = { + if (room.unreadCount > 0) { + Badge { + Text(room.unreadCount.toString()) + } + } + } + ) { + Avatar(picture = roomState.picture, description = roomState.picture) + } }, headlineContent = { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = roomState.name, - style = MaterialTheme.typography.titleMediumEmphasized, + style = MaterialTheme.typography.titleMediumEmphasized.copy( + fontWeight = if (room.unreadCount > 0) FontWeight.SemiBold else FontWeight.Normal + ), modifier = Modifier.weight(1f) ) Text( text = room.createdAt.ago(), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline + color = MaterialTheme.colorScheme.outline, + textAlign = TextAlign.End, ) } }, @@ -735,7 +767,10 @@ fun ChatRoom(room: Room, onClick: () -> Unit) { if (!room.lastMessage.isNullOrBlank()) { Text( text = room.lastMessage ?: "", - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = if (room.unreadCount > 0) FontWeight.SemiBold else FontWeight.Normal, + color = if (room.unreadCount > 0) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline + ), overflow = TextOverflow.Ellipsis, maxLines = 1, ) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt index d761aad..bcfe38f 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt @@ -30,7 +30,8 @@ data class Room( val subject: String?, val members: Set, val kind: RoomKind = RoomKind.default(), - val lastMessage: String? = null + val lastMessage: String? = null, + val unreadCount: Int = 0 ) : Comparable { override fun compareTo(other: Room): Int { return this.createdAt.asSecs().compareTo(other.createdAt.asSecs()) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt b/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt index 2cabfe3..cb6ec28 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/repository/ChatRepository.kt @@ -130,6 +130,19 @@ class ChatRepository( return _state.value.rooms[id] } + fun markAsRead(roomId: Long) { + _state.update { currentState -> + val rooms = currentState.rooms.toMutableMap() + val room = rooms[roomId] + if (room != null && room.unreadCount > 0) { + rooms[roomId] = room.copy(unreadCount = 0) + currentState.copy(rooms = rooms) + } else { + currentState + } + } + } + fun refreshChatRooms() { scope.launch(defaultDispatcher) { try { @@ -140,10 +153,14 @@ class ChatRepository( val existing = newMap[dbRoom.id] // Only update if the database version is newer or equal if (existing == null || dbRoom.createdAt.asSecs() >= existing.createdAt.asSecs()) { - // Preserve Ongoing kind if already marked as such in memory + // Preserve Ongoing kind and unreadCount status if already marked as such in memory val mergedKind = if (existing?.kind == RoomKind.Ongoing) RoomKind.Ongoing else dbRoom.kind - newMap[dbRoom.id] = dbRoom.copy(kind = mergedKind) + val mergedUnreadCount = existing?.unreadCount ?: 0 + newMap[dbRoom.id] = dbRoom.copy( + kind = mergedKind, + unreadCount = mergedUnreadCount + ) } } currentState.copy(rooms = newMap) @@ -237,14 +254,18 @@ class ChatRepository( if (existingRoom == null) { // New room discovery - val newRoom = Room.new(event, currentUser, roomId).copy(kind = newKind) + val newRoom = Room.new(event, currentUser, roomId).copy( + kind = newKind, + unreadCount = if (isFromMe) 0 else 1 + ) rooms[newRoom.id] = newRoom } else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) { // Only update preview if message is newer (handles sync/late arrivals) rooms[roomId] = existingRoom.copy( lastMessage = event.content(), createdAt = event.createdAt(), - kind = newKind + kind = newKind, + unreadCount = if (isFromMe) existingRoom.unreadCount else existingRoom.unreadCount + 1 ) } else if (isFromMe && existingRoom.kind != RoomKind.Ongoing) { // Even if it's an older message, if it's from me, the room is ongoing diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt index bec9f34..60fd994 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/ChatScreenViewModel.kt @@ -42,6 +42,8 @@ class ChatScreenViewModel( messages.clear() messages.addAll(initialMessages.distinctBy { it.id() }) loading = false + // Mark the room as read once messages are loaded + chatRepository.markAsRead(id) } } @@ -55,6 +57,7 @@ class ChatScreenViewModel( if (event.roomId() == id) { if (messages.none { it.id() == event.id() }) { messages.add(0, event) + chatRepository.markAsRead(id) } } else { newOtherMessages++