mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
fix(calendars): top-bar filter switch resets scroll; week/month/day headers scroll with the disappearing bar
Two related fixes for the calendar surface: 1. Filter switch leaving the user in the past section. The feed-content state already calls sendToTop() when feedKey changes, but the calendar feed's LazyColumn never subscribed to the scrollToTop signal — so a filter switch (e.g. People List → Global) preserved the previous scroll position. Often that landed the user mid-feed in the "Past" section of the much larger Global list, looking like "the events didn't come back". Added a LazyListState + WatchScrollToTop wiring. 2. Week/month/day headers stayed pinned when the DisappearingScaffold's top bar collapsed on scroll. The views had a Column with disappearingScaffoldPadding() that reserved a fixed top inset, so when the bar slid up the headers stayed put and a visual gap opened. Pulled the nav header (and for the week view, the WeekStrip + DaySummaryHeader) into the same LazyColumn as the events, with rememberFeedContentPadding as contentPadding — now the whole stack scrolls under the bar together.
This commit is contained in:
+43
-45
@@ -57,10 +57,12 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpan
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@@ -91,58 +93,54 @@ fun CalendarDayView(
|
||||
val byDay by remember(notes) { derivedStateOf { groupByDayKeyExpanded(notes) } }
|
||||
val dayEvents = byDay[visibleDate.toEpochDay()].orEmpty()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.disappearingScaffoldPadding()
|
||||
.calendarSwipeNavigation(
|
||||
key = visibleEpochDay,
|
||||
onSwipeLeft = { visibleEpochDay = visibleDate.plusDays(1).toEpochDay() },
|
||||
onSwipeRight = { visibleEpochDay = visibleDate.minusDays(1).toEpochDay() },
|
||||
),
|
||||
) {
|
||||
CalendarNavigationHeader(
|
||||
title = formatLongDate(visibleDate.atStartOfDay(ZoneId.systemDefault()).toEpochSecond()),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_day),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_day),
|
||||
onPrev = { visibleEpochDay = visibleDate.minusDays(1).toEpochDay() },
|
||||
onNext = { visibleEpochDay = visibleDate.plusDays(1).toEpochDay() },
|
||||
onToday = { visibleEpochDay = LocalDate.now().toEpochDay() },
|
||||
)
|
||||
|
||||
if (dayEvents.isEmpty()) {
|
||||
CalendarEmptyState(
|
||||
title = stringRes(R.string.calendar_empty_day_title),
|
||||
subtitle = stringRes(R.string.calendar_empty_day_subtitle),
|
||||
)
|
||||
return@Column
|
||||
}
|
||||
|
||||
DayTimeline(dayEvents, visibleEpochDay, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayTimeline(
|
||||
dayEvents: List<Note>,
|
||||
visibleEpochDay: Long,
|
||||
nav: INav,
|
||||
) {
|
||||
val sorted =
|
||||
remember(dayEvents) {
|
||||
// All-day events bubble to the top (Long.MIN_VALUE), then time-slot events in order.
|
||||
dayEvents.sortedBy { it.appointmentView()?.startSeconds ?: Long.MAX_VALUE }
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
||||
items(sorted, key = { it.idHex }) { note ->
|
||||
DayRow(
|
||||
note = note,
|
||||
visibleEpochDay = visibleEpochDay,
|
||||
onClick = { nav.nav(Route.Note(note.idHex)) },
|
||||
// Single LazyColumn — nav header is the first item so it scrolls with the disappearing
|
||||
// top bar instead of staying pinned mid-screen when the bar collapses.
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.calendarSwipeNavigation(
|
||||
key = visibleEpochDay,
|
||||
onSwipeLeft = { visibleEpochDay = visibleDate.plusDays(1).toEpochDay() },
|
||||
onSwipeRight = { visibleEpochDay = visibleDate.minusDays(1).toEpochDay() },
|
||||
),
|
||||
) {
|
||||
item(key = "day-nav") {
|
||||
CalendarNavigationHeader(
|
||||
title = formatLongDate(visibleDate.atStartOfDay(ZoneId.systemDefault()).toEpochSecond()),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_day),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_day),
|
||||
onPrev = { visibleEpochDay = visibleDate.minusDays(1).toEpochDay() },
|
||||
onNext = { visibleEpochDay = visibleDate.plusDays(1).toEpochDay() },
|
||||
onToday = { visibleEpochDay = LocalDate.now().toEpochDay() },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (dayEvents.isEmpty()) {
|
||||
item(key = "day-empty") {
|
||||
CalendarEmptyState(
|
||||
title = stringRes(R.string.calendar_empty_day_title),
|
||||
subtitle = stringRes(R.string.calendar_empty_day_subtitle),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(sorted, key = { it.idHex }) { note ->
|
||||
Column(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
DayRow(
|
||||
note = note,
|
||||
visibleEpochDay = visibleEpochDay,
|
||||
onClick = { nav.nav(Route.Note(note.idHex)) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -44,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -62,7 +64,7 @@ fun CalendarFeedView(
|
||||
val state by feedState.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
when (val s = state) {
|
||||
is FeedState.Loaded -> CalendarFeedLoadedBody(s, accountViewModel, nav, filterAddresses)
|
||||
is FeedState.Loaded -> CalendarFeedLoadedBody(s, feedState, accountViewModel, nav, filterAddresses)
|
||||
is FeedState.Empty -> CalendarFeedEmpty()
|
||||
is FeedState.Loading -> Box(modifier = Modifier.fillMaxSize())
|
||||
is FeedState.FeedError -> CalendarFeedError(s)
|
||||
@@ -73,6 +75,7 @@ fun CalendarFeedView(
|
||||
@Composable
|
||||
private fun CalendarFeedLoadedBody(
|
||||
loaded: FeedState.Loaded,
|
||||
feedState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
filterAddresses: Set<com.vitorpamplona.quartz.nip01Core.core.Address>?,
|
||||
@@ -85,7 +88,15 @@ private fun CalendarFeedLoadedBody(
|
||||
}
|
||||
}
|
||||
|
||||
// Without this the top-bar filter switch fires `sendToTop()`, but the LazyColumn never hears
|
||||
// it — so the scroll position from the previous filter (e.g. mid-way through a tiny People
|
||||
// List) is preserved when the user flips back to Global, leaving the user staring at the
|
||||
// past-events section of a 100-item feed instead of the top.
|
||||
val listState = rememberLazyListState()
|
||||
WatchScrollToTop(feedState, listState)
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
|
||||
+46
-35
@@ -61,9 +61,11 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.computeMonthGridBa
|
||||
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpanded
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
@@ -102,11 +104,15 @@ fun CalendarMonthView(
|
||||
|
||||
var selectedDayKey by rememberSaveable { mutableStateOf<Long?>(null) }
|
||||
|
||||
Column(
|
||||
val selectedEvents = selectedDayKey?.let { eventsByDay[it] }.orEmpty()
|
||||
|
||||
// Single LazyColumn — nav header + weekday header + grid scroll together with the
|
||||
// disappearing top bar so the grid doesn't stay pinned mid-screen when the bar collapses.
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.disappearingScaffoldPadding()
|
||||
.calendarSwipeNavigation(
|
||||
key = visibleYear to visibleMonthValue,
|
||||
onSwipeLeft = {
|
||||
@@ -119,44 +125,49 @@ fun CalendarMonthView(
|
||||
},
|
||||
),
|
||||
) {
|
||||
CalendarNavigationHeader(
|
||||
title = formatMonthYear(visibleMonth.year, visibleMonth.monthValue - 1),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_month),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_month),
|
||||
onPrev = {
|
||||
setVisibleMonth(visibleMonth.minusMonths(1))
|
||||
selectedDayKey = null
|
||||
},
|
||||
onNext = {
|
||||
setVisibleMonth(visibleMonth.plusMonths(1))
|
||||
selectedDayKey = null
|
||||
},
|
||||
onToday = {
|
||||
setVisibleMonth(YearMonth.from(LocalDate.now()))
|
||||
selectedDayKey = null
|
||||
},
|
||||
)
|
||||
item(key = "month-nav") {
|
||||
CalendarNavigationHeader(
|
||||
title = formatMonthYear(visibleMonth.year, visibleMonth.monthValue - 1),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_month),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_month),
|
||||
onPrev = {
|
||||
setVisibleMonth(visibleMonth.minusMonths(1))
|
||||
selectedDayKey = null
|
||||
},
|
||||
onNext = {
|
||||
setVisibleMonth(visibleMonth.plusMonths(1))
|
||||
selectedDayKey = null
|
||||
},
|
||||
onToday = {
|
||||
setVisibleMonth(YearMonth.from(LocalDate.now()))
|
||||
selectedDayKey = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
WeekdayHeader()
|
||||
item(key = "month-weekday-header") {
|
||||
WeekdayHeader()
|
||||
}
|
||||
|
||||
MonthGrid(
|
||||
visibleMonth = visibleMonth,
|
||||
today = today,
|
||||
barsByDay = barsByDay,
|
||||
selectedDayKey = selectedDayKey,
|
||||
onDayClick = { dayKey ->
|
||||
selectedDayKey = if (selectedDayKey == dayKey) null else dayKey
|
||||
},
|
||||
)
|
||||
item(key = "month-grid") {
|
||||
MonthGrid(
|
||||
visibleMonth = visibleMonth,
|
||||
today = today,
|
||||
barsByDay = barsByDay,
|
||||
selectedDayKey = selectedDayKey,
|
||||
onDayClick = { dayKey ->
|
||||
selectedDayKey = if (selectedDayKey == dayKey) null else dayKey
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
item(key = "month-grid-spacer") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
val selectedEvents = selectedDayKey?.let { eventsByDay[it] }.orEmpty()
|
||||
if (selectedEvents.isNotEmpty()) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(selectedEvents, key = { it.idHex }) { note ->
|
||||
CalendarEventListCard(note, accountViewModel, nav)
|
||||
}
|
||||
items(selectedEvents, key = { it.idHex }) { note ->
|
||||
CalendarEventListCard(note, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-39
@@ -57,9 +57,11 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpan
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@@ -92,11 +94,18 @@ fun CalendarWeekView(
|
||||
|
||||
val eventsByDay by remember(notes) { derivedStateOf { groupByDayKeyExpanded(notes) } }
|
||||
|
||||
Column(
|
||||
val selectedDate = weekStart.plusDays(selectedDayIndex.toLong())
|
||||
val dayNotes = eventsByDay[selectedDate.toEpochDay()].orEmpty()
|
||||
|
||||
// Single LazyColumn containing nav + strip + day-summary + events so the whole stack
|
||||
// scrolls together with the [DisappearingScaffold]'s top bar. The previous Column-with-
|
||||
// disappearingScaffoldPadding kept the strip pinned at a fixed offset, so when the top bar
|
||||
// collapsed on scroll the strip stayed put and left a visual gap.
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.disappearingScaffoldPadding()
|
||||
.calendarSwipeNavigation(
|
||||
key = weekStartEpochDay,
|
||||
onSwipeLeft = {
|
||||
@@ -109,49 +118,54 @@ fun CalendarWeekView(
|
||||
},
|
||||
),
|
||||
) {
|
||||
CalendarNavigationHeader(
|
||||
title = formatMonthYear(weekStart.year, weekStart.monthValue - 1),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_week),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_week),
|
||||
onPrev = {
|
||||
weekStartEpochDay = weekStart.minusWeeks(1).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
onNext = {
|
||||
weekStartEpochDay = weekStart.plusWeeks(1).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
onToday = {
|
||||
weekStartEpochDay = startOfWeek(LocalDate.now()).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
)
|
||||
item(key = "week-nav") {
|
||||
CalendarNavigationHeader(
|
||||
title = formatMonthYear(weekStart.year, weekStart.monthValue - 1),
|
||||
prevContentDescription = stringRes(R.string.calendar_nav_previous_week),
|
||||
nextContentDescription = stringRes(R.string.calendar_nav_next_week),
|
||||
onPrev = {
|
||||
weekStartEpochDay = weekStart.minusWeeks(1).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
onNext = {
|
||||
weekStartEpochDay = weekStart.plusWeeks(1).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
onToday = {
|
||||
weekStartEpochDay = startOfWeek(LocalDate.now()).toEpochDay()
|
||||
selectedDayIndex = 0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
WeekStrip(
|
||||
weekStart = weekStart,
|
||||
today = today,
|
||||
selectedIndex = selectedDayIndex,
|
||||
eventsByDay = eventsByDay,
|
||||
onSelect = { selectedDayIndex = it },
|
||||
)
|
||||
item(key = "week-strip") {
|
||||
WeekStrip(
|
||||
weekStart = weekStart,
|
||||
today = today,
|
||||
selectedIndex = selectedDayIndex,
|
||||
eventsByDay = eventsByDay,
|
||||
onSelect = { selectedDayIndex = it },
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
item(key = "week-spacer") {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
val selectedDate = weekStart.plusDays(selectedDayIndex.toLong())
|
||||
val dayNotes = eventsByDay[selectedDate.toEpochDay()].orEmpty()
|
||||
|
||||
DaySummaryHeader(selectedDate)
|
||||
item(key = "week-day-summary") {
|
||||
DaySummaryHeader(selectedDate)
|
||||
}
|
||||
|
||||
if (dayNotes.isEmpty()) {
|
||||
CalendarEmptyState(
|
||||
title = stringRes(R.string.calendar_empty_week_title),
|
||||
subtitle = stringRes(R.string.calendar_empty_week_subtitle),
|
||||
)
|
||||
item(key = "week-empty") {
|
||||
CalendarEmptyState(
|
||||
title = stringRes(R.string.calendar_empty_week_title),
|
||||
subtitle = stringRes(R.string.calendar_empty_week_subtitle),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(dayNotes, key = { it.idHex }) { note ->
|
||||
CalendarEventListCard(note, accountViewModel, nav)
|
||||
}
|
||||
items(dayNotes, key = { it.idHex }) { note ->
|
||||
CalendarEventListCard(note, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user