diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b0d5445a6f..79ce1181cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -82,6 +82,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.bookmarkgroups.repositories.BookmarkedRepositoriesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.BrowserScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen @@ -444,6 +445,7 @@ fun BuildNavigation( composableFromEnd { BookmarkListScreen(accountViewModel, nav) } composableFromEnd { OldBookmarkListScreen(accountViewModel, nav) } composableFromEnd { PinnedNotesScreen(accountViewModel, nav) } + composableFromEnd { BookmarkedRepositoriesScreen(accountViewModel, nav) } composableFromEnd { WebBookmarksScreen(accountViewModel, nav) } composableFromEnd { DraftListScreen(accountViewModel, nav) } composableFromEnd { ScheduledPostsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 261ed4eabc..42760f3437 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -302,6 +302,8 @@ sealed class Route { @Serializable object PinnedNotes : Route() + @Serializable object BookmarkedRepositories : Route() + @Serializable object BookmarkGroups : Route() @Serializable object InterestSets : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt index b66e6d5b55..9dd55c401d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/BookmarkListScreen.kt @@ -55,7 +55,6 @@ import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPrivateFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkPublicFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal.BookmarkRepositoriesFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import kotlinx.coroutines.launch @@ -77,31 +76,18 @@ fun BookmarkListScreen( factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account), ) - val repositoriesFeedViewModel: BookmarkRepositoriesFeedViewModel = - viewModel( - key = "NostrBookmarkRepositoriesFeedViewModel", - factory = BookmarkRepositoriesFeedViewModel.Factory(accountViewModel.account), - ) - val bookmarkState by accountViewModel.account.bookmarkState.bookmarks .collectAsStateWithLifecycle(null) - val repositoryBookmarks by accountViewModel.account.gitRepositoryListState.publicRepositoryAddressSet - .collectAsStateWithLifecycle() - LaunchedEffect(bookmarkState) { publicFeedViewModel.invalidateData() privateFeedViewModel.invalidateData() } - LaunchedEffect(repositoryBookmarks) { - repositoriesFeedViewModel.invalidateData() - } - // Preload all bookmarked events so they don't load one-by-one when scrolling - PreloadBookmarkEvents(bookmarkState, repositoryBookmarks, accountViewModel) + PreloadBookmarkEvents(bookmarkState, accountViewModel) - RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, repositoriesFeedViewModel, bookmarkState, accountViewModel, nav) + RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, bookmarkState, accountViewModel, nav) } @Composable @@ -109,12 +95,11 @@ fun BookmarkListScreen( private fun RenderBookmarkScreen( publicFeedViewModel: BookmarkPublicFeedViewModel, privateFeedViewModel: BookmarkPrivateFeedViewModel, - repositoriesFeedViewModel: BookmarkRepositoriesFeedViewModel, bookmarkState: com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState.BookmarkList?, accountViewModel: AccountViewModel, nav: INav, ) { - val pagerState = rememberPagerState { 3 } + val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() val cache = accountViewModel.account.cache @@ -161,11 +146,6 @@ private fun RenderBookmarkScreen( onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, text = { Text(text = stringRes(R.string.public_bookmarks)) }, ) - Tab( - selected = pagerState.currentPage == 2, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } }, - text = { Text(text = stringRes(R.string.repository_bookmarks)) }, - ) } } }, @@ -191,15 +171,6 @@ private fun RenderBookmarkScreen( nav = nav, ) } - - 2 -> { - RefresheableFeedView( - repositoriesFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } } } @@ -227,18 +198,14 @@ private fun RenderBookmarkScreen( @Composable private fun PreloadBookmarkEvents( bookmarkState: com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState.BookmarkList?, - repositoryBookmarks: Set, accountViewModel: AccountViewModel, ) { val eventFinder = accountViewModel.dataSources().eventFinder val account = accountViewModel.account val queries = - remember(bookmarkState, repositoryBookmarks) { - val allNotes = - bookmarkState?.public.orEmpty() + - bookmarkState?.private.orEmpty() + - repositoryBookmarks.map { account.cache.getOrCreateAddressableNote(it) } + remember(bookmarkState) { + val allNotes = bookmarkState?.public.orEmpty() + bookmarkState?.private.orEmpty() allNotes .filter { it.event == null } .map { EventFinderQueryState(it, account) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt index af5c4bcf59..37caa116b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt @@ -42,6 +42,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState @@ -59,10 +60,12 @@ fun ListOfBookmarkGroupsFeedView( defaultBookmarks: BookmarkListState, oldBookmarks: OldBookmarkListState, pinnedNotes: PinListState, + repositories: GitRepositoryListState, groupListFeedSource: StateFlow>, openDefaultBookmarks: () -> Unit, openOldBookmarks: () -> Unit, openPinnedNotes: () -> Unit, + openRepositories: () -> Unit, onOpenItem: (String, BookmarkType) -> Unit, onRenameItem: (targetBookmarkGroup: LabeledBookmarkList) -> Unit, onItemDescriptionChange: (bookmarkGroup: LabeledBookmarkList) -> Unit, @@ -91,6 +94,11 @@ fun ListOfBookmarkGroupsFeedView( HorizontalDivider(thickness = DividerThickness) } + item { + RepositoriesBookmarkList(repositories, openRepositories) + HorizontalDivider(thickness = DividerThickness) + } + itemsIndexed( bookmarkGroupFeedState, key = { _: Int, item: LabeledBookmarkList -> item.identifier }, @@ -197,6 +205,50 @@ fun PinnedNotesList( ) } +@Composable +fun RepositoriesBookmarkList( + repositories: GitRepositoryListState, + openRepositories: () -> Unit, +) { + val repositoryAddresses by repositories.publicRepositoryAddressSet.collectAsStateWithLifecycle() + + ListItem( + modifier = Modifier.clickable(onClick = openRepositories), + headlineContent = { + Text(stringRes(R.string.repository_bookmarks), maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + supportingContent = { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + stringRes(R.string.repository_bookmarks_explainer), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } + }, + leadingContent = { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + symbol = MaterialSymbols.Code, + contentDescription = stringRes(R.string.bookmark_list_icon_label), + modifier = Size40Modifier, + ) + Spacer(StdVertSpacer) + BookmarkMembershipStatusAndNumberDisplay( + modifier = Modifier.align(Alignment.CenterHorizontally), + postBookmarksSize = repositoryAddresses.size, + articleBookmarksSize = 0, + ) + } + }, + ) +} + @Composable fun OldBookmarkList( oldBookmarks: OldBookmarkListState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt index 2ac0c207cd..e1aa912ebd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState @@ -59,10 +60,12 @@ fun ListOfBookmarkGroupsScreen( defaultBookmarks = accountViewModel.account.bookmarkState, oldBookmarks = accountViewModel.account.oldBookmarkState, pinnedNotes = accountViewModel.account.pinState, + repositories = accountViewModel.account.gitRepositoryListState, listSource = accountViewModel.account.labeledBookmarkLists.listFeedFlow, openDefaultBookmarks = { nav.nav(Route.Bookmarks) }, openOldBookmarks = { nav.nav(Route.OldBookmarks) }, openPinnedNotes = { nav.nav(Route.PinnedNotes) }, + openRepositories = { nav.nav(Route.BookmarkedRepositories) }, addBookmarkGroup = { nav.nav(Route.BookmarkGroupMetadataEdit()) }, openBookmarkGroup = { identifier, bookmarkType -> nav.nav(Route.BookmarkGroupView(identifier, bookmarkType)) @@ -101,10 +104,12 @@ fun ListOfBookmarkGroupsFeed( defaultBookmarks: BookmarkListState, oldBookmarks: OldBookmarkListState, pinnedNotes: PinListState, + repositories: GitRepositoryListState, listSource: StateFlow>, openDefaultBookmarks: () -> Unit, openOldBookmarks: () -> Unit, openPinnedNotes: () -> Unit, + openRepositories: () -> Unit, addBookmarkGroup: () -> Unit, openBookmarkGroup: (identifier: String, bookmarkType: BookmarkType) -> Unit, renameBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit, @@ -148,10 +153,12 @@ fun ListOfBookmarkGroupsFeed( defaultBookmarks = defaultBookmarks, oldBookmarks = oldBookmarks, pinnedNotes = pinnedNotes, + repositories = repositories, groupListFeedSource = listSource, openDefaultBookmarks = openDefaultBookmarks, openOldBookmarks = openOldBookmarks, openPinnedNotes = openPinnedNotes, + openRepositories = openRepositories, onOpenItem = openBookmarkGroup, onRenameItem = renameBookmarkGroup, onItemDescriptionChange = changeBookmarkGroupDescription, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/BookmarkedRepositoriesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/BookmarkedRepositoriesScreen.kt new file mode 100644 index 0000000000..9f5749cd77 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/BookmarkedRepositoriesScreen.kt @@ -0,0 +1,100 @@ +/* + * 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.bookmarkgroups.repositories + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.repositories.dal.BookmarkRepositoriesFeedViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.Address + +@Composable +fun BookmarkedRepositoriesScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val repositoriesFeedViewModel: BookmarkRepositoriesFeedViewModel = + viewModel( + key = "NostrBookmarkRepositoriesFeedViewModel", + factory = BookmarkRepositoriesFeedViewModel.Factory(accountViewModel.account), + ) + + val repositoryBookmarks by accountViewModel.account.gitRepositoryListState.publicRepositoryAddressSet + .collectAsStateWithLifecycle() + + LaunchedEffect(repositoryBookmarks) { + repositoriesFeedViewModel.invalidateData() + } + + // Preload any bookmarked repo announcements not yet in cache so they don't pop in one by one. + PreloadRepositoryEvents(repositoryBookmarks, accountViewModel) + + DisappearingScaffold( + isInvertedLayout = false, + topBar = { + TopBarWithBackButton(stringRes(id = R.string.repository_bookmarks), nav) + }, + accountViewModel = accountViewModel, + ) { + RefresheableFeedView( + repositoriesFeedViewModel, + null, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +private fun PreloadRepositoryEvents( + repositoryBookmarks: Set
, + accountViewModel: AccountViewModel, +) { + val eventFinder = accountViewModel.dataSources().eventFinder + val account = accountViewModel.account + + val queries = + remember(repositoryBookmarks) { + repositoryBookmarks + .map { account.cache.getOrCreateAddressableNote(it) } + .filter { it.event == null } + .map { EventFinderQueryState(it, account) } + } + + DisposableEffect(queries) { + eventFinder.subscribe(queries) + onDispose { + eventFinder.unsubscribe(queries) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedFilter.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedFilter.kt index efc085e715..a04feb1f8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedFilter.kt @@ -18,7 +18,7 @@ * 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.bookmarkgroups.default.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.repositories.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note @@ -27,8 +27,7 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter /** * The user's bookmarked (starred) git repositories — the public NIP-51 kind 10018 * [com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState] addresses - * resolved to their addressable notes, newest first. Mirrors [BookmarkPublicFeedFilter] but - * sources its list from the repository bookmark state instead of the kind-10003 note bookmarks. + * resolved to their addressable notes, newest first. */ class BookmarkRepositoriesFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedViewModel.kt index ea793be1d6..cbef857aec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/default/dal/BookmarkRepositoriesFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/repositories/dal/BookmarkRepositoriesFeedViewModel.kt @@ -18,7 +18,7 @@ * 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.bookmarkgroups.default.dal +package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.repositories.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 2435ef4c32..a80072af2b 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1017,6 +1017,7 @@ Private Bookmarks Public Bookmarks Repositories + Your bookmarked git repositories Add to Private Bookmarks Add to Public Bookmarks Remove from Private Bookmarks