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:<id> / gc), reset on group switch;
members + colours loaded from the group detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-06-01 18:10:11 +02:00
parent 87fc8df146
commit 644b532104
2 changed files with 70 additions and 9 deletions

View File

@@ -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<CalendarFilterEntry> = 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)
},
)
}
}

View File

@@ -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<Group> = emptyList(),
val activeGroup: Group? = null,
// Group overlay: full member list (for the filter) + per-member / group-cal
// hidden keys ("gm:<userId>" / "gc"). In-memory; reset when switching group.
val activeGroupMembers: List<GroupMember> = emptyList(),
val hiddenGroupKeys: Set<String> = 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<String>) {
_state.update { it.copy(hiddenGroupKeys = keys) }
refreshFromCache()
}
// ---- Writable calendars ----