android: add datastore notification settings and gating usecase
Some checks failed
CI / test (push) Failing after 2m6s

This commit is contained in:
Codex
2026-03-09 15:02:56 +03:00
parent dfa67c34c9
commit 33514265e3
12 changed files with 341 additions and 9 deletions

View File

@@ -0,0 +1,82 @@
package ru.daemonlord.messenger.data.notifications.repository
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import ru.daemonlord.messenger.domain.notifications.model.ChatNotificationOverride
import ru.daemonlord.messenger.domain.notifications.model.NotificationSettings
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DataStoreNotificationSettingsRepository @Inject constructor(
private val dataStore: DataStore<Preferences>,
) : NotificationSettingsRepository {
override fun observeSettings(): Flow<NotificationSettings> {
return dataStore.data.map { preferences ->
NotificationSettings(
globalEnabled = preferences[GLOBAL_ENABLED_KEY] ?: true,
previewEnabled = preferences[PREVIEW_ENABLED_KEY] ?: true,
)
}
}
override suspend fun getSettings(): NotificationSettings {
return observeSettings().first()
}
override suspend fun setGlobalEnabled(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[GLOBAL_ENABLED_KEY] = enabled
}
}
override suspend fun setPreviewEnabled(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PREVIEW_ENABLED_KEY] = enabled
}
}
override fun observeChatOverride(chatId: Long): Flow<ChatNotificationOverride> {
return dataStore.data.map { preferences ->
preferences.chatOverride(chatId)
}
}
override suspend fun getChatOverride(chatId: Long): ChatNotificationOverride {
return observeChatOverride(chatId).first()
}
override suspend fun setChatOverride(chatId: Long, mode: ChatNotificationOverride) {
dataStore.edit { preferences ->
preferences[chatOverrideKey(chatId)] = mode.name
}
}
override suspend fun clearChatOverride(chatId: Long) {
dataStore.edit { preferences ->
preferences.remove(chatOverrideKey(chatId))
}
}
private fun Preferences.chatOverride(chatId: Long): ChatNotificationOverride {
return this[chatOverrideKey(chatId)]
?.let { runCatching { ChatNotificationOverride.valueOf(it) }.getOrNull() }
?: ChatNotificationOverride.DEFAULT
}
private fun chatOverrideKey(chatId: Long) = stringPreferencesKey("notification_chat_override_$chatId")
private companion object {
val GLOBAL_ENABLED_KEY = booleanPreferencesKey("notification_global_enabled")
val PREVIEW_ENABLED_KEY = booleanPreferencesKey("notification_preview_enabled")
}
}

View File

@@ -8,10 +8,12 @@ import ru.daemonlord.messenger.data.auth.repository.NetworkAuthRepository
import ru.daemonlord.messenger.data.chat.repository.NetworkChatRepository
import ru.daemonlord.messenger.data.media.repository.NetworkMediaRepository
import ru.daemonlord.messenger.data.message.repository.NetworkMessageRepository
import ru.daemonlord.messenger.data.notifications.repository.DataStoreNotificationSettingsRepository
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
import ru.daemonlord.messenger.domain.message.repository.MessageRepository
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
import javax.inject.Singleton
@Module
@@ -41,4 +43,10 @@ abstract class RepositoryModule {
abstract fun bindMediaRepository(
repository: NetworkMediaRepository,
): MediaRepository
@Binds
@Singleton
abstract fun bindNotificationSettingsRepository(
repository: DataStoreNotificationSettingsRepository,
): NotificationSettingsRepository
}

View File

@@ -0,0 +1,8 @@
package ru.daemonlord.messenger.domain.notifications.model
enum class ChatNotificationOverride {
DEFAULT,
ENABLED,
MUTED,
}

View File

@@ -0,0 +1,7 @@
package ru.daemonlord.messenger.domain.notifications.model
data class NotificationSettings(
val globalEnabled: Boolean = true,
val previewEnabled: Boolean = true,
)

View File

@@ -0,0 +1,18 @@
package ru.daemonlord.messenger.domain.notifications.repository
import kotlinx.coroutines.flow.Flow
import ru.daemonlord.messenger.domain.notifications.model.ChatNotificationOverride
import ru.daemonlord.messenger.domain.notifications.model.NotificationSettings
interface NotificationSettingsRepository {
fun observeSettings(): Flow<NotificationSettings>
suspend fun getSettings(): NotificationSettings
suspend fun setGlobalEnabled(enabled: Boolean)
suspend fun setPreviewEnabled(enabled: Boolean)
fun observeChatOverride(chatId: Long): Flow<ChatNotificationOverride>
suspend fun getChatOverride(chatId: Long): ChatNotificationOverride
suspend fun setChatOverride(chatId: Long, mode: ChatNotificationOverride)
suspend fun clearChatOverride(chatId: Long)
}

View File

@@ -0,0 +1,28 @@
package ru.daemonlord.messenger.domain.notifications.usecase
import ru.daemonlord.messenger.domain.notifications.model.ChatNotificationOverride
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
import javax.inject.Inject
class ShouldShowMessageNotificationUseCase @Inject constructor(
private val notificationSettingsRepository: NotificationSettingsRepository,
) {
suspend operator fun invoke(
chatId: Long,
isMention: Boolean,
serverMuted: Boolean,
): Boolean {
val settings = notificationSettingsRepository.getSettings()
if (!settings.globalEnabled) return false
val chatOverride = notificationSettingsRepository.getChatOverride(chatId)
val effectiveMuted = when (chatOverride) {
ChatNotificationOverride.DEFAULT -> serverMuted
ChatNotificationOverride.ENABLED -> false
ChatNotificationOverride.MUTED -> true
}
return !effectiveMuted || isMention
}
}

View File

@@ -13,6 +13,7 @@ import ru.daemonlord.messenger.core.notifications.NotificationDispatcher
import ru.daemonlord.messenger.data.message.local.dao.MessageDao
import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
import ru.daemonlord.messenger.domain.notifications.usecase.ShouldShowMessageNotificationUseCase
import ru.daemonlord.messenger.domain.realtime.RealtimeManager
import ru.daemonlord.messenger.domain.realtime.model.RealtimeEvent
import javax.inject.Inject
@@ -26,6 +27,7 @@ class HandleRealtimeEventsUseCase @Inject constructor(
private val messageDao: MessageDao,
private val notificationDispatcher: NotificationDispatcher,
private val activeChatTracker: ActiveChatTracker,
private val shouldShowMessageNotificationUseCase: ShouldShowMessageNotificationUseCase,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -76,7 +78,12 @@ class HandleRealtimeEventsUseCase @Inject constructor(
chatDao.incrementUnread(chatId = event.chatId)
val activeChatId = activeChatTracker.activeChatId.value
val muted = chatDao.isChatMuted(event.chatId) == true
if (activeChatId != event.chatId && (!muted || event.isMention)) {
val shouldNotify = shouldShowMessageNotificationUseCase(
chatId = event.chatId,
isMention = event.isMention,
serverMuted = muted,
)
if (activeChatId != event.chatId && shouldNotify) {
val title = chatDao.getChatDisplayTitle(event.chatId) ?: "New message"
val body = event.text?.takeIf { it.isNotBlank() }
?: when (event.type?.lowercase()) {