diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/BookmarkGroupManagementItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/BookmarkGroupManagementItem.kt new file mode 100644 index 000000000..5c07cd9ee --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/BookmarkGroupManagementItem.kt @@ -0,0 +1,214 @@ +/** + * 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.membershipManagement + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material.icons.filled.BookmarkRemove +import androidx.compose.material.icons.outlined.CollectionsBookmark +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material.icons.outlined.RemoveCircleOutline +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.style.TextOverflow +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.Size50Modifier +import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp + +@Composable +fun BookmarkGroupManagementItem( + modifier: Modifier = Modifier, + listTitle: String, + isPrivateMemberBookmark: Boolean, + isPublicMemberBookmark: Boolean, + onClick: () -> Unit, + onAddBookmarkToGroup: (shouldBookmarkBePrivate: Boolean) -> Unit, + onRemoveBookmarkFromGroup: () -> Unit, +) { + ListItem( + modifier = modifier.clickable(onClick = onClick), + headlineContent = { + Text( + text = listTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + supportingContent = { + BookmarkStatusInList(isPublicMemberBookmark, isPrivateMemberBookmark) + }, + leadingContent = { + Box( + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.CollectionsBookmark, + contentDescription = stringRes(R.string.bookmark_list_icon_label), + modifier = Size50Modifier, + ) + } + }, + trailingContent = { + val isBookmarkInGroup = isPrivateMemberBookmark || isPublicMemberBookmark + BookmarkManagementOptions( + isBookmarkInList = isBookmarkInGroup, + onAddBookmark = onAddBookmarkToGroup, + onRemoveBookmark = onRemoveBookmarkFromGroup, + ) + }, + ) +} + +@Composable +fun BookmarkStatusInList( + isPublicMemberBookmark: Boolean, + isPrivateMemberBookmark: Boolean, +) { + Row( + modifier = HalfHalfVertPadding, + horizontalArrangement = SpacedBy5dp, + verticalAlignment = Alignment.CenterVertically, + ) { + val text = + if (isPublicMemberBookmark) { + "is a public bookmark here" + } else if (isPrivateMemberBookmark) { + "is a private bookmark here" + } else { + "is not a bookmark here" + } + + val icon = + if (isPublicMemberBookmark) { + Icons.Outlined.Public + } else if (isPrivateMemberBookmark) { + Icons.Outlined.Lock + } else { + Icons.Outlined.RemoveCircleOutline + } + + Icon( + imageVector = icon, + contentDescription = text, + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = text, + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, + ) + } +} + +@Composable +fun BookmarkManagementOptions( + isBookmarkInList: Boolean, + onAddBookmark: (shouldBePrivate: Boolean) -> Unit, + onRemoveBookmark: () -> Unit, +) { + val isBookmarkAddTapped = remember { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconButton( + onClick = { + if (isBookmarkInList) { + onRemoveBookmark() + } else { + isBookmarkAddTapped.value = true + } + }, + modifier = + Modifier + .background( + color = + if (isBookmarkInList) { + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.primary + }, + shape = RoundedCornerShape(percent = 80), + ), + ) { + if (isBookmarkInList) { + Icon( + imageVector = Icons.Filled.BookmarkRemove, + contentDescription = "Remove bookmark from list", + tint = MaterialTheme.colorScheme.onErrorContainer, + ) + } else { + Icon( + imageVector = Icons.Filled.BookmarkAdd, + contentDescription = "Add bookmark to list", + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } + + DropdownMenu( + expanded = isBookmarkAddTapped.value, + onDismissRequest = { isBookmarkAddTapped.value = false }, + ) { + DropdownMenuItem( + text = { + Text(text = "Add as public bookmark") + }, + onClick = { + onAddBookmark(false) + isBookmarkAddTapped.value = false + }, + ) + DropdownMenuItem( + text = { + Text(text = "Add as private bookmark") + }, + onClick = { + onAddBookmark(true) + isBookmarkAddTapped.value = false + }, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt new file mode 100644 index 000000000..e7c9c6da5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -0,0 +1,139 @@ +/** + * 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.membershipManagement + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.recalculateWindowInsets +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +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.lists.list.NewListButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark + +@Composable +fun PostBookmarkListManagementScreen( + postId: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadNote(baseNoteHex = postId, accountViewModel = accountViewModel) { + it?.let { + ListManagementView( + modifier = Modifier.fillMaxSize().recalculateWindowInsets(), + note = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@Composable +private fun ListManagementView( + modifier: Modifier = Modifier, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow + .collectAsStateWithLifecycle() + Scaffold( + modifier = modifier, + topBar = { + TopBarWithBackButton(caption = "Bookmark this post", nav::popBack) + }, + floatingActionButton = { + NewListButton { } + }, + ) { contentPadding -> + Column( + modifier = + Modifier + .padding( + top = contentPadding.calculateTopPadding(), + bottom = contentPadding.calculateBottomPadding(), + ).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, + 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, + ) + } + }, + ) + } + } + } + } + } +}