Compare commits

..

2 Commits

Author SHA1 Message Date
Codex
6a1961e045 android: fix unread badge reset when chat is read
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled
2026-03-09 21:34:03 +03:00
Codex
8101cbbffd android: chat list preview cleanup without emoji icons 2026-03-09 21:33:44 +03:00
5 changed files with 40 additions and 15 deletions

View File

@@ -521,3 +521,12 @@
- Bound chats top bar title to realtime state: - Bound chats top bar title to realtime state:
- shows `Connecting...` while reconnect/initial connect is in progress, - shows `Connecting...` while reconnect/initial connect is in progress,
- shows regular page title once connected. - shows regular page title once connected.
### Step 84 - Chats list preview icon policy cleanup
- Updated chat last-message preview text to remove emoji prefixes.
- Switched media-type preview prefixes to plain text labels (`Photo`, `Video`, `Voice`, etc.) to match Material-icons-only UI policy.
### Step 85 - Unread counter fix for active/read chats
- Added `ChatDao.markChatRead(chatId)` to clear `unread_count` and `unread_mentions_count` in Room.
- Applied optimistic local unread reset on `markMessageRead(...)` in message repository.
- Fixed realtime unread logic: incoming messages in currently active chat no longer increment unread badge.

View File

@@ -139,6 +139,16 @@ interface ChatDao {
) )
suspend fun incrementUnread(chatId: Long, incrementBy: Int = 1) suspend fun incrementUnread(chatId: Long, incrementBy: Int = 1)
@Query(
"""
UPDATE chats
SET unread_count = 0,
unread_mentions_count = 0
WHERE id = :chatId
"""
)
suspend fun markChatRead(chatId: Long)
@Transaction @Transaction
suspend fun clearAndReplaceChats( suspend fun clearAndReplaceChats(
archived: Boolean, archived: Boolean,

View File

@@ -401,6 +401,8 @@ class NetworkMessageRepository @Inject constructor(
} }
override suspend fun markMessageRead(chatId: Long, messageId: Long): AppResult<Unit> = withContext(ioDispatcher) { override suspend fun markMessageRead(chatId: Long, messageId: Long): AppResult<Unit> = withContext(ioDispatcher) {
// User already viewed this chat/message in UI, so unread badge should drop immediately.
chatDao.markChatRead(chatId)
try { try {
messageApiService.updateMessageStatus( messageApiService.updateMessageStatus(
request = MessageStatusUpdateRequestDto( request = MessageStatusUpdateRequestDto(

View File

@@ -45,6 +45,7 @@ class HandleRealtimeEventsUseCase @Inject constructor(
} }
is RealtimeEvent.ReceiveMessage -> { is RealtimeEvent.ReceiveMessage -> {
val activeChatId = activeChatTracker.activeChatId.value
messageDao.upsertMessages( messageDao.upsertMessages(
listOf( listOf(
MessageEntity( MessageEntity(
@@ -75,8 +76,11 @@ class HandleRealtimeEventsUseCase @Inject constructor(
lastMessageCreatedAt = event.createdAt, lastMessageCreatedAt = event.createdAt,
updatedSortAt = event.createdAt, updatedSortAt = event.createdAt,
) )
if (activeChatId == event.chatId) {
chatDao.markChatRead(chatId = event.chatId)
} else {
chatDao.incrementUnread(chatId = event.chatId) chatDao.incrementUnread(chatId = event.chatId)
val activeChatId = activeChatTracker.activeChatId.value }
val muted = chatDao.isChatMuted(event.chatId) == true val muted = chatDao.isChatMuted(event.chatId) == true
val shouldNotify = shouldShowMessageNotificationUseCase( val shouldNotify = shouldShowMessageNotificationUseCase(
chatId = event.chatId, chatId = event.chatId,

View File

@@ -716,22 +716,22 @@ private fun CenterState(
private fun ChatItem.previewText(): String { private fun ChatItem.previewText(): String {
val raw = lastMessageText.orEmpty().trim() val raw = lastMessageText.orEmpty().trim()
val prefix = when (lastMessageType) { val prefix = when (lastMessageType) {
"image" -> "\uD83D\uDDBC" "image" -> "Photo"
"video" -> "\uD83C\uDFA5" "video" -> "Video"
"audio" -> "\uD83C\uDFB5" "audio" -> "Audio"
"voice" -> "\uD83C\uDFA4" "voice" -> "Voice"
"file" -> "\uD83D\uDCCE" "file" -> "File"
"circle_video" -> "\u25EF" "circle_video" -> "Video message"
else -> "" else -> ""
} }
if (raw.isNotEmpty()) return if (prefix.isBlank()) raw else "$prefix $raw" if (raw.isNotEmpty()) return if (prefix.isBlank()) raw else "$prefix: $raw"
return when (lastMessageType) { return when (lastMessageType) {
"image" -> "\uD83D\uDDBC Photo" "image" -> "Photo"
"video" -> "\uD83C\uDFA5 Video" "video" -> "Video"
"audio" -> "\uD83C\uDFB5 Audio" "audio" -> "Audio"
"voice" -> "\uD83C\uDFA4 Voice message" "voice" -> "Voice message"
"file" -> "\uD83D\uDCCE File" "file" -> "File"
"circle_video" -> "\u25EF Video message" "circle_video" -> "Video message"
null, "text" -> "" null, "text" -> ""
else -> "Media" else -> "Media"
} }