From c1ad35e15248d00ae247e5584c293dbdba4a8e33 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 12:24:44 +0100 Subject: [PATCH] Persist channel mute consistently and own panel preference changes --- .../java/com/utun/chat/data/ConfigProvider.kt | 11 ++++-- .../utun/chat/ui/components/AvatarEditors.kt | 26 +++++++++----- .../com/utun/chat/viewmodel/ChatViewModel.kt | 36 ++++++++++++------- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index afd0fea2..7ce5879a 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -549,7 +549,14 @@ class ConfigProvider(private val context: Context) { /* ── Connection mode ── */ fun getConnectionMode(): String = runBlocking { - context.dataStore.data.first()[connectionModeKey] ?: "auto" + connectionMode(context.dataStore.data.first()) + } + + private fun connectionMode(prefs: Preferences): String { + val mode = prefs[connectionModeKey] ?: return "auto" + if (mode == "auto" || mode == "manual") return mode + LogManager.addLog("WARN", "CFG", "Unknown stored connection mode '$mode'; using auto") + return "auto" } suspend fun setConnectionMode(mode: String) { @@ -985,7 +992,7 @@ class ConfigProvider(private val context: Context) { val cfgFile = File(snapshot.directory, "utun.cfg") val stored = loadServers(cfgFile) val saved = stored.active + stored.inactive - val mode = snapshot.preferences[connectionModeKey] ?: "auto" + val mode = connectionMode(snapshot.preferences) val active = if (mode == "auto") detectInterfaces(saved, protocols) else stored.active val activeKeys = active.map { it.socketKey }.toSet() val inactive = saved.filter { it.socketKey !in activeKeys }.distinctBy { it.socketKey } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt index e4e6248d..92335246 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AvatarEditors.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp import com.utun.chat.ChatApplication import com.utun.chat.R import com.utun.chat.data.* -import kotlinx.coroutines.launch /** Свойства CHAT-группы: отдельный аватар группы и локальный звук уведомлений. * При открытии обновляет снимок; canEdit показывает кнопки публикации, окончательный допуск проверяет core. @@ -28,10 +27,13 @@ fun GroupPropertiesDialog(channel: Channel, onDismiss: () -> Unit) { val state = groups[channel.id] val editState = edits["group:${channel.id}"] ?: AvatarEditState.IDLE val working = editState == AvatarEditState.WORKING - var soundOn by rememberSaveable(channel.id) { mutableStateOf(!NotificationSound.isMuted(channel.id)) } + val provider = ChatApplication.instance.configProvider + val soundPreferences = remember(provider) { provider.changes.observe("group sound", provider.notificationSoundSettings) } + val soundSettings by soundPreferences.collectAsState(initial = NotificationSound.enabled to NotificationSound.mutedChannels) + val soundFailed by provider.changes.failed.collectAsState() + var pickerFailed by remember { mutableStateOf(false) } var pickedAccount by rememberSaveable { mutableStateOf(0L) } var pickedGroup by rememberSaveable { mutableStateOf("") } - val scope = rememberCoroutineScope() val picker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> if (uri != null) repository.editGroup(pickedGroup, uri, pickedAccount) } @@ -53,21 +55,27 @@ fun GroupPropertiesDialog(channel: Channel, onDismiss: () -> Unit) { Text(stringResource(R.string.group_avatar_limits), style = MaterialTheme.typography.bodySmall) Row { TextButton(enabled = !working, onClick = { - pickedAccount = repository.accountId; pickedGroup = channel.id; picker.launch("image/*") + pickedAccount = repository.accountId; pickedGroup = channel.id + try { pickerFailed = false; picker.launch("image/*") } + catch (e: Exception) { pickerFailed = true; LogManager.addLog("ERROR", "Avatar", "Group image picker failed: $e") } }) { Text(stringResource(R.string.avatar_upload)) } TextButton(enabled = !working && state.hasImage, onClick = { repository.editGroup(channel.id, null, repository.accountId) }) { Text(stringResource(R.string.avatar_remove)) } } } else Text(stringResource(R.string.group_avatar_key_required), style = MaterialTheme.typography.bodySmall) - if (editState == AvatarEditState.ERROR) + if (editState == AvatarEditState.ERROR || pickerFailed) Text(stringResource(R.string.avatar_edit_failed), color = MaterialTheme.colorScheme.error) + if (soundFailed) Text(stringResource(R.string.settings_update_failed), color = MaterialTheme.colorScheme.error) Row(verticalAlignment = Alignment.CenterVertically) { Text(stringResource(R.string.sound), modifier = Modifier.weight(1f)) - Switch(checked = soundOn, onCheckedChange = { - soundOn = it; NotificationSound.setMuted(channel.id, !it) - NativeLib.setSoundOnMessage(channel.id, it) - scope.launch { ChatApplication.instance.configProvider.setChannelMuted(channel.id, !it) } + Switch(checked = channel.id !in soundSettings.second, onCheckedChange = { enabled -> + provider.changes.submit("group notification sound") { + provider.setChannelMuted(channel.id, !enabled) + NotificationSound.setMuted(channel.id, !enabled) + if (NativeLib.isReady()) NativeLib.setSoundOnMessage(channel.id, enabled) + LogManager.addLog("INFO", "Sound", "Group sound saved channel=${channel.id} enabled=$enabled") + } }) } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index b1f4a5e2..337bae16 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -438,8 +438,17 @@ class ChatViewModel : ViewModel() { } } viewModelScope.launch(Dispatchers.IO) { - val key = ChatApplication.instance.configProvider.getString("node.public_key") - if (key != null && key.length == 64) _pubKey.value = key + try { + val key = ChatApplication.instance.configProvider.getString("node.public_key") + if (key != null && key.length == 64) _pubKey.value = key + } catch (e: kotlinx.coroutines.CancellationException) { throw e } + catch (e: Exception) { LogManager.addLog("ERROR", "KEYS", "Public key preferences unavailable: $e") } + } + viewModelScope.launch { + val provider = ChatApplication.instance.configProvider + provider.changes.observe("channel sound", provider.notificationSoundSettings).collect { (_, muted) -> + _currentChannel.value?.let { _soundOn.value = it.id !in muted } + } } viewModelScope.launch { delay(500) @@ -1060,16 +1069,15 @@ class ChatViewModel : ViewModel() { fun setRadioVolumePtt(enabled: Boolean) { if (!_radioActive.value) return - viewModelScope.launch { - ChatApplication.instance.configProvider.setRadioVolumePtt(enabled) - } + val provider = ChatApplication.instance.configProvider + provider.changes.submit("panel volume PTT") { provider.setRadioVolumePtt(enabled) } } /** Переключатель авто-PTT на панели рации: сохранить выбор и перенастроить захват. */ fun setRadioAutomatic(enabled: Boolean) { if (!_radioActive.value) return - viewModelScope.launch { - val ctx = ChatApplication.instance + val ctx = ChatApplication.instance + ctx.configProvider.changes.submit("panel automatic PTT") { ctx.configProvider.setValue("radio.auto_enabled", if (enabled) "1" else "0") LogManager.addLog("INFO", "RadioAudio", "Panel automatic transmission enabled=$enabled channel=$radioChannelId") if (enabled) RadioOverlayService.requestPermissions() else RadioOverlayService.sync(ctx) @@ -1271,8 +1279,8 @@ class ChatViewModel : ViewModel() { watchStreams() _radioActive.value = (radioChannelId == channel.id) _radioTalking.value = if (_radioActive.value) _radioTalkState.value.speakers.firstOrNull() else null - _soundOn.value = NativeLib.getSoundOnMessage(channel.id) - NotificationSound.setMuted(channel.id, !_soundOn.value) + _soundOn.value = !NotificationSound.isMuted(channel.id) + NativeLib.setSoundOnMessage(channel.id, _soundOn.value) r.connectChannel(channel.id) refreshMessages(channel.id) refreshMembers(channel.id) @@ -1282,9 +1290,13 @@ class ChatViewModel : ViewModel() { fun toggleSound() { val ch = _currentChannel.value ?: return val on = !_soundOn.value - _soundOn.value = on - NotificationSound.setMuted(ch.id, !on) - NativeLib.setSoundOnMessage(ch.id, on) + val provider = ChatApplication.instance.configProvider + provider.changes.submit("channel notification sound") { + provider.setChannelMuted(ch.id, !on) + NotificationSound.setMuted(ch.id, !on) + if (NativeLib.isReady()) NativeLib.setSoundOnMessage(ch.id, on) + LogManager.addLog("INFO", "Sound", "Channel sound saved channel=${ch.id} enabled=$on") + } } /* ── DM (личные сообщения) ── */