fix(#145): review round A — group-blocked deep-link, status refresh race, hint dismiss on open

This commit is contained in:
makiolaj committed 2026-11-14 02:34:07 +01:00
1 parent 955dccff2b
commit 2e71e4dc5f
4 files changed
+22 -17

No files matched your search

@@ -106,15 +106,17 @@ class ReminderChannels @Inject constructor(
fun status(calendarIds: Collection<Long>): ReminderChannelStatus {
val manager = manager
val appEnabled = manager.areNotificationsEnabled()
val blocked = blockedReminderCalendars(
calendarIds = calendarIds,
appEnabled = appEnabled,
channels = manager.notificationChannels.associate {
it.id to ReminderChannelState(it.importance == NotificationManager.IMPORTANCE_NONE, it.group)
val channels = manager.notificationChannels.associate {
it.id to ReminderChannelState(it.importance == NotificationManager.IMPORTANCE_NONE, it.group)
}
val blockedGroups = manager.notificationChannelGroups.filter { it.isBlocked }.mapTo(HashSet()) { it.id }
return ReminderChannelStatus(
appBlocked = !appEnabled,
blockedCalendarIds = blockedReminderCalendars(calendarIds, appEnabled, channels, blockedGroups),
groupBlockedCalendarIds = calendarIds.filterTo(HashSet()) {
channels[reminderChannelId(it)]?.groupId in blockedGroups
},
blockedGroupIds = manager.notificationChannelGroups.filter { it.isBlocked }.mapTo(HashSet()) { it.id },
)
return ReminderChannelStatus(appBlocked = !appEnabled, blockedCalendarIds = blocked)
}
private fun plan(
@@ -141,8 +143,7 @@ class ReminderChannels @Inject constructor(
if (channels.isEmpty()) return
val byId = existing.associateBy { it.id }
val template = byId[LEGACY_REMINDER_CHANNEL_ID]
// Only a channel's first creation sets its behaviour; later calls just
// rename it, so an existing one passes its own importance back.
// Importance only applies on creation; an existing channel keeps its own.
manager.createNotificationChannels(
channels.map { spec ->
val current = byId[spec.id]
@@ -210,4 +211,6 @@ class ReminderChannels @Inject constructor(
data class ReminderChannelStatus(
val appBlocked: Boolean = false,
val blockedCalendarIds: Set<Long> = emptySet(),
/** The blocked ones whose whole account group is off, which only the app's own screen can undo. */
val groupBlockedCalendarIds: Set<Long> = emptySet(),
)
@@ -104,7 +104,6 @@ internal fun NotificationsScreen(
val scope = rememberCoroutineScope()
val channelStatus by viewModel.channelStatus.collectAsStateWithLifecycle()
val calendarIds = state.reminderCalendars.map { it.id }
// Re-read whenever the screen returns from the system settings it links to.
LifecycleResumeEffect(calendarIds) {
viewModel.refreshChannelStatus(calendarIds)
onPauseOrDispose { }
@@ -112,8 +111,8 @@ internal fun NotificationsScreen(
val openCalendarSound: (Long) -> Unit = { calendarId ->
scope.launch {
val channelId = viewModel.reminderChannelFor(calendarId)
// With the whole app blocked, only the app's own screen can allow it.
context.openNotificationSettings(channelId.takeUnless { channelStatus.appBlocked })
val appScreen = channelStatus.appBlocked || calendarId in channelStatus.groupBlockedCalendarIds
context.openNotificationSettings(channelId.takeUnless { appScreen })
}
}
@@ -126,6 +125,7 @@ internal fun NotificationsScreen(
if (IS_SAMSUNG && !state.notificationCategoryHintDismissed) {
NotificationCategoryHint(
onOpen = {
viewModel.dismissNotificationCategoryHint()
scope.launch {
viewModel.syncReminderChannels()
context.openNotificationSettings()
@@ -207,9 +207,7 @@ internal fun NotificationsScreen(
onClick = { showSnooze = true },
)
// Per-calendar settings, folded behind one header: every switched-on
// calendar gets its notification switch and sound (#145, #272); the ones
// you can edit also keep, drop or replace the default reminders.
// Per-calendar notifications and reminder overrides (#145, #272).
if (state.reminderCalendars.isNotEmpty()) {
Spacer(Modifier.height(24.dp))
GroupedRow(
@@ -50,6 +50,7 @@ import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -377,8 +378,11 @@ class SettingsViewModel @Inject constructor(
/** What the system blocks of the reminders; re-read with [refreshChannelStatus] (#145). */
val channelStatus: StateFlow<ReminderChannelStatus> = _channelStatus.asStateFlow()
private var channelStatusJob: Job? = null
fun refreshChannelStatus(calendarIds: Collection<Long>) {
viewModelScope.launch {
channelStatusJob?.cancel()
channelStatusJob = viewModelScope.launch {
onChannels { status(calendarIds) }?.let { _channelStatus.value = it }
}
}