Adds the default bookmark option to be inside the bookmark list screen

This commit is contained in:
Vitor Pamplona
2026-01-05 13:38:24 -05:00
parent 2fc7e505a3
commit a9df6fcc61
18 changed files with 307 additions and 249 deletions
@@ -1554,6 +1554,15 @@ class Account(
}
}
suspend fun removeBookmark(note: Note) {
if (!isWriteable() || note.isDraft()) return
val event = bookmarkState.removeBookmark(note)
if (event != null) {
sendMyPublicAndPrivateOutbox(event)
}
}
suspend fun createAuthEvent(
relay: NormalizedRelayUrl,
challenge: String,
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.model.nip51Lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@@ -40,7 +41,9 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import java.lang.reflect.Modifier.isPrivate
@Stable
class BookmarkListState(
val signer: NostrSigner,
val cache: LocalCache,
@@ -274,4 +277,26 @@ class BookmarkListState(
null
}
}
suspend fun removeBookmark(note: Note): BookmarkListEvent? {
val bookmarkList = getBookmarkList()
return if (bookmarkList != null) {
if (note is AddressableNote) {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()),
signer = signer,
)
} else {
BookmarkListEvent.remove(
earlierVersion = bookmarkList,
bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()),
signer = signer,
)
}
} else {
null
}
}
}
@@ -58,12 +58,12 @@ import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen
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.bookmarks.BookmarkListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen
@@ -46,7 +46,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material.icons.outlined.CloudUpload
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.Drafts
@@ -458,14 +457,6 @@ fun ListContent(
NavigationRow(
title = R.string.bookmarks,
icon = Icons.Outlined.BookmarkBorder,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
route = Route.Bookmarks,
)
NavigationRow(
title = R.string.bookmark_lists,
icon = Icons.Outlined.CollectionsBookmark,
tint = MaterialTheme.colorScheme.onBackground,
nav = nav,
@@ -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.bookmarks
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
@@ -44,8 +44,8 @@ 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.bookmarks.dal.BookmarkPrivateFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal.BookmarkPublicFeedViewModel
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.stringRes
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import kotlinx.coroutines.launch
@@ -93,7 +93,7 @@ private fun RenderBookmarkScreen(
isInvertedLayout = false,
topBar = {
Column {
TopBarWithBackButton(stringRes(id = R.string.bookmarks), nav::popBack)
TopBarWithBackButton(stringRes(id = R.string.bookmarks_title), nav::popBack)
TabRow(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
@@ -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.bookmarks.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
@@ -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.bookmarks.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
@@ -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.bookmarks.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
@@ -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.bookmarks.dal
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.default.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
@@ -42,7 +42,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -59,6 +58,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -118,7 +118,7 @@ fun BookmarkGroupScreenView(
Scaffold(
topBar = {
Column {
TopAppBar(
ShorterTopAppBar(
title = {
TitleAndDescription(bookmarkGroupViewModel)
},
@@ -20,34 +20,43 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.Size40Modifier
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import kotlinx.coroutines.flow.StateFlow
@Composable
fun ListOfBookmarkGroupsFeedView(
defaultBookmarks: BookmarkListState,
groupListFeedSource: StateFlow<List<LabeledBookmarkList>>,
openDefaultBookmarks: () -> Unit,
onOpenItem: (String, BookmarkType) -> Unit,
onRenameItem: (targetBookmarkGroup: LabeledBookmarkList) -> Unit,
onItemDescriptionChange: (bookmarkGroup: LabeledBookmarkList) -> Unit,
@@ -56,40 +65,73 @@ fun ListOfBookmarkGroupsFeedView(
) {
val bookmarkGroupFeedState by groupListFeedSource.collectAsStateWithLifecycle()
if (bookmarkGroupFeedState.isEmpty()) {
BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg))
} else {
LazyColumn(
state = rememberLazyListState(),
contentPadding = FeedPadding,
) {
itemsIndexed(
bookmarkGroupFeedState,
key = { _: Int, item: LabeledBookmarkList -> item.identifier },
) { _, groupItem ->
BookmarkGroupItem(
modifier = Modifier.fillMaxSize().animateItem(),
bookmarkList = groupItem,
onClick = { bookmarkType -> onOpenItem(groupItem.identifier, bookmarkType) },
onRename = { onRenameItem(groupItem) },
onDescriptionChange = { onItemDescriptionChange(groupItem) },
onClone = { cloneName, cloneDescription -> onItemClone(groupItem, cloneName, cloneDescription) },
onDelete = { onDeleteItem(groupItem) },
)
HorizontalDivider(thickness = DividerThickness)
}
LazyColumn(
state = rememberLazyListState(),
contentPadding = FeedPadding,
) {
item {
DefaultBookmarkList(defaultBookmarks, openDefaultBookmarks)
HorizontalDivider(thickness = DividerThickness)
}
itemsIndexed(
bookmarkGroupFeedState,
key = { _: Int, item: LabeledBookmarkList -> item.identifier },
) { _, groupItem ->
BookmarkGroupItem(
modifier = Modifier.fillMaxSize().animateItem(),
bookmarkList = groupItem,
onClick = { bookmarkType -> onOpenItem(groupItem.identifier, bookmarkType) },
onRename = { onRenameItem(groupItem) },
onDescriptionChange = { onItemDescriptionChange(groupItem) },
onClone = { cloneName, cloneDescription -> onItemClone(groupItem, cloneName, cloneDescription) },
onDelete = { onDeleteItem(groupItem) },
)
HorizontalDivider(thickness = DividerThickness)
}
}
}
@Composable
fun BookmarkGroupsFeedEmpty(message: String = stringRes(R.string.feed_is_empty)) {
Column(
Modifier.fillMaxSize().padding(horizontal = Size40dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(message)
Spacer(modifier = StdVertSpacer)
}
fun DefaultBookmarkList(
defaultBookmarks: BookmarkListState,
openDefaultBookmarks: () -> Unit,
) {
val bookmarkState by defaultBookmarks.bookmarks.collectAsStateWithLifecycle()
ListItem(
modifier = Modifier.clickable(onClick = openDefaultBookmarks),
headlineContent = {
Text(stringRes(R.string.bookmarks_title), maxLines = 1, overflow = TextOverflow.Ellipsis)
},
supportingContent = {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Text(
stringRes(R.string.bookmarks_explainer),
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
}
},
leadingContent = {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Outlined.BookmarkBorder,
contentDescription = stringRes(R.string.bookmark_list_icon_label),
modifier = Size40Modifier,
)
Spacer(StdVertSpacer)
BookmarkMembershipStatusAndNumberDisplay(
modifier = Modifier.align(Alignment.CenterHorizontally),
postBookmarksSize = bookmarkState.public.size + bookmarkState.private.size,
articleBookmarksSize = 0,
)
}
},
)
}
@@ -34,6 +34,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -49,7 +50,9 @@ fun ListOfBookmarkGroupsScreen(
nav: INav,
) {
ListOfBookmarkGroupsFeed(
defaultBookmarks = accountViewModel.account.bookmarkState,
listSource = accountViewModel.account.labeledBookmarkLists.listFeedFlow,
openDefaultBookmarks = { nav.nav(Route.Bookmarks) },
addBookmarkGroup = { nav.nav(Route.BookmarkGroupMetadataEdit()) },
openBookmarkGroup = { identifier, bookmarkType ->
nav.nav(Route.BookmarkGroupView(identifier, bookmarkType))
@@ -84,7 +87,9 @@ fun ListOfBookmarkGroupsScreen(
@Composable
fun ListOfBookmarkGroupsFeed(
defaultBookmarks: BookmarkListState,
listSource: StateFlow<List<LabeledBookmarkList>>,
openDefaultBookmarks: () -> Unit,
addBookmarkGroup: () -> Unit,
openBookmarkGroup: (identifier: String, bookmarkType: BookmarkType) -> Unit,
renameBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit,
@@ -109,7 +114,9 @@ fun ListOfBookmarkGroupsFeed(
).fillMaxHeight(),
) {
ListOfBookmarkGroupsFeedView(
defaultBookmarks = defaultBookmarks,
groupListFeedSource = listSource,
openDefaultBookmarks = openDefaultBookmarks,
onOpenItem = openBookmarkGroup,
onRenameItem = renameBookmarkGroup,
onItemDescriptionChange = changeBookmarkGroupDescription,
@@ -1,105 +0,0 @@
/**
* 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.list
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
@Composable
fun NewBookmarkGroupCreationDialog(
modifier: Modifier = Modifier,
onDismiss: () -> Unit,
onCreateGroup: (name: String, description: String?) -> Unit,
) {
val newGroupName = remember { mutableStateOf("") }
val newGroupDescription = remember { mutableStateOf<String?>(null) }
AlertDialog(
modifier = modifier,
onDismissRequest = onDismiss,
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "New Bookmark Group",
)
}
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
// For the new bookmark group name
TextField(
value = newGroupName.value,
onValueChange = { newGroupName.value = it },
label = {
Text(text = "Group name")
},
)
Spacer(modifier = DoubleVertSpacer)
// For the group description
TextField(
value =
(if (newGroupDescription.value != null) newGroupDescription.value else "").toString(),
onValueChange = { newGroupDescription.value = it },
label = {
Text(text = "Group description(optional)")
},
)
}
},
confirmButton = {
Button(
onClick = {
onCreateGroup(newGroupName.value, newGroupDescription.value)
onDismiss()
},
) {
Text("Create Group")
}
},
dismissButton = {
Button(
onClick = onDismiss,
) {
Text(stringRes(R.string.cancel))
}
},
)
}
@@ -44,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.BookmarkGroupsFeedEmpty
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -75,8 +74,6 @@ private fun ListManagementView(
accountViewModel: AccountViewModel,
nav: INav,
) {
val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow
.collectAsStateWithLifecycle()
Scaffold(
modifier = modifier,
topBar = {
@@ -95,48 +92,86 @@ private fun ListManagementView(
).consumeWindowInsets(contentPadding)
.imePadding(),
) {
if (bookmarkGroups.isEmpty()) {
BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg))
} else {
LazyColumn(
state = rememberLazyListState(),
modifier = Modifier.fillMaxWidth(),
) {
itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList ->
val maybePublicBookmark = bookmarkList.publicArticleBookmarks.firstOrNull { it.address == note.address }
val maybePrivateBookmark = bookmarkList.privateArticleBookmarks.firstOrNull { it.address == note.address }
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = bookmarkList.title,
isPublicMemberBookmark = maybePublicBookmark != null,
isPrivateMemberBookmark = maybePrivateBookmark != null,
totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size,
totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size,
onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.ArticleBookmark)) },
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.addBookmarkToList(
bookmark = AddressBookmark(address = note.address, relayHint = note.relayHintUrl()),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = shouldBePrivate,
account = accountViewModel.account,
)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList(
bookmark = AddressBookmark(address = note.address),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = maybePrivateBookmark != null,
account = accountViewModel.account,
)
}
},
)
}
}
}
ListManagementViewBody(note, accountViewModel, nav)
}
}
}
@Composable
private fun ListManagementViewBody(
note: AddressableNote,
accountViewModel: AccountViewModel,
nav: INav,
) {
val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow
.collectAsStateWithLifecycle()
val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks
.collectAsStateWithLifecycle()
LazyColumn(
state = rememberLazyListState(),
modifier = Modifier.fillMaxWidth(),
) {
item {
val maybePublicBookmark = defaultBookmarks.public.contains(note)
val maybePrivateBookmark = defaultBookmarks.private.contains(note)
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = stringRes(R.string.bookmarks_title),
isPublicMemberBookmark = maybePublicBookmark,
isPrivateMemberBookmark = maybePrivateBookmark,
totalPostBookmarkSize = defaultBookmarks.public.size + defaultBookmarks.private.size,
totalArticleBookmarkSize = 0,
onClick = {
nav.nav(Route.Bookmarks)
},
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.addBookmark(note, shouldBePrivate)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.removeBookmark(note)
}
},
)
}
itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList ->
val maybePublicBookmark = bookmarkList.publicArticleBookmarks.firstOrNull { it.address == note.address }
val maybePrivateBookmark = bookmarkList.privateArticleBookmarks.firstOrNull { it.address == note.address }
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = bookmarkList.title,
isPublicMemberBookmark = maybePublicBookmark != null,
isPrivateMemberBookmark = maybePrivateBookmark != null,
totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size,
totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size,
onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.ArticleBookmark)) },
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.addBookmarkToList(
bookmark = AddressBookmark(address = note.address, relayHint = note.relayHintUrl()),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = shouldBePrivate,
account = accountViewModel.account,
)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList(
bookmark = AddressBookmark(address = note.address),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = maybePrivateBookmark != null,
account = accountViewModel.account,
)
}
},
)
}
}
}
@@ -44,7 +44,8 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.BookmarkGroupsFeedEmpty
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ListManagementViewBody
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
@@ -74,8 +75,6 @@ private fun ListManagementView(
accountViewModel: AccountViewModel,
nav: INav,
) {
val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow
.collectAsStateWithLifecycle()
Scaffold(
modifier = modifier,
topBar = {
@@ -94,48 +93,86 @@ private fun ListManagementView(
).consumeWindowInsets(contentPadding)
.imePadding(),
) {
if (bookmarkGroups.isEmpty()) {
BookmarkGroupsFeedEmpty(message = stringRes(R.string.bookmark_list_feed_empty_msg))
} else {
LazyColumn(
state = rememberLazyListState(),
modifier = Modifier.fillMaxWidth(),
) {
itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList ->
val maybePublicBookmark = bookmarkList.publicPostBookmarks.firstOrNull { it.eventId == note.idHex }
val maybePrivateBookmark = bookmarkList.privatePostBookmarks.firstOrNull { it.eventId == note.idHex }
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = bookmarkList.title,
isPublicMemberBookmark = maybePublicBookmark != null,
isPrivateMemberBookmark = maybePrivateBookmark != null,
totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size,
totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size,
onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.PostBookmark)) },
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.addBookmarkToList(
bookmark = EventBookmark(eventId = note.idHex, relay = note.relayHintUrl(), author = note.author?.pubkeyHex),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = shouldBePrivate,
account = accountViewModel.account,
)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList(
bookmark = EventBookmark(eventId = note.idHex),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = maybePrivateBookmark != null,
account = accountViewModel.account,
)
}
},
)
}
}
}
ListManagementViewBody(note, accountViewModel, nav)
}
}
}
@Composable
private fun ListManagementViewBody(
note: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow
.collectAsStateWithLifecycle()
val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks
.collectAsStateWithLifecycle()
LazyColumn(
state = rememberLazyListState(),
modifier = Modifier.fillMaxWidth(),
) {
item {
val maybePublicBookmark = defaultBookmarks.public.contains(note)
val maybePrivateBookmark = defaultBookmarks.private.contains(note)
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = stringRes(R.string.bookmarks_title),
isPublicMemberBookmark = maybePublicBookmark,
isPrivateMemberBookmark = maybePrivateBookmark,
totalPostBookmarkSize = defaultBookmarks.public.size + defaultBookmarks.private.size,
totalArticleBookmarkSize = 0,
onClick = {
nav.nav(Route.Bookmarks)
},
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.addBookmark(note, shouldBePrivate)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.removeBookmark(note)
}
},
)
}
itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList ->
val maybePublicBookmark = bookmarkList.publicPostBookmarks.firstOrNull { it.eventId == note.idHex }
val maybePrivateBookmark = bookmarkList.privatePostBookmarks.firstOrNull { it.eventId == note.idHex }
BookmarkGroupManagementItem(
modifier = Modifier.fillMaxWidth().animateItem(),
listTitle = bookmarkList.title,
isPublicMemberBookmark = maybePublicBookmark != null,
isPrivateMemberBookmark = maybePrivateBookmark != null,
totalPostBookmarkSize = bookmarkList.publicPostBookmarks.size + bookmarkList.privatePostBookmarks.size,
totalArticleBookmarkSize = bookmarkList.publicArticleBookmarks.size + bookmarkList.privateArticleBookmarks.size,
onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.PostBookmark)) },
onAddBookmarkToGroup = { shouldBePrivate ->
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.addBookmarkToList(
bookmark = EventBookmark(eventId = note.idHex, relay = note.relayHintUrl(), author = note.author?.pubkeyHex),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = shouldBePrivate,
account = accountViewModel.account,
)
}
},
onRemoveBookmarkFromGroup = {
accountViewModel.launchSigner {
accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList(
bookmark = EventBookmark(eventId = note.idHex),
bookmarkListIdentifier = bookmarkList.identifier,
isBookmarkPrivate = maybePrivateBookmark != null,
account = accountViewModel.account,
)
}
},
)
}
}
}
@@ -36,5 +36,5 @@ fun BookmarkTabHeader(
) {
val userBookmarks by observeUserBookmarkCount(baseUser, accountViewModel)
Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}")
Text(text = "$userBookmarks ${stringRes(R.string.bookmarks_title)}")
}
+2
View File
@@ -391,6 +391,8 @@
<string name="manual_zaps">Manual Zap Splits</string>
<string name="bookmarks">Bookmarks</string>
<string name="bookmarks_title">Default Bookmarks</string>
<string name="bookmarks_explainer">Your default Bookmarks that many clients support</string>
<string name="drafts">Drafts</string>
<string name="private_bookmarks">Private Bookmarks</string>
<string name="public_bookmarks">Public Bookmarks</string>
@@ -148,6 +148,21 @@ class BookmarkListEvent(
)
}
suspend fun remove(
earlierVersion: BookmarkListEvent,
bookmarkIdTag: BookmarkIdTag,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): BookmarkListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
return resign(
privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()),
tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
signer = signer,
createdAt = createdAt,
)
}
suspend fun resign(
tags: TagArray,
privateTags: TagArray,