Persist channel mute consistently and own panel preference changes

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent fb696d0d56
commit c1ad35e152
3 files changed
+50 -23

No files matched your search

@@ -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 }
@@ -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")
}
})
}
}
@@ -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 (личные сообщения) ── */