mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(git): bookmarked repositories as a standalone screen
Replace the third "Repositories" tab on the default bookmark screen with a dedicated entry on the bookmark-lists screen, mirroring how Pinned Notes works: a row in ListOfBookmarkGroupsFeedView that opens its own BookmarkedRepositoriesScreen via the new Route.BookmarkedRepositories. The row shows the bookmarked-repo count from gitRepositoryListState.publicRepositoryAddressSet; the screen renders the BookmarkRepositoriesFeedViewModel feed (moved to a repositories/ package), invalidates on bookmark changes, and preloads any uncached repo announcements via the EventFinder. Reverts the tab added to BookmarkListScreen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DpNmN8CvP6HnEsdTGAjVUr
This commit is contained in:
@@ -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<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.OldBookmarks> { OldBookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.PinnedNotes> { PinnedNotesScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.BookmarkedRepositories> { BookmarkedRepositoriesScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.WebBookmarks> { WebBookmarksScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ScheduledPosts> { ScheduledPostsScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -302,6 +302,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object PinnedNotes : Route()
|
||||
|
||||
@Serializable object BookmarkedRepositories : Route()
|
||||
|
||||
@Serializable object BookmarkGroups : Route()
|
||||
|
||||
@Serializable object InterestSets : Route()
|
||||
|
||||
+5
-38
@@ -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<com.vitorpamplona.quartz.nip01Core.core.Address>,
|
||||
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) }
|
||||
|
||||
+52
@@ -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<List<LabeledBookmarkList>>,
|
||||
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,
|
||||
|
||||
+7
@@ -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<List<LabeledBookmarkList>>,
|
||||
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,
|
||||
|
||||
+100
@@ -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<Address>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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,
|
||||
+1
-1
@@ -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
|
||||
@@ -1017,6 +1017,7 @@
|
||||
<string name="private_bookmarks">Private Bookmarks</string>
|
||||
<string name="public_bookmarks">Public Bookmarks</string>
|
||||
<string name="repository_bookmarks">Repositories</string>
|
||||
<string name="repository_bookmarks_explainer">Your bookmarked git repositories</string>
|
||||
<string name="add_to_private_bookmarks">Add to Private Bookmarks</string>
|
||||
<string name="add_to_public_bookmarks">Add to Public Bookmarks</string>
|
||||
<string name="remove_from_private_bookmarks">Remove from Private Bookmarks</string>
|
||||
|
||||
Reference in New Issue
Block a user