diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt index 7f8a89ff86..a937a04db2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayView.kt @@ -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, - 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() + } + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt index 490c5807be..b5e311f8b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt @@ -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?, @@ -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(), ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt index 767671fd22..648210bee6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt @@ -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(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) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt index 3741e2035d..c71f126c2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt @@ -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) } } }