feat: Integrate notification management and preferences across platforms (#4819)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-16 20:17:34 -05:00 committed by GitHub
parent 0b2e89c46f
commit 8c964a15ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1304 additions and 61 deletions

View file

@ -26,6 +26,7 @@ import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.AppSettingsAlt
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -41,6 +42,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.acknowledgements
import org.meshtastic.core.resources.app_notifications
import org.meshtastic.core.resources.app_version
import org.meshtastic.core.resources.info
import org.meshtastic.core.resources.intro_show
@ -74,6 +76,18 @@ fun AppInfoSection(
onShowAppIntro()
}
ListItem(
text = stringResource(Res.string.app_notifications),
leadingIcon = Icons.Rounded.Notifications,
trailingIcon = null,
) {
val intent =
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
}
settingsLauncher.launch(intent)
}
ListItem(
text = stringResource(Res.string.system_settings),
leadingIcon = Icons.Rounded.AppSettingsAlt,

View file

@ -38,6 +38,7 @@ import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase
import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase
import org.meshtastic.core.domain.usecase.settings.SetLocaleUseCase
import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
import org.meshtastic.core.domain.usecase.settings.SetNotificationSettingsUseCase
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.MyNodeInfo
@ -46,6 +47,7 @@ import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
@ -61,12 +63,14 @@ class SettingsViewModel(
private val buildConfigProvider: BuildConfigProvider,
private val databaseManager: DatabaseManager,
private val meshLogPrefs: MeshLogPrefs,
private val notificationPrefs: NotificationPrefs,
private val setThemeUseCase: SetThemeUseCase,
private val setLocaleUseCase: SetLocaleUseCase,
private val setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase,
private val setProvideLocationUseCase: SetProvideLocationUseCase,
private val setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase,
private val setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase,
private val setNotificationSettingsUseCase: SetNotificationSettingsUseCase,
private val meshLocationUseCase: MeshLocationUseCase,
private val exportDataUseCase: ExportDataUseCase,
private val isOtaCapableUseCase: IsOtaCapableUseCase,
@ -120,6 +124,17 @@ class SettingsViewModel(
setDatabaseCacheLimitUseCase(limit)
}
// Notifications
val messagesEnabled = notificationPrefs.messagesEnabled
val nodeEventsEnabled = notificationPrefs.nodeEventsEnabled
val lowBatteryEnabled = notificationPrefs.lowBatteryEnabled
fun setMessagesEnabled(enabled: Boolean) = setNotificationSettingsUseCase.setMessagesEnabled(enabled)
fun setNodeEventsEnabled(enabled: Boolean) = setNotificationSettingsUseCase.setNodeEventsEnabled(enabled)
fun setLowBatteryEnabled(enabled: Boolean) = setNotificationSettingsUseCase.setLowBatteryEnabled(enabled)
// MeshLog retention period (bounded by MeshLogPrefsImpl constants)
private val _meshLogRetentionDays = MutableStateFlow(meshLogPrefs.retentionDays.value)
val meshLogRetentionDays: StateFlow<Int> = _meshLogRetentionDays.asStateFlow()

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.settings.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BatteryAlert
import androidx.compose.material.icons.rounded.Message
import androidx.compose.material.icons.rounded.PersonAdd
import androidx.compose.runtime.Composable
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.app_notifications
import org.meshtastic.core.resources.meshtastic_low_battery_notifications
import org.meshtastic.core.resources.meshtastic_messages_notifications
import org.meshtastic.core.resources.meshtastic_new_nodes_notifications
import org.meshtastic.core.ui.component.SwitchListItem
/**
* Notification settings section with in-app toggles. Primarily used on platforms without system notification channels.
*/
@Composable
fun NotificationSection(
messagesEnabled: Boolean,
onToggleMessages: (Boolean) -> Unit,
nodeEventsEnabled: Boolean,
onToggleNodeEvents: (Boolean) -> Unit,
lowBatteryEnabled: Boolean,
onToggleLowBattery: (Boolean) -> Unit,
) {
ExpressiveSection(title = stringResource(Res.string.app_notifications)) {
SwitchListItem(
text = stringResource(Res.string.meshtastic_messages_notifications),
leadingIcon = Icons.Rounded.Message,
checked = messagesEnabled,
onClick = { onToggleMessages(!messagesEnabled) },
)
SwitchListItem(
text = stringResource(Res.string.meshtastic_new_nodes_notifications),
leadingIcon = Icons.Rounded.PersonAdd,
checked = nodeEventsEnabled,
onClick = { onToggleNodeEvents(!nodeEventsEnabled) },
)
SwitchListItem(
text = stringResource(Res.string.meshtastic_low_battery_notifications),
leadingIcon = Icons.Rounded.BatteryAlert,
checked = lowBatteryEnabled,
onClick = { onToggleLowBattery(!lowBatteryEnabled) },
)
}
}

View file

@ -71,12 +71,14 @@ class SettingsViewModelTest {
buildConfigProvider = buildConfigProvider,
databaseManager = databaseManager,
meshLogPrefs = meshLogPrefs,
notificationPrefs = mockk(relaxed = true),
setThemeUseCase = mockk(relaxed = true),
setLocaleUseCase = mockk(relaxed = true),
setAppIntroCompletedUseCase = mockk(relaxed = true),
setProvideLocationUseCase = mockk(relaxed = true),
setDatabaseCacheLimitUseCase = mockk(relaxed = true),
setMeshLogSettingsUseCase = mockk(relaxed = true),
setNotificationSettingsUseCase = mockk(relaxed = true),
meshLocationUseCase = mockk(relaxed = true),
exportDataUseCase = mockk(relaxed = true),
isOtaCapableUseCase = mockk(relaxed = true),