android: add mention override for muted chat notifications
Some checks failed
CI / test (push) Failing after 2m18s
Some checks failed
CI / test (push) Failing after 2m18s
This commit is contained in:
@@ -295,3 +295,9 @@
|
||||
- Wired realtime receive-message handling to trigger local notification via `NotificationDispatcher` when chat is not active.
|
||||
- Added chat title lookup helper in `ChatDao` for notification titles.
|
||||
- Added explicit realtime stop in `ChatViewModel.onCleared()` to avoid stale collectors.
|
||||
|
||||
### Step 49 - Mention override for muted chats
|
||||
- Extended realtime receive-message model/parsing with `isMention` flag support.
|
||||
- Added muted-chat guard in realtime notification flow: muted chats stay silent unless message is a mention.
|
||||
- Routed mention notifications to mentions channel/priority via `NotificationDispatcher`.
|
||||
- Added parser unit test for mention-flag mapping.
|
||||
|
||||
@@ -84,6 +84,9 @@ interface ChatDao {
|
||||
@Query("SELECT display_title FROM chats WHERE id = :chatId LIMIT 1")
|
||||
suspend fun getChatDisplayTitle(chatId: Long): String?
|
||||
|
||||
@Query("SELECT muted FROM chats WHERE id = :chatId LIMIT 1")
|
||||
suspend fun isChatMuted(chatId: Long): Boolean?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertChats(chats: List<ChatEntity>)
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ class RealtimeEventParser @Inject constructor(
|
||||
text = messageObject?.get("text").stringOrNull(),
|
||||
type = messageObject?.get("type").stringOrNull(),
|
||||
createdAt = messageObject?.get("created_at").stringOrNull(),
|
||||
isMention = messageObject?.get("is_mention").boolOrNull()
|
||||
?: payload["is_mention"].boolOrNull()
|
||||
?: messageObject?.get("mentions_me").boolOrNull()
|
||||
?: payload["mentions_me"].boolOrNull()
|
||||
?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,4 +124,13 @@ class RealtimeEventParser @Inject constructor(
|
||||
private fun JsonElement?.longOrNull(): Long? {
|
||||
return this?.jsonPrimitive?.contentOrNull?.toLongOrNull()
|
||||
}
|
||||
|
||||
private fun JsonElement?.boolOrNull(): Boolean? {
|
||||
val raw = this?.jsonPrimitive?.contentOrNull?.trim()?.lowercase() ?: return null
|
||||
return when (raw) {
|
||||
"true", "1" -> true
|
||||
"false", "0" -> false
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ sealed interface RealtimeEvent {
|
||||
val text: String?,
|
||||
val type: String?,
|
||||
val createdAt: String?,
|
||||
val isMention: Boolean,
|
||||
) : RealtimeEvent
|
||||
|
||||
data class MessageUpdated(
|
||||
|
||||
@@ -75,7 +75,8 @@ class HandleRealtimeEventsUseCase @Inject constructor(
|
||||
)
|
||||
chatDao.incrementUnread(chatId = event.chatId)
|
||||
val activeChatId = activeChatTracker.activeChatId.value
|
||||
if (activeChatId != event.chatId) {
|
||||
val muted = chatDao.isChatMuted(event.chatId) == true
|
||||
if (activeChatId != event.chatId && (!muted || event.isMention)) {
|
||||
val title = chatDao.getChatDisplayTitle(event.chatId) ?: "New message"
|
||||
val body = event.text?.takeIf { it.isNotBlank() }
|
||||
?: when (event.type?.lowercase()) {
|
||||
@@ -92,6 +93,7 @@ class HandleRealtimeEventsUseCase @Inject constructor(
|
||||
messageId = event.messageId,
|
||||
title = title,
|
||||
body = body,
|
||||
isMention = event.isMention,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ class NetworkChatRepositoryTest {
|
||||
return chats.value.firstOrNull { it.id == chatId }?.displayTitle
|
||||
}
|
||||
|
||||
override suspend fun isChatMuted(chatId: Long): Boolean? {
|
||||
return chats.value.firstOrNull { it.id == chatId }?.muted
|
||||
}
|
||||
|
||||
override suspend fun upsertChats(chats: List<ChatEntity>) {
|
||||
val merged = this.chats.value.associateBy { it.id }.toMutableMap()
|
||||
chats.forEach { merged[it.id] = it }
|
||||
|
||||
@@ -2,6 +2,7 @@ package ru.daemonlord.messenger.data.realtime
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import ru.daemonlord.messenger.domain.realtime.model.RealtimeEvent
|
||||
@@ -70,6 +71,33 @@ class RealtimeEventParserTest {
|
||||
assertEquals(5L, mapped.senderId)
|
||||
assertEquals(100L, mapped.replyToMessageId)
|
||||
assertEquals("hi", mapped.text)
|
||||
assertFalse(mapped.isMention)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseReceiveMessage_withMentionFlag_mapsMention() {
|
||||
val payload = """
|
||||
{
|
||||
"event": "receive_message",
|
||||
"payload": {
|
||||
"chat_id": 88,
|
||||
"message": {
|
||||
"id": 9002,
|
||||
"chat_id": 88,
|
||||
"sender_id": 5,
|
||||
"type": "text",
|
||||
"text": "@you hi",
|
||||
"created_at": "2026-03-08T12:00:00Z",
|
||||
"is_mention": true
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val event = parser.parse(payload)
|
||||
assertTrue(event is RealtimeEvent.ReceiveMessage)
|
||||
val mapped = event as RealtimeEvent.ReceiveMessage
|
||||
assertTrue(mapped.isMention)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
- [x] Локальные уведомления для foreground
|
||||
- [x] Notification channels (Android)
|
||||
- [x] Deep links: open chat/message
|
||||
- [ ] Mention override для muted чатов
|
||||
- [x] Mention override для muted чатов
|
||||
|
||||
## 13. UI/UX и темы
|
||||
- [ ] Светлая/темная тема (читаемая)
|
||||
|
||||
Reference in New Issue
Block a user