mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
refactor(calendars): rename appointments feed, split collections to own route
Naming: - CalendarsFeedFilter → CalendarAppointmentsFeedFilter, calendarsFeed → calendarAppointmentsFeed. NIP-52 calls kind 31924 the "calendar" (a list of events); kinds 31922/31923 are appointments that *go into* calendars. The old name made `calendarsFeed` look like "the feed of calendars" when it was actually the feed of appointments. Architecture: - Dropped CalendarsViewMode.COLLECTIONS. Calendar collections are a sibling feed, not a view mode of the appointment timeline. They now live on a dedicated CalendarCollectionsScreen reached via the new Route.CalendarCollections, with their own drawer entry and bottom-bar slot under NavBarItem.CALENDAR_COLLECTIONS. - Both screens share the same CalendarsFilterAssembler subscription (which already pulled all four kinds), so opening either keeps the relay subscription warm for the other. https://claude.ai/code/session_01CbyrA2GdM4EQh8T6nsst6U
This commit is contained in:
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadat
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen
|
||||
@@ -256,6 +257,7 @@ fun BuildNavigation(
|
||||
composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) }
|
||||
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Calendars> { CalendarsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.CalendarCollections> { CalendarCollectionsScreen(accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.NewCalendarEvent> { NewCalendarEventScreen(nav, accountViewModel) }
|
||||
composableFromBottomArgs<Route.NewCalendarCollection> { NewCalendarCollectionScreen(nav, accountViewModel) }
|
||||
composableFromEnd<Route.Products> { ProductsScreen(accountViewModel, nav) }
|
||||
|
||||
+9
@@ -51,6 +51,7 @@ enum class NavBarItem {
|
||||
ARTICLES,
|
||||
PICTURES,
|
||||
CALENDARS,
|
||||
CALENDAR_COLLECTIONS,
|
||||
SHORTS,
|
||||
PUBLIC_CHATS,
|
||||
FOLLOW_PACKS,
|
||||
@@ -207,6 +208,13 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
|
||||
icon = MaterialSymbols.CalendarMonth,
|
||||
resolveRoute = { Route.Calendars },
|
||||
),
|
||||
NavBarItem.CALENDAR_COLLECTIONS to
|
||||
NavBarItemDef(
|
||||
id = NavBarItem.CALENDAR_COLLECTIONS,
|
||||
labelRes = R.string.route_calendar_collections,
|
||||
icon = MaterialSymbols.AutoMirrored.FormatListBulleted,
|
||||
resolveRoute = { Route.CalendarCollections },
|
||||
),
|
||||
NavBarItem.SHORTS to
|
||||
NavBarItemDef(
|
||||
id = NavBarItem.SHORTS,
|
||||
@@ -327,6 +335,7 @@ val DrawerFeedsItems: List<NavBarItem> =
|
||||
NavBarItem.ARTICLES,
|
||||
NavBarItem.PICTURES,
|
||||
NavBarItem.CALENDARS,
|
||||
NavBarItem.CALENDAR_COLLECTIONS,
|
||||
NavBarItem.SHORTS,
|
||||
NavBarItem.PUBLIC_CHATS,
|
||||
NavBarItem.FOLLOW_PACKS,
|
||||
|
||||
+2
-2
@@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal.ArticlesFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal.BadgesFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal.CalendarAppointmentsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal.CalendarCollectionsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal.CalendarsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListKnownFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ChatroomListNewFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.dal.CommunitiesFeedFilter
|
||||
@@ -101,7 +101,7 @@ class AccountFeedContentStates(
|
||||
val communitiesList = FeedContentState(CommunitiesFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val picturesFeed = FeedContentState(PictureFeedFilter(account), scope, LocalCache)
|
||||
val calendarsFeed = FeedContentState(CalendarsFeedFilter(account), scope, LocalCache)
|
||||
val calendarAppointmentsFeed = FeedContentState(CalendarAppointmentsFeedFilter(account), scope, LocalCache)
|
||||
val calendarCollectionsFeed = FeedContentState(CalendarCollectionsFeedFilter(account), scope, LocalCache)
|
||||
val productsFeed = FeedContentState(ProductsFeedFilter(account), scope, LocalCache)
|
||||
val shortsFeed = FeedContentState(ShortsFeedFilter(account), scope, LocalCache)
|
||||
|
||||
+3
-1
@@ -86,7 +86,9 @@ private fun PreloadFor(
|
||||
|
||||
NavBarItem.PICTURES -> PicturesFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
NavBarItem.CALENDARS -> CalendarsFilterAssemblerSubscription(accountViewModel)
|
||||
NavBarItem.CALENDARS,
|
||||
NavBarItem.CALENDAR_COLLECTIONS,
|
||||
-> CalendarsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
NavBarItem.SHORTS -> ShortsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
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.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
|
||||
|
||||
/**
|
||||
* Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses
|
||||
* the [CalendarsFilterAssembler] subscription so opening this screen also keeps the appointment
|
||||
* subscription warm — both feeds share one relay subscription.
|
||||
*/
|
||||
@Composable
|
||||
fun CalendarCollectionsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
CalendarCollectionsScreen(
|
||||
feedState = accountViewModel.feedStates.calendarCollectionsFeed,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CalendarCollectionsScreen(
|
||||
feedState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedState)
|
||||
WatchAccountForCalendarCollectionsScreen(feedState, accountViewModel)
|
||||
CalendarsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
CalendarCollectionsTopBar(accountViewModel, nav)
|
||||
},
|
||||
bottomBar = {
|
||||
AppBottomBar(Route.CalendarCollections, nav, accountViewModel) { route ->
|
||||
if (route == Route.CalendarCollections) {
|
||||
feedState.sendToTop()
|
||||
} else {
|
||||
nav.navBottomBar(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingButton = {
|
||||
FabBottomBarPadded(nav) {
|
||||
NewCalendarButton(nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
CalendarCollectionsView(feedState, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchAccountForCalendarCollectionsScreen(
|
||||
feedState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState by accountViewModel.account.liveCalendarsFollowLists.collectAsStateWithLifecycle()
|
||||
val hiddenUsers by
|
||||
accountViewModel.account.hiddenUsers.flow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||
feedState.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun CalendarCollectionsTopBar(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||
val list by accountViewModel.account.settings.defaultCalendarsFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
CalendarCollectionsTopNavFilterBar(
|
||||
followListsModel = accountViewModel.feedStates.feedListOptions,
|
||||
listName = list,
|
||||
accountViewModel = accountViewModel,
|
||||
onChange = accountViewModel.account.settings::changeDefaultCalendarsFollowList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CalendarCollectionsTopNavFilterBar(
|
||||
followListsModel: TopNavFilterState,
|
||||
listName: TopFilter,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChange: (FeedDefinition) -> Unit,
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
|
||||
|
||||
// We could reuse CalendarsTopNavFilterBar verbatim, but the screen title isn't shown by
|
||||
// UserDrawerSearchTopBar's content slot — wrapping the spinner with the route title keeps
|
||||
// the user oriented inside an otherwise filter-only header.
|
||||
Text(
|
||||
text = stringRes(R.string.route_calendar_collections),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
FeedFilterSpinner(
|
||||
placeholderCode = listName,
|
||||
explainer = stringRes(R.string.select_list_to_filter),
|
||||
options = allLists,
|
||||
onSelect = onChange,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
+2
-9
@@ -47,8 +47,7 @@ fun CalendarsScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
CalendarsScreen(
|
||||
feedState = accountViewModel.feedStates.calendarsFeed,
|
||||
collectionsState = accountViewModel.feedStates.calendarCollectionsFeed,
|
||||
feedState = accountViewModel.feedStates.calendarAppointmentsFeed,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
@@ -57,13 +56,11 @@ fun CalendarsScreen(
|
||||
@Composable
|
||||
fun CalendarsScreen(
|
||||
feedState: FeedContentState,
|
||||
collectionsState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedState)
|
||||
WatchLifecycleAndUpdateModel(collectionsState)
|
||||
WatchAccountForCalendarsScreen(feedState, collectionsState, accountViewModel)
|
||||
WatchAccountForCalendarsScreen(feedState, accountViewModel)
|
||||
CalendarsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
var viewMode by rememberSaveable { mutableStateOf(CalendarsViewMode.FEED) }
|
||||
@@ -105,8 +102,6 @@ fun CalendarsScreen(
|
||||
CalendarWeekView(feedState, accountViewModel, nav)
|
||||
CalendarsViewMode.DAY ->
|
||||
CalendarDayView(feedState, accountViewModel, nav)
|
||||
CalendarsViewMode.COLLECTIONS ->
|
||||
CalendarCollectionsView(collectionsState, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +111,6 @@ fun CalendarsScreen(
|
||||
@Composable
|
||||
private fun WatchAccountForCalendarsScreen(
|
||||
feedState: FeedContentState,
|
||||
collectionsState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState by accountViewModel.account.liveCalendarsFollowLists.collectAsStateWithLifecycle()
|
||||
@@ -126,6 +120,5 @@ private fun WatchAccountForCalendarsScreen(
|
||||
|
||||
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||
feedState.checkKeysInvalidateDataAndSendToTop()
|
||||
collectionsState.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -23,6 +23,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
/**
|
||||
* Lenses on the same appointment timeline. Calendar *collections* (kind 31924) live on their
|
||||
* own screen ([CalendarCollectionsScreen]) since they're a sibling feed, not a different view
|
||||
* of the appointment data.
|
||||
*/
|
||||
enum class CalendarsViewMode(
|
||||
@StringRes val labelRes: Int,
|
||||
) {
|
||||
@@ -30,5 +35,4 @@ enum class CalendarsViewMode(
|
||||
MONTH(R.string.calendar_view_month),
|
||||
WEEK(R.string.calendar_view_week),
|
||||
DAY(R.string.calendar_view_day),
|
||||
COLLECTIONS(R.string.calendar_view_collections),
|
||||
}
|
||||
|
||||
+6
-1
@@ -29,7 +29,12 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
|
||||
class CalendarsFeedFilter(
|
||||
/**
|
||||
* Feed of NIP-52 calendar *appointments* — kinds 31922 (date-slot) and 31923 (time-slot). The
|
||||
* NIP calls kind 31924 a "calendar" (a list of appointments), so this filter intentionally does
|
||||
* not load 31924; see [CalendarCollectionsFeedFilter] for that.
|
||||
*/
|
||||
class CalendarAppointmentsFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
|
||||
+2
-2
@@ -44,7 +44,7 @@ class CalendarsSubAssembler(
|
||||
): List<RelayBasedFilter> {
|
||||
val feedSettings = key.followsPerRelay()
|
||||
|
||||
return makeCalendarsFilter(feedSettings, since, key.feedStates.calendarsFeed.lastNoteCreatedAtIfFilled())
|
||||
return makeCalendarsFilter(feedSettings, since, key.feedStates.calendarAppointmentsFeed.lastNoteCreatedAtIfFilled())
|
||||
}
|
||||
|
||||
override fun user(key: CalendarsQueryState) = key.account.userProfile()
|
||||
@@ -78,7 +78,7 @@ class CalendarsSubAssembler(
|
||||
}
|
||||
},
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
key.feedStates.calendarsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||
key.feedStates.calendarAppointmentsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1853,6 +1853,7 @@
|
||||
<string name="route_video">Shorts</string>
|
||||
<string name="route_pictures">Pictures</string>
|
||||
<string name="route_calendars">Calendars</string>
|
||||
<string name="route_calendar_collections">Calendar lists</string>
|
||||
<string name="route_chess">Chess</string>
|
||||
<string name="wallet">Wallet</string>
|
||||
<string name="wallet_balance">Balance</string>
|
||||
@@ -1926,7 +1927,6 @@
|
||||
<string name="calendar_view_month">Month</string>
|
||||
<string name="calendar_view_week">Week</string>
|
||||
<string name="calendar_view_day">Day</string>
|
||||
<string name="calendar_view_collections">Calendars</string>
|
||||
|
||||
<string name="calendar_section_upcoming">Upcoming</string>
|
||||
<string name="calendar_section_past">Past</string>
|
||||
|
||||
Reference in New Issue
Block a user