From 644b53210498937f21fa0c1cdba4e1454d291903 Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Mon, 1 Jun 2026 18:10:11 +0200 Subject: [PATCH] feat: hide individual member calendars in the group view (Android) The calendar filter, in group mode, lists the group's members (+ the shared group calendar) with toggles to hide each individually. Filtering is client-side via CalendarViewModel.hiddenGroupKeys (gm: / gc), reset on group switch; members + colours loaded from the group detail. Co-Authored-By: Claude Opus 4.8 --- .../ui/calendar/CalendarFilterSheet.kt | 32 ++++++++++--- .../ui/calendar/CalendarViewModel.kt | 47 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt index b84a831..3a7df34 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFilterSheet.kt @@ -36,6 +36,20 @@ fun CalendarFilterSheet( onDismiss: () -> Unit, ) { val state by vm.state.collectAsState() + val groupMode = state.activeGroup != null + + // In group mode the filter lists members (+ the group calendar) so they can + // be hidden individually, Outlook-style; otherwise the normal calendars. + val groupEntries: List = if (groupMode) { + buildList { + state.activeGroupMembers.forEach { m -> + add(CalendarFilterEntry(groupMemberKey(m.id), m.displayName, m.color ?: "#4285f4")) + } + add(CalendarFilterEntry(GROUP_CALENDAR_KEY, tr("groups.calendar"), state.activeGroup?.groupCalendarColor ?: "#4285f4")) + } + } else emptyList() + val rows = if (groupMode) groupEntries else events + val hiddenSet = if (groupMode) state.hiddenGroupKeys else state.hiddenKeys ModalBottomSheet(onDismissRequest = onDismiss) { Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { @@ -46,21 +60,24 @@ fun CalendarFilterSheet( ) { Text(tr("filter.title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) Row { - TextButton(onClick = { vm.setHiddenCalendars(emptySet()) }) { Text(tr("filter.show_all")) } TextButton(onClick = { - vm.setHiddenCalendars(events.map { it.key }.toSet()) + if (groupMode) vm.setHiddenGroupKeys(emptySet()) else vm.setHiddenCalendars(emptySet()) + }) { Text(tr("filter.show_all")) } + TextButton(onClick = { + if (groupMode) vm.setHiddenGroupKeys(rows.map { it.key }.toSet()) + else vm.setHiddenCalendars(rows.map { it.key }.toSet()) }) { Text(tr("filter.hide_all")) } } } - if (events.isEmpty()) { + if (rows.isEmpty()) { Text( tr("filter.empty"), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(vertical = 24.dp), ) } - events.forEach { entry -> - val visible = entry.key !in state.hiddenKeys + rows.forEach { entry -> + val visible = entry.key !in hiddenSet Row( Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -73,7 +90,10 @@ fun CalendarFilterSheet( ) Switch( checked = visible, - onCheckedChange = { vm.setCalendarHidden(entry.key, hidden = !it) }, + onCheckedChange = { + if (groupMode) vm.setGroupKeyHidden(entry.key, hidden = !it) + else vm.setCalendarHidden(entry.key, hidden = !it) + }, ) } } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index 9133813..bfc4daa 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -7,6 +7,7 @@ import com.scarriffle.calendarr.data.SettingsStore import com.scarriffle.calendarr.domain.model.CalEvent import com.scarriffle.calendarr.domain.model.CalViewType import com.scarriffle.calendarr.domain.model.Group +import com.scarriffle.calendarr.domain.model.GroupMember import com.scarriffle.calendarr.domain.model.WritableCalendar import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -42,8 +43,15 @@ data class CalendarUiState( // Group overlay: when non-null the calendar shows the group's combined view. val groups: List = emptyList(), val activeGroup: Group? = null, + // Group overlay: full member list (for the filter) + per-member / group-cal + // hidden keys ("gm:" / "gc"). In-memory; reset when switching group. + val activeGroupMembers: List = emptyList(), + val hiddenGroupKeys: Set = emptySet(), ) +fun groupMemberKey(ownerId: Int): String = "gm:$ownerId" +const val GROUP_CALENDAR_KEY = "gc" + @HiltViewModel class CalendarViewModel @Inject constructor( private val repository: CalendarRepository, @@ -252,9 +260,18 @@ class CalendarViewModel @Inject constructor( private fun refreshFromCache() { val st = _state.value - // In group mode the server already scopes + filters; show everything. + // In group mode: server scopes/filters by privacy; locally honour the + // per-member / group-calendar hide toggles (hiddenGroupKeys). val visible = if (st.activeGroup != null) { - allCachedEvents + val hg = st.hiddenGroupKeys + if (hg.isEmpty()) allCachedEvents + else allCachedEvents.filter { ev -> + when { + ev.isGroupEvent -> GROUP_CALENDAR_KEY !in hg + ev.owner != null -> groupMemberKey(ev.owner.id ?: -1) !in hg + else -> true + } + } } else { val hidden = st.hiddenKeys val banished = st.banishedKeys @@ -329,9 +346,33 @@ class CalendarViewModel @Inject constructor( /** Flip between personal and a group's combined overlay; reloads the wide window. */ fun switchGroup(group: Group?) { if (_state.value.activeGroup?.id == group?.id) return - _state.update { it.copy(activeGroup = group) } + _state.update { + it.copy(activeGroup = group, hiddenGroupKeys = emptySet(), activeGroupMembers = emptyList()) + } invalidateCache() initialLoad() + // Load the full member list (with server colours) for the filter sheet. + if (group != null) { + viewModelScope.launch { + runCatching { repository.getGroup(group.id) } + .onSuccess { g -> _state.update { it.copy(activeGroupMembers = g.members) } } + } + } + } + + /** Toggle a single member's calendar / the group calendar in the overlay. */ + fun setGroupKeyHidden(key: String, hidden: Boolean) { + _state.update { + val next = it.hiddenGroupKeys.toMutableSet().apply { if (hidden) add(key) else remove(key) } + it.copy(hiddenGroupKeys = next) + } + refreshFromCache() + } + + /** Replace the group-overlay hidden set (bulk show/hide all). */ + fun setHiddenGroupKeys(keys: Set) { + _state.update { it.copy(hiddenGroupKeys = keys) } + refreshFromCache() } // ---- Writable calendars ----