341 lines
16 KiB
Kotlin
341 lines
16 KiB
Kotlin
package com.rosetta.messenger.data
|
|
|
|
import android.content.Context
|
|
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.intPreferencesKey
|
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
|
import androidx.datastore.preferences.preferencesDataStore
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.coroutines.launch
|
|
|
|
private val Context.dataStore: DataStore<Preferences> by
|
|
preferencesDataStore(name = "rosetta_preferences")
|
|
|
|
class PreferencesManager(private val context: Context) {
|
|
|
|
companion object {
|
|
// Onboarding & Theme
|
|
val HAS_SEEN_ONBOARDING = booleanPreferencesKey("has_seen_onboarding")
|
|
val IS_DARK_THEME = booleanPreferencesKey("is_dark_theme")
|
|
val THEME_MODE = stringPreferencesKey("theme_mode") // "light", "dark", "auto"
|
|
|
|
// Notifications
|
|
val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
|
|
val NOTIFICATION_SOUND_ENABLED = booleanPreferencesKey("notification_sound_enabled")
|
|
val NOTIFICATION_VIBRATE_ENABLED = booleanPreferencesKey("notification_vibrate_enabled")
|
|
val NOTIFICATION_PREVIEW_ENABLED = booleanPreferencesKey("notification_preview_enabled")
|
|
|
|
// Chat Settings
|
|
val MESSAGE_TEXT_SIZE = intPreferencesKey("message_text_size") // 0=small, 1=medium, 2=large
|
|
val SEND_BY_ENTER = booleanPreferencesKey("send_by_enter")
|
|
val AUTO_DOWNLOAD_PHOTOS = booleanPreferencesKey("auto_download_photos")
|
|
val AUTO_DOWNLOAD_VIDEOS = booleanPreferencesKey("auto_download_videos")
|
|
val AUTO_DOWNLOAD_FILES = booleanPreferencesKey("auto_download_files")
|
|
|
|
// Privacy
|
|
val SHOW_ONLINE_STATUS = booleanPreferencesKey("show_online_status")
|
|
val SHOW_READ_RECEIPTS = booleanPreferencesKey("show_read_receipts")
|
|
val SHOW_TYPING_INDICATOR = booleanPreferencesKey("show_typing_indicator")
|
|
|
|
// Language
|
|
val APP_LANGUAGE = stringPreferencesKey("app_language") // "en", "ru", etc.
|
|
|
|
// Appearance / Customization (legacy global key)
|
|
val BACKGROUND_BLUR_COLOR_ID =
|
|
stringPreferencesKey("background_blur_color_id") // id from BackgroundBlurPresets
|
|
|
|
// Pinned Chats (max 3)
|
|
val PINNED_CHATS = stringSetPreferencesKey("pinned_chats") // Set of opponent public keys
|
|
|
|
// Muted Chats (stored as "account::opponentKey")
|
|
val MUTED_CHATS = stringSetPreferencesKey("muted_chats")
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🎨 ONBOARDING & THEME
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val hasSeenOnboarding: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[HAS_SEEN_ONBOARDING] ?: false }
|
|
|
|
val isDarkTheme: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[IS_DARK_THEME] ?: true // Default to dark theme like Telegram
|
|
}
|
|
|
|
suspend fun setHasSeenOnboarding(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[HAS_SEEN_ONBOARDING] = value }
|
|
}
|
|
|
|
suspend fun setDarkTheme(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[IS_DARK_THEME] = value }
|
|
}
|
|
|
|
// In-memory cache for instant theme switching (no DataStore disk I/O delay)
|
|
private val _themeMode = MutableStateFlow("dark")
|
|
private val themeModeInitScope = CoroutineScope(Dispatchers.IO)
|
|
|
|
init {
|
|
// Load persisted value on startup
|
|
themeModeInitScope.launch {
|
|
val persisted = context.dataStore.data.first()[THEME_MODE] ?: "dark"
|
|
_themeMode.value = persisted
|
|
}
|
|
}
|
|
|
|
val themeMode: Flow<String> = _themeMode
|
|
|
|
suspend fun setThemeMode(value: String) {
|
|
_themeMode.value = value // Instant in-memory update
|
|
context.dataStore.edit { preferences -> preferences[THEME_MODE] = value } // Persist
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🔔 NOTIFICATIONS
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val notificationsEnabled: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[NOTIFICATIONS_ENABLED] ?: true }
|
|
|
|
val notificationSoundEnabled: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[NOTIFICATION_SOUND_ENABLED] ?: true
|
|
}
|
|
|
|
val notificationVibrateEnabled: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[NOTIFICATION_VIBRATE_ENABLED] ?: true
|
|
}
|
|
|
|
val notificationPreviewEnabled: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[NOTIFICATION_PREVIEW_ENABLED] ?: true
|
|
}
|
|
|
|
suspend fun setNotificationsEnabled(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[NOTIFICATIONS_ENABLED] = value }
|
|
}
|
|
|
|
suspend fun setNotificationSoundEnabled(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[NOTIFICATION_SOUND_ENABLED] = value }
|
|
}
|
|
|
|
suspend fun setNotificationVibrateEnabled(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[NOTIFICATION_VIBRATE_ENABLED] = value }
|
|
}
|
|
|
|
suspend fun setNotificationPreviewEnabled(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[NOTIFICATION_PREVIEW_ENABLED] = value }
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 💬 CHAT SETTINGS
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val messageTextSize: Flow<Int> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[MESSAGE_TEXT_SIZE] ?: 1 // Default medium
|
|
}
|
|
|
|
val sendByEnter: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[SEND_BY_ENTER] ?: false }
|
|
|
|
val autoDownloadPhotos: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] ?: true }
|
|
|
|
val autoDownloadVideos: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] ?: false }
|
|
|
|
val autoDownloadFiles: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_FILES] ?: false }
|
|
|
|
suspend fun setMessageTextSize(value: Int) {
|
|
context.dataStore.edit { preferences -> preferences[MESSAGE_TEXT_SIZE] = value }
|
|
}
|
|
|
|
suspend fun setSendByEnter(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[SEND_BY_ENTER] = value }
|
|
}
|
|
|
|
suspend fun setAutoDownloadPhotos(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] = value }
|
|
}
|
|
|
|
suspend fun setAutoDownloadVideos(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] = value }
|
|
}
|
|
|
|
suspend fun setAutoDownloadFiles(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_FILES] = value }
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🔐 PRIVACY
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val showOnlineStatus: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[SHOW_ONLINE_STATUS] ?: true }
|
|
|
|
val showReadReceipts: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[SHOW_READ_RECEIPTS] ?: true }
|
|
|
|
val showTypingIndicator: Flow<Boolean> =
|
|
context.dataStore.data.map { preferences -> preferences[SHOW_TYPING_INDICATOR] ?: true }
|
|
|
|
suspend fun setShowOnlineStatus(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[SHOW_ONLINE_STATUS] = value }
|
|
}
|
|
|
|
suspend fun setShowReadReceipts(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[SHOW_READ_RECEIPTS] = value }
|
|
}
|
|
|
|
suspend fun setShowTypingIndicator(value: Boolean) {
|
|
context.dataStore.edit { preferences -> preferences[SHOW_TYPING_INDICATOR] = value }
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🌐 LANGUAGE
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val appLanguage: Flow<String> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[APP_LANGUAGE] ?: "en" // Default English
|
|
}
|
|
|
|
suspend fun setAppLanguage(value: String) {
|
|
context.dataStore.edit { preferences -> preferences[APP_LANGUAGE] = value }
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🎨 APPEARANCE / CUSTOMIZATION
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
private fun buildBackgroundBlurColorKey(account: String): Preferences.Key<String>? {
|
|
val trimmedAccount = account.trim()
|
|
if (trimmedAccount.isBlank()) return null
|
|
return stringPreferencesKey("background_blur_color_id::$trimmedAccount")
|
|
}
|
|
|
|
val backgroundBlurColorId: Flow<String> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[BACKGROUND_BLUR_COLOR_ID] ?: "avatar" // Default: use avatar blur
|
|
}
|
|
|
|
suspend fun setBackgroundBlurColorId(value: String) {
|
|
context.dataStore.edit { preferences -> preferences[BACKGROUND_BLUR_COLOR_ID] = value }
|
|
}
|
|
|
|
fun backgroundBlurColorIdForAccount(account: String): Flow<String> =
|
|
context.dataStore.data.map { preferences ->
|
|
val scopedKey = buildBackgroundBlurColorKey(account)
|
|
if (scopedKey != null) preferences[scopedKey] ?: "avatar" else "avatar"
|
|
}
|
|
|
|
suspend fun setBackgroundBlurColorId(account: String, value: String) {
|
|
val scopedKey = buildBackgroundBlurColorKey(account)
|
|
context.dataStore.edit { preferences ->
|
|
if (scopedKey != null) {
|
|
preferences[scopedKey] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 📌 PINNED CHATS
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
val pinnedChats: Flow<Set<String>> =
|
|
context.dataStore.data.map { preferences ->
|
|
preferences[PINNED_CHATS] ?: emptySet()
|
|
}
|
|
|
|
suspend fun setPinnedChats(value: Set<String>) {
|
|
context.dataStore.edit { preferences -> preferences[PINNED_CHATS] = value }
|
|
}
|
|
|
|
suspend fun togglePinChat(opponentKey: String): Boolean {
|
|
var wasPinned = false
|
|
context.dataStore.edit { preferences ->
|
|
val current = preferences[PINNED_CHATS] ?: emptySet()
|
|
if (current.contains(opponentKey)) {
|
|
// Unpin
|
|
preferences[PINNED_CHATS] = current - opponentKey
|
|
wasPinned = true
|
|
} else if (current.size < 3) {
|
|
// Pin (max 3)
|
|
preferences[PINNED_CHATS] = current + opponentKey
|
|
}
|
|
}
|
|
return wasPinned
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════
|
|
// 🔕 MUTED CHATS
|
|
// ═════════════════════════════════════════════════════════════
|
|
|
|
private fun buildMutedKey(account: String, opponentKey: String): String {
|
|
val trimmedAccount = account.trim()
|
|
val trimmedOpponent = opponentKey.trim()
|
|
return if (trimmedAccount.isBlank()) trimmedOpponent else "$trimmedAccount::$trimmedOpponent"
|
|
}
|
|
|
|
private fun parseMutedKeyForAccount(rawKey: String, account: String): String? {
|
|
val trimmedAccount = account.trim()
|
|
if (rawKey.isBlank()) return null
|
|
|
|
// Legacy format support: plain opponentKey without account prefix.
|
|
if ("::" !in rawKey) return rawKey
|
|
|
|
val parts = rawKey.split("::", limit = 2)
|
|
if (parts.size != 2) return null
|
|
val keyAccount = parts[0]
|
|
val opponentKey = parts[1]
|
|
return if (trimmedAccount.isBlank() || keyAccount == trimmedAccount) opponentKey else null
|
|
}
|
|
|
|
val mutedChats: Flow<Set<String>> =
|
|
context.dataStore.data.map { preferences -> preferences[MUTED_CHATS] ?: emptySet() }
|
|
|
|
fun mutedChatsForAccount(account: String): Flow<Set<String>> =
|
|
mutedChats.map { muted ->
|
|
muted.mapNotNull { parseMutedKeyForAccount(it, account) }.toSet()
|
|
}
|
|
|
|
suspend fun isChatMuted(account: String, opponentKey: String): Boolean {
|
|
if (opponentKey.isBlank()) return false
|
|
val current = mutedChats.first()
|
|
return current.contains(buildMutedKey(account, opponentKey)) || current.contains(opponentKey)
|
|
}
|
|
|
|
suspend fun setChatMuted(account: String, opponentKey: String, muted: Boolean) {
|
|
if (opponentKey.isBlank()) return
|
|
val scopedKey = buildMutedKey(account, opponentKey)
|
|
context.dataStore.edit { preferences ->
|
|
val current = preferences[MUTED_CHATS] ?: emptySet()
|
|
preferences[MUTED_CHATS] =
|
|
if (muted) {
|
|
current + scopedKey
|
|
} else {
|
|
current - scopedKey - opponentKey
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun toggleMuteChat(account: String, opponentKey: String): Boolean {
|
|
if (opponentKey.isBlank()) return false
|
|
val mutedNow = !isChatMuted(account, opponentKey)
|
|
setChatMuted(account, opponentKey, mutedNow)
|
|
return mutedNow
|
|
}
|
|
}
|