From c48436b0a6b334a7f7ded3a600e28dff958e788e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 17 Nov 2025 18:51:39 +0100 Subject: [PATCH] Introduce BookmarkGroupScreen, with it's associated ViewModel, and the different category-based views. --- .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../bookmarkgroups/display/ArticleListView.kt | 98 ++++++ .../display/BookmarkGroupScreen.kt | 291 ++++++++++++++++++ .../display/BookmarkGroupViewModel.kt | 126 ++++++++ .../bookmarkgroups/display/HashtagListView.kt | 59 ++++ .../bookmarkgroups/display/LinksListView.kt | 59 ++++ .../bookmarkgroups/display/PostListView.kt | 98 ++++++ 7 files changed, 733 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/HashtagListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/LinksListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt 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 f12291edb..d0d134dc1 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 @@ -57,6 +57,7 @@ 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.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen @@ -139,6 +140,7 @@ fun AppNavigation( composableFromBottomArgs { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) } composableFromEnd { ListOfBookmarkGroupsScreen(accountViewModel, nav) } + composableFromEndArgs { BookmarkGroupScreen(it.dTag, it.bookmarkType, accountViewModel, nav) } composableFromBottomArgs { NewUserMetadataScreen(nav, accountViewModel) } composable { SearchScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt new file mode 100644 index 000000000..164efe81f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt @@ -0,0 +1,98 @@ +/** + * 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.display + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.FeedPadding + +@Composable +fun RenderArticleList( + bookmarkGroupViewModel: BookmarkGroupViewModel, + pagerState: PagerState, + accountViewModel: AccountViewModel, + nav: INav, + modifier: Modifier = Modifier, +) { + val privateArticles = bookmarkGroupViewModel.privateArticles().collectAsStateWithLifecycle() + val publicArticles = bookmarkGroupViewModel.publicArticles().collectAsStateWithLifecycle() + + HorizontalPager(pagerState, modifier) { page -> + when (page) { + 0 -> + ArticleList( + modifier = Modifier.fillMaxSize(), + articles = publicArticles.value, + isPrivate = false, + accountViewModel = accountViewModel, + nav = nav, + ) + 1 -> + ArticleList( + modifier = Modifier.fillMaxSize(), + articles = privateArticles.value, + isPrivate = true, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@Composable +fun ArticleList( + modifier: Modifier = Modifier, + articles: List, + isPrivate: Boolean, + accountViewModel: AccountViewModel, + nav: INav, +) { + val listState = rememberLazyListState() + + LazyColumn( + modifier = modifier, + contentPadding = FeedPadding, + state = listState, + ) { + itemsIndexed(articles, key = { _, item -> item.toNAddr() }) { _, item -> + // TODO: Find a way to integrate bookmark group callbacks into the note below + NoteCompose( + baseNote = item, + modifier = Modifier.animateContentSize(), + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt new file mode 100644 index 000000000..3cf1390fa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt @@ -0,0 +1,291 @@ +/** + * 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.display + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.ui.components.ClickableBox +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.TabRowHeight +import kotlinx.coroutines.launch + +@Composable +fun BookmarkGroupScreen( + bookmarkIdentifier: String, + bookmarkType: BookmarkType, + accountViewModel: AccountViewModel, + nav: INav, +) { + val bookmarkGroupViewModel: BookmarkGroupViewModel = + viewModel( + factory = BookmarkGroupViewModel.Initializer(accountViewModel.account, bookmarkIdentifier), + ) + + BookmarkGroupScreenView( + bookmarkGroupViewModel, + bookmarkType, + broadcastBookmarkGroup = { + accountViewModel.launchSigner { + val groupNote = accountViewModel.account.labeledBookmarkLists.getLabeledBookmarkListNote(bookmarkIdentifier) + groupNote?.let { + accountViewModel.broadcast(it) + } + } + }, + deleteBookmarkGroup = { + accountViewModel.launchSigner { + bookmarkGroupViewModel.deleteBookmarkGroup() + } + }, + accountViewModel, + nav, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BookmarkGroupScreenView( + bookmarkGroupViewModel: BookmarkGroupViewModel, + bookmarkType: BookmarkType, + broadcastBookmarkGroup: () -> Unit, + deleteBookmarkGroup: () -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val pagerState = rememberPagerState { 2 } + val privateItemTypeLabel = + when (bookmarkType) { + BookmarkType.ArticleBookmark -> "Private Articles" + BookmarkType.HashtagBookmark -> "Private Hashtags" + BookmarkType.LinkBookmark -> "Private Links" + BookmarkType.PostBookmark -> "Private Posts" + } + + val publicItemTypeLabel = + when (bookmarkType) { + BookmarkType.ArticleBookmark -> "Public Articles" + BookmarkType.HashtagBookmark -> "Public Hashtags" + BookmarkType.LinkBookmark -> "Public Links" + BookmarkType.PostBookmark -> "Public Posts" + } + Scaffold( + topBar = { + Column { + TopAppBar( + title = { + TitleAndDescription(bookmarkGroupViewModel) + }, + navigationIcon = { + IconButton(nav::popBack) { + ArrowBackIcon() + } + }, + actions = { + BookmarkGroupActionsMenuButton( + onBroadcastList = broadcastBookmarkGroup, + onDeleteList = deleteBookmarkGroup, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + TabRow( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + selectedTabIndex = pagerState.currentPage, + modifier = TabRowHeight, + ) { + val scope = rememberCoroutineScope() + Tab( + selected = pagerState.currentPage == 0, + onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(text = publicItemTypeLabel) }, + ) + Tab( + selected = pagerState.currentPage == 1, + onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(text = privateItemTypeLabel) }, + ) + } + } + }, + ) { padding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding( + top = padding.calculateTopPadding(), + bottom = padding.calculateBottomPadding(), + ).consumeWindowInsets(padding) + .imePadding(), + ) { + when (bookmarkType) { + BookmarkType.PostBookmark -> + RenderPostList( + bookmarkGroupViewModel, + pagerState, + accountViewModel, + nav, + ) + BookmarkType.ArticleBookmark -> + RenderArticleList( + bookmarkGroupViewModel, + pagerState, + accountViewModel, + nav, + ) + BookmarkType.HashtagBookmark -> RenderHashtagList(bookmarkGroupViewModel, pagerState) + BookmarkType.LinkBookmark -> RenderLinksList(bookmarkGroupViewModel, pagerState) + } + } + } +} + +@Composable +private fun TitleAndDescription(viewModel: BookmarkGroupViewModel) { + val selectedSetState = viewModel.selectedBookmarkGroupFlow.collectAsStateWithLifecycle() + selectedSetState.value?.let { bookmarkGroup -> + ListItem( + headlineContent = { + Text( + text = bookmarkGroup.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + supportingContent = { + bookmarkGroup.description?.let { description -> + Text( + text = description, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) + } +} + +@Composable +fun BookmarkGroupActionsMenuButton( + onBroadcastList: () -> Unit, + onDeleteList: () -> Unit, +) { + val isActionListOpen = remember { mutableStateOf(false) } + + ClickableBox( + modifier = + StdPadding + .size(30.dp) + .border( + width = Dp.Hairline, + color = ButtonDefaults.filledTonalButtonColors().containerColor, + shape = ButtonBorder, + ).background( + color = ButtonDefaults.filledTonalButtonColors().containerColor, + shape = ButtonBorder, + ), + onClick = { isActionListOpen.value = true }, + ) { + VerticalDotsIcon() + BookmarkGroupActionsMenu( + onCloseMenu = { isActionListOpen.value = false }, + isOpen = isActionListOpen.value, + onBroadcastList = onBroadcastList, + onDeleteList = onDeleteList, + ) + } +} + +@Composable +fun BookmarkGroupActionsMenu( + onCloseMenu: () -> Unit, + isOpen: Boolean, + onBroadcastList: () -> Unit, + onDeleteList: () -> Unit, +) { + DropdownMenu( + expanded = isOpen, + onDismissRequest = onCloseMenu, + ) { + DropdownMenuItem( + text = { + Text("Broadcast Bookmark Group") + }, + onClick = { + onBroadcastList() + onCloseMenu() + }, + ) + HorizontalDivider(thickness = DividerThickness) + DropdownMenuItem( + text = { + Text("Delete Bookmark Group") + }, + onClick = { + onDeleteList() + onCloseMenu() + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt new file mode 100644 index 000000000..e057dff93 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt @@ -0,0 +1,126 @@ +/** + * 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.display + +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@Stable +class BookmarkGroupViewModel( + val account: Account, + bookmarkGroupIdentifier: String, +) : ViewModel() { + val selectedBookmarkGroupFlow = + account.labeledBookmarkLists + .getLabeledBookmarkListFlow(bookmarkGroupIdentifier) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(2500), null) + + fun publicPosts() = + selectedBookmarkGroupFlow + .filterNotNull() + .map { group -> + group.publicBookmarks + .filter { it is EventBookmark } + .map { account.cache.getOrCreateNote((it as EventBookmark).eventId) } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + fun privatePosts() = + selectedBookmarkGroupFlow + .filterNotNull() + .map { group -> + group.privateBookmarks + .filter { it is EventBookmark } + .map { account.cache.getOrCreateNote((it as EventBookmark).eventId) } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + fun publicArticles() = + selectedBookmarkGroupFlow + .filterNotNull() + .map { group -> + group.publicBookmarks + .filter { it is AddressBookmark } + .map { account.cache.getOrCreateAddressableNote((it as AddressBookmark).address) } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + fun privateArticles() = + selectedBookmarkGroupFlow + .filterNotNull() + .map { group -> + group.privateBookmarks + .filter { it is AddressBookmark } + .map { account.cache.getOrCreateAddressableNote((it as AddressBookmark).address) } + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + // TODO: Add implementations for Hashtag and Link bookmarks + + suspend fun deleteBookmarkGroup() { + selectedBookmarkGroupFlow.value?.let { + account.labeledBookmarkLists.deleteBookmarkList(it, account) + } + } + + suspend fun addBookmarkToGroup( + bookmark: BookmarkIdTag, + isPrivate: Boolean, + account: Account, + ) { + selectedBookmarkGroupFlow.value?.let { + account.labeledBookmarkLists.addBookmarkToList( + bookmark, + it, + isPrivate, + account, + ) + } + } + + suspend fun removeBookmarkFromGroup( + bookmark: BookmarkIdTag, + isPrivate: Boolean, + account: Account, + ) { + selectedBookmarkGroupFlow.value?.let { + account.labeledBookmarkLists.removeBookmarkFromList( + bookmark, + it, + isPrivate, + account, + ) + } + } + + class Initializer( + val account: Account, + val bookmarkGroupIdentifier: String, + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = BookmarkGroupViewModel(account, bookmarkGroupIdentifier) as T + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/HashtagListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/HashtagListView.kt new file mode 100644 index 000000000..7f3f922ff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/HashtagListView.kt @@ -0,0 +1,59 @@ +/** + * 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.display + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun RenderHashtagList( + bookmarkGroupViewModel: BookmarkGroupViewModel, + pagerState: PagerState, + modifier: Modifier = Modifier, +) { + HorizontalPager(pagerState, modifier) { page -> + when (page) { + 0 -> HashtagsList(isPrivate = true) + 1 -> HashtagsList(isPrivate = false) + } + } +} + +@Composable +private fun HashtagsList( + modifier: Modifier = Modifier, + // list: List, + isPrivate: Boolean, +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("To be implemented") + // Text("Number of ${if (isPrivate) "private" else "public"} hashtags: ${list.size}") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/LinksListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/LinksListView.kt new file mode 100644 index 000000000..13ae5e64e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/LinksListView.kt @@ -0,0 +1,59 @@ +/** + * 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.display + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +// TODO: Come back to work on this +@Composable +fun RenderLinksList( + bookmarkGroupViewModel: BookmarkGroupViewModel, + pagerState: PagerState, + modifier: Modifier = Modifier, +) { + HorizontalPager(pagerState, modifier) { page -> + when (page) { + 0 -> LinksList(isPrivate = true) + 1 -> LinksList(isPrivate = false) + } + } +} + +@Composable +private fun LinksList( + // list: List, + isPrivate: Boolean, +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("To be implemented") + // Text("Number of ${if (isPrivate) "private" else "public"} links: ${list.size}") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt new file mode 100644 index 000000000..fd7b11fb3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/PostListView.kt @@ -0,0 +1,98 @@ +/** + * 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.display + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.FeedPadding + +@Composable +fun RenderPostList( + bookmarkGroupViewModel: BookmarkGroupViewModel, + pagerState: PagerState, + accountViewModel: AccountViewModel, + nav: INav, + modifier: Modifier = Modifier, +) { + val privatePosts = bookmarkGroupViewModel.privatePosts().collectAsStateWithLifecycle() + val publicPosts = bookmarkGroupViewModel.publicPosts().collectAsStateWithLifecycle() + + HorizontalPager(pagerState, modifier) { page -> + when (page) { + 0 -> + PostList( + modifier = Modifier.fillMaxSize(), + posts = publicPosts.value, + isPrivate = false, + accountViewModel = accountViewModel, + nav = nav, + ) + 1 -> + PostList( + modifier = Modifier.fillMaxSize(), + posts = privatePosts.value, + isPrivate = true, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} + +@Composable +private fun PostList( + modifier: Modifier = Modifier, + posts: List, + isPrivate: Boolean, + accountViewModel: AccountViewModel, + nav: INav, +) { + val listState = rememberLazyListState() + + LazyColumn( + modifier = modifier, + contentPadding = FeedPadding, + state = listState, + ) { + itemsIndexed(posts, key = { _, item -> item.idHex }) { _, item -> + // TODO: Find a way to integrate bookmark group callbacks into the note below + NoteCompose( + baseNote = item, + modifier = Modifier.animateContentSize(), + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +}