Introduce BookmarkGroupScreen, with it's associated ViewModel, and the different category-based views.
This commit is contained in:
@@ -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.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.bookmarkgroups.list.ListOfBookmarkGroupsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
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.ChatroomByAuthorScreen
|
||||||
@@ -139,6 +140,7 @@ fun AppNavigation(
|
|||||||
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
|
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.BookmarkGroups> { ListOfBookmarkGroupsScreen(accountViewModel, nav) }
|
||||||
|
composableFromEndArgs<Route.BookmarkGroupView> { BookmarkGroupScreen(it.dTag, it.bookmarkType, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||||
|
|||||||
+98
@@ -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<AddressableNote>,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+291
@@ -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()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -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 <T : ViewModel> create(modelClass: Class<T>): T = BookmarkGroupViewModel(account, bookmarkGroupIdentifier) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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<BookmarkIdTag>,
|
||||||
|
isPrivate: Boolean,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text("To be implemented")
|
||||||
|
// Text("Number of ${if (isPrivate) "private" else "public"} hashtags: ${list.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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<BookmarkIdTag>,
|
||||||
|
isPrivate: Boolean,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text("To be implemented")
|
||||||
|
// Text("Number of ${if (isPrivate) "private" else "public"} links: ${list.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -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<Note>,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user