feat(notifications): optional alert when any session completes (#1401)

Co-authored-by: M57 <41645758+Hy4ri@users.noreply.github.com>
This commit is contained in:
Nikhil NairandM57 authored and GitHub committed 2026-09-30 23:31:08 +03:00
1 parent 0c336ecc4d
commit 5bf43e4a21
14 files changed
+474 -40

No files matched your search

@@ -241,6 +241,8 @@ internal fun SettingsBehaviorPage(
onAutoReconnectChange = viewModel::onAutoReconnectChange,
keepConnectedInBackground = state.keepConnectedInBackground,
onKeepConnectedInBackgroundChange = viewModel::onKeepConnectedInBackgroundChange,
notifySessionCompletions = state.notifySessionCompletions,
onNotifySessionCompletionsChange = viewModel::onNotifySessionCompletionsChange,
restoreLastSession = state.restoreLastSession,
onRestoreLastSessionChange = viewModel::onRestoreLastSessionChange,
)
@@ -47,6 +47,7 @@ data class SettingsUiState(
val showTokensPerSecond: Boolean = true,
val showModelProvider: Boolean = false,
val keepConnectedInBackground: Boolean = false,
val notifySessionCompletions: Boolean = false,
val profiles: List<ConnectionProfile> = emptyList(),
val selectedProfileId: String? = null,
val renameProfileName: String = "",
@@ -96,6 +97,7 @@ class SettingsViewModel(
val showTokensPerSecond = AuthManager.isTokensPerSecondEnabled()
val showModelProvider = AuthManager.isModelProviderShown()
val keepConnectedInBackground = AuthManager.isKeepConnectedInBackground()
val notifySessionCompletions = AuthManager.isNotifySessionCompletions()
val profiles = AuthManager.getConnectionProfiles()
val appLanguage = AuthManager.getAppLanguage()
val renameProfileName =
@@ -124,6 +126,7 @@ class SettingsViewModel(
showTokensPerSecond = showTokensPerSecond,
showModelProvider = showModelProvider,
keepConnectedInBackground = keepConnectedInBackground,
notifySessionCompletions = notifySessionCompletions,
profiles = profiles,
selectedProfileId = selectedId,
renameProfileName = renameProfileName,
@@ -394,6 +397,11 @@ class SettingsViewModel(
AuthManager.setKeepConnectedInBackground(enabled)
}
fun onNotifySessionCompletionsChange(enabled: Boolean) {
_uiState.update { it.copy(notifySessionCompletions = enabled) }
AuthManager.setNotifySessionCompletions(enabled)
}
/** Reconcile the chat shortcut with changes made through Settings or another screen. */
fun refreshKeepConnectedInBackground() {
val enabled = AuthManager.isKeepConnectedInBackground()
@@ -1,5 +1,10 @@
package com.m57.hermescontrol.ui.settings.components
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -13,8 +18,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.settings.SectionCard
@@ -24,9 +31,20 @@ internal fun BehaviorSection(
onAutoReconnectChange: (Boolean) -> Unit,
keepConnectedInBackground: Boolean,
onKeepConnectedInBackgroundChange: (Boolean) -> Unit,
notifySessionCompletions: Boolean,
onNotifySessionCompletionsChange: (Boolean) -> Unit,
restoreLastSession: Boolean,
onRestoreLastSessionChange: (Boolean) -> Unit,
) {
val context = LocalContext.current
// Turning session-complete alerts on needs POST_NOTIFICATIONS (Android
// 13+). The chat screen requests it on open, but the toggle lives in
// Settings — gate here so enabling never lands in a state where the OS
// silently blocks what the user just asked for. Denial reverts the toggle.
val permissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) onNotifySessionCompletionsChange(true)
}
SectionCard {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -84,6 +102,49 @@ internal fun BehaviorSection(
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.settings_notify_session_completions_title),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = stringResource(R.string.settings_notify_session_completions_desc),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
Switch(
checked = notifySessionCompletions,
onCheckedChange = { enabled ->
if (!enabled) {
onNotifySessionCompletionsChange(false)
} else {
val permission = Manifest.permission.POST_NOTIFICATIONS
val granted =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
if (granted) {
onNotifySessionCompletionsChange(true)
} else {
permissionLauncher.launch(permission)
}
}
},
)
}
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,