mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
Merge pull request #2448 from vitorpamplona/claude/unpin-deleted-posts-gTD4S
Add deleted items banner to bookmarks, pins, and bookmark groups
This commit is contained in:
@@ -134,6 +134,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupStateStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -2195,6 +2196,24 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeDeletedBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
val event = bookmarkState.removeDeletedBookmarks(deletedEventIds, deletedAddresses) ?: return
|
||||
sendMyPublicAndPrivateOutbox(event)
|
||||
}
|
||||
|
||||
suspend fun removeDeletedOldBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
val event = oldBookmarkState.removeDeletedBookmarks(deletedEventIds, deletedAddresses) ?: return
|
||||
sendMyPublicAndPrivateOutbox(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bookmark event without sending it.
|
||||
* Returns the event and target relays for tracked broadcasting.
|
||||
@@ -2293,6 +2312,13 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeDeletedPins(deletedNotes: Set<Note>) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val event = pinState.removeDeletedPins(deletedNotes) ?: return
|
||||
sendMyPublicAndPrivateOutbox(event)
|
||||
}
|
||||
|
||||
suspend fun createAddPinEvent(note: Note): Pair<Event, Set<NormalizedRelayUrl>>? {
|
||||
if (!isWriteable() || note.isDraft()) return null
|
||||
|
||||
|
||||
@@ -127,4 +127,21 @@ class PinListState(
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun removeDeletedPins(deletedNotes: Set<Note>): PinListEvent? {
|
||||
val currentList = getPinList() ?: return null
|
||||
if (deletedNotes.isEmpty()) return null
|
||||
|
||||
val deletedIds = deletedNotes.mapTo(HashSet()) { it.idHex }
|
||||
val newTags =
|
||||
currentList.tags
|
||||
.filter { tag ->
|
||||
val bookmark = EventBookmark.parse(tag)
|
||||
bookmark == null || bookmark.eventId !in deletedIds
|
||||
}.toTypedArray()
|
||||
|
||||
if (newTags.size == currentList.tags.size) return null
|
||||
|
||||
return PinListEvent.resign(tags = newTags, signer = signer)
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -29,10 +29,13 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.filter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.update
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
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 com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description
|
||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image
|
||||
@@ -298,4 +301,55 @@ class LabeledBookmarkListsState(
|
||||
)
|
||||
account.sendMyPublicAndPrivateOutbox(updatedList)
|
||||
}
|
||||
|
||||
suspend fun removeDeletedBookmarksFromList(
|
||||
bookmarkListIdentifier: String,
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
account: Account,
|
||||
) {
|
||||
if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return
|
||||
|
||||
val currentList = getLabeledBookmarkListNote(bookmarkListIdentifier)?.event as? LabeledBookmarkListEvent ?: return
|
||||
|
||||
val newPublicTags =
|
||||
currentList.tags
|
||||
.filter { tag ->
|
||||
when (val bookmark = BookmarkIdTag.parse(tag)) {
|
||||
is EventBookmark -> bookmark.eventId !in deletedEventIds
|
||||
is AddressBookmark -> bookmark.address !in deletedAddresses
|
||||
null -> true
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
val oldPrivateTags = currentList.privateTags(account.signer)
|
||||
|
||||
val updatedList =
|
||||
if (oldPrivateTags == null) {
|
||||
if (newPublicTags.size == currentList.tags.size) return
|
||||
LabeledBookmarkListEvent.resign(
|
||||
content = currentList.content,
|
||||
tags = newPublicTags,
|
||||
signer = account.signer,
|
||||
)
|
||||
} else {
|
||||
val newPrivateTags =
|
||||
oldPrivateTags
|
||||
.filter { tag ->
|
||||
when (val bookmark = BookmarkIdTag.parse(tag)) {
|
||||
is EventBookmark -> bookmark.eventId !in deletedEventIds
|
||||
is AddressBookmark -> bookmark.address !in deletedAddresses
|
||||
null -> true
|
||||
}
|
||||
}.toTypedArray()
|
||||
if (newPublicTags.size == currentList.tags.size && newPrivateTags.size == oldPrivateTags.size) return
|
||||
LabeledBookmarkListEvent.resign(
|
||||
tags = newPublicTags,
|
||||
privateTags = newPrivateTags,
|
||||
signer = account.signer,
|
||||
)
|
||||
}
|
||||
|
||||
account.sendMyPublicAndPrivateOutbox(updatedList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
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.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BigPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
|
||||
@Composable
|
||||
fun DeletedItemsBanner(
|
||||
count: Int,
|
||||
onRemove: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
if (count <= 0) return
|
||||
|
||||
Column(modifier = StdPadding) {
|
||||
Card(
|
||||
modifier = MaterialTheme.colorScheme.imageModifier,
|
||||
) {
|
||||
Column(modifier = BigPadding) {
|
||||
Text(
|
||||
text = stringRes(R.string.deleted_items_banner_title, count),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringRes(R.string.deleted_items_banner_dismiss))
|
||||
}
|
||||
Button(onClick = onRemove) {
|
||||
Text(text = stringRes(R.string.deleted_items_banner_remove))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -919,6 +919,10 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDeletedPins(deletedNotes: Set<Note>) {
|
||||
launchSigner { account.removeDeletedPins(deletedNotes) }
|
||||
}
|
||||
|
||||
fun addPrivateBookmark(note: Note) {
|
||||
if (settings.isCompleteUIMode()) {
|
||||
launchSigner {
|
||||
@@ -988,6 +992,20 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDeletedBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
) {
|
||||
launchSigner { account.removeDeletedBookmarks(deletedEventIds, deletedAddresses) }
|
||||
}
|
||||
|
||||
fun removeDeletedOldBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
) {
|
||||
launchSigner { account.removeDeletedOldBookmarks(deletedEventIds, deletedAddresses) }
|
||||
}
|
||||
|
||||
fun broadcast(note: Note) = launchSigner { account.broadcast(note) }
|
||||
|
||||
fun timestamp(note: Note) = launchSigner { account.otsState.timestamp(note) }
|
||||
|
||||
+42
-1
@@ -34,14 +34,18 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
|
||||
import com.vitorpamplona.amethyst.ui.components.DeletedItemsBanner
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
@@ -81,7 +85,7 @@ fun BookmarkListScreen(
|
||||
// Preload all bookmarked events so they don't load one-by-one when scrolling
|
||||
PreloadBookmarkEvents(bookmarkState, accountViewModel)
|
||||
|
||||
RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, accountViewModel, nav)
|
||||
RenderBookmarkScreen(publicFeedViewModel, privateFeedViewModel, bookmarkState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -89,12 +93,36 @@ fun BookmarkListScreen(
|
||||
private fun RenderBookmarkScreen(
|
||||
publicFeedViewModel: BookmarkPublicFeedViewModel,
|
||||
privateFeedViewModel: BookmarkPrivateFeedViewModel,
|
||||
bookmarkState: com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState.BookmarkList?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val cache = accountViewModel.account.cache
|
||||
val deletedEventIds = remember(bookmarkState) { mutableSetOf<String>() }
|
||||
val deletedAddresses = remember(bookmarkState) { mutableSetOf<com.vitorpamplona.quartz.nip01Core.core.Address>() }
|
||||
val deletedCount =
|
||||
remember(bookmarkState) {
|
||||
deletedEventIds.clear()
|
||||
deletedAddresses.clear()
|
||||
val all = bookmarkState?.public.orEmpty() + bookmarkState?.private.orEmpty()
|
||||
all.forEach { note ->
|
||||
val event = note.event
|
||||
if (event != null && cache.hasBeenDeleted(event)) {
|
||||
deletedEventIds.add(note.idHex)
|
||||
if (note is AddressableNote) deletedAddresses.add(note.address)
|
||||
}
|
||||
}
|
||||
deletedEventIds.size + deletedAddresses.size
|
||||
}
|
||||
|
||||
var bannerDismissed by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(deletedCount) {
|
||||
if (deletedCount == 0) bannerDismissed = false
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -122,6 +150,19 @@ private fun RenderBookmarkScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
if (!bannerDismissed) {
|
||||
DeletedItemsBanner(
|
||||
count = deletedCount,
|
||||
onRemove = {
|
||||
accountViewModel.removeDeletedBookmarks(
|
||||
deletedEventIds.toSet(),
|
||||
deletedAddresses.toSet(),
|
||||
)
|
||||
bannerDismissed = true
|
||||
},
|
||||
onDismiss = { bannerDismissed = true },
|
||||
)
|
||||
}
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
|
||||
+71
@@ -44,10 +44,12 @@ import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -56,7 +58,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.DeletedItemsBanner
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
@@ -70,6 +74,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -158,6 +163,11 @@ fun BookmarkGroupScreenView(
|
||||
).consumeWindowInsets(padding)
|
||||
.imePadding(),
|
||||
) {
|
||||
DeletedBookmarksBanner(
|
||||
bookmarkGroupViewModel = bookmarkGroupViewModel,
|
||||
bookmarkType = bookmarkType,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
when (bookmarkType) {
|
||||
BookmarkType.PostBookmark -> {
|
||||
RenderPostList(
|
||||
@@ -217,6 +227,67 @@ fun BookmarkGroupScreenView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeletedBookmarksBanner(
|
||||
bookmarkGroupViewModel: BookmarkGroupViewModel,
|
||||
bookmarkType: BookmarkType,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val postsFlow = remember(bookmarkGroupViewModel) { bookmarkGroupViewModel.publicPosts() }
|
||||
val privatePostsFlow = remember(bookmarkGroupViewModel) { bookmarkGroupViewModel.privatePosts() }
|
||||
val articlesFlow = remember(bookmarkGroupViewModel) { bookmarkGroupViewModel.publicArticles() }
|
||||
val privateArticlesFlow = remember(bookmarkGroupViewModel) { bookmarkGroupViewModel.privateArticles() }
|
||||
|
||||
val publicPosts by postsFlow.collectAsStateWithLifecycle()
|
||||
val privatePosts by privatePostsFlow.collectAsStateWithLifecycle()
|
||||
val publicArticles by articlesFlow.collectAsStateWithLifecycle()
|
||||
val privateArticles by privateArticlesFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val cache = accountViewModel.account.cache
|
||||
|
||||
val deletedEventIds = remember(publicPosts, privatePosts, publicArticles, privateArticles, bookmarkType) { mutableSetOf<String>() }
|
||||
val deletedAddresses = remember(publicPosts, privatePosts, publicArticles, privateArticles, bookmarkType) { mutableSetOf<Address>() }
|
||||
val deletedCount =
|
||||
remember(publicPosts, privatePosts, publicArticles, privateArticles, bookmarkType) {
|
||||
deletedEventIds.clear()
|
||||
deletedAddresses.clear()
|
||||
val scope =
|
||||
when (bookmarkType) {
|
||||
BookmarkType.PostBookmark -> publicPosts + privatePosts
|
||||
BookmarkType.ArticleBookmark -> publicArticles + privateArticles
|
||||
}
|
||||
scope.forEach { note ->
|
||||
val event = note.event
|
||||
if (event != null && cache.hasBeenDeleted(event)) {
|
||||
deletedEventIds.add(note.idHex)
|
||||
if (note is AddressableNote) deletedAddresses.add(note.address)
|
||||
}
|
||||
}
|
||||
deletedEventIds.size + deletedAddresses.size
|
||||
}
|
||||
|
||||
var bannerDismissed by remember(bookmarkType) { mutableStateOf(false) }
|
||||
LaunchedEffect(deletedCount) {
|
||||
if (deletedCount == 0) bannerDismissed = false
|
||||
}
|
||||
|
||||
if (!bannerDismissed) {
|
||||
DeletedItemsBanner(
|
||||
count = deletedCount,
|
||||
onRemove = {
|
||||
accountViewModel.launchSigner {
|
||||
bookmarkGroupViewModel.removeDeletedBookmarksFromGroup(
|
||||
deletedEventIds = deletedEventIds.toSet(),
|
||||
deletedAddresses = deletedAddresses.toSet(),
|
||||
)
|
||||
}
|
||||
bannerDismissed = true
|
||||
},
|
||||
onDismiss = { bannerDismissed = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleAndDescription(viewModel: BookmarkGroupViewModel) {
|
||||
val selectedSetState = viewModel.selectedBookmarkGroupFlow.collectAsStateWithLifecycle()
|
||||
|
||||
+13
@@ -155,6 +155,19 @@ class BookmarkGroupViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun removeDeletedBookmarksFromGroup(
|
||||
groupIdentifier: String = bookmarkGroupIdentifier,
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
) {
|
||||
account.labeledBookmarkLists.removeDeletedBookmarksFromList(
|
||||
groupIdentifier,
|
||||
deletedEventIds,
|
||||
deletedAddresses,
|
||||
account,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Initializer(
|
||||
val account: Account,
|
||||
|
||||
+42
-1
@@ -40,15 +40,19 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
|
||||
import com.vitorpamplona.amethyst.ui.components.DeletedItemsBanner
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
@@ -88,7 +92,7 @@ fun OldBookmarkListScreen(
|
||||
// Preload all bookmarked events so they don't load one-by-one when scrolling
|
||||
PreloadOldBookmarkEvents(bookmarkState, accountViewModel)
|
||||
|
||||
RenderOldBookmarkScreen(publicFeedViewModel, privateFeedViewModel, accountViewModel, nav)
|
||||
RenderOldBookmarkScreen(publicFeedViewModel, privateFeedViewModel, bookmarkState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@SuppressLint("LocalContextGetResourceValueCall")
|
||||
@@ -97,6 +101,7 @@ fun OldBookmarkListScreen(
|
||||
private fun RenderOldBookmarkScreen(
|
||||
publicFeedViewModel: OldBookmarkPublicFeedViewModel,
|
||||
privateFeedViewModel: OldBookmarkPrivateFeedViewModel,
|
||||
bookmarkState: com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState.BookmarkList?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -104,6 +109,29 @@ private fun RenderOldBookmarkScreen(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val cache = accountViewModel.account.cache
|
||||
val deletedEventIds = remember(bookmarkState) { mutableSetOf<String>() }
|
||||
val deletedAddresses = remember(bookmarkState) { mutableSetOf<com.vitorpamplona.quartz.nip01Core.core.Address>() }
|
||||
val deletedCount =
|
||||
remember(bookmarkState) {
|
||||
deletedEventIds.clear()
|
||||
deletedAddresses.clear()
|
||||
val all = bookmarkState?.public.orEmpty() + bookmarkState?.private.orEmpty()
|
||||
all.forEach { note ->
|
||||
val event = note.event
|
||||
if (event != null && cache.hasBeenDeleted(event)) {
|
||||
deletedEventIds.add(note.idHex)
|
||||
if (note is AddressableNote) deletedAddresses.add(note.address)
|
||||
}
|
||||
}
|
||||
deletedEventIds.size + deletedAddresses.size
|
||||
}
|
||||
|
||||
var bannerDismissed by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(deletedCount) {
|
||||
if (deletedCount == 0) bannerDismissed = false
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -156,6 +184,19 @@ private fun RenderOldBookmarkScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
if (!bannerDismissed) {
|
||||
DeletedItemsBanner(
|
||||
count = deletedCount,
|
||||
onRemove = {
|
||||
accountViewModel.removeDeletedOldBookmarks(
|
||||
deletedEventIds.toSet(),
|
||||
deletedAddresses.toSet(),
|
||||
)
|
||||
bannerDismissed = true
|
||||
},
|
||||
onDismiss = { bannerDismissed = true },
|
||||
)
|
||||
}
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
|
||||
+28
-1
@@ -27,13 +27,16 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
|
||||
import com.vitorpamplona.amethyst.ui.components.DeletedItemsBanner
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
@@ -63,15 +66,29 @@ fun PinnedNotesScreen(
|
||||
// Preload all pinned events so they don't load one-by-one when scrolling
|
||||
PreloadPinnedEvents(pinState, accountViewModel)
|
||||
|
||||
RenderPinnedNotesScreen(pinnedNotesFeedViewModel, accountViewModel, nav)
|
||||
RenderPinnedNotesScreen(pinnedNotesFeedViewModel, pinState, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderPinnedNotesScreen(
|
||||
pinnedNotesFeedViewModel: PinnedNotesFeedViewModel,
|
||||
pinState: List<Note>?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
var bannerDismissed by remember { mutableStateOf(false) }
|
||||
val deletedPins =
|
||||
remember(pinState) {
|
||||
pinState
|
||||
?.filter { note ->
|
||||
note.event?.let(accountViewModel.account.cache::hasBeenDeleted) == true
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
LaunchedEffect(deletedPins) {
|
||||
if (deletedPins.isEmpty()) bannerDismissed = false
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -80,6 +97,16 @@ private fun RenderPinnedNotesScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
if (!bannerDismissed) {
|
||||
DeletedItemsBanner(
|
||||
count = deletedPins.size,
|
||||
onRemove = {
|
||||
accountViewModel.removeDeletedPins(deletedPins.toSet())
|
||||
bannerDismissed = true
|
||||
},
|
||||
onDismiss = { bannerDismissed = true },
|
||||
)
|
||||
}
|
||||
RefresheableFeedView(
|
||||
pinnedNotesFeedViewModel,
|
||||
null,
|
||||
|
||||
@@ -435,6 +435,10 @@
|
||||
<string name="pin_to_profile">Pin to Profile</string>
|
||||
<string name="unpin_from_profile">Unpin from Profile</string>
|
||||
|
||||
<string name="deleted_items_banner_title">%1$d item(s) in this list have been deleted by their author.</string>
|
||||
<string name="deleted_items_banner_remove">Remove from list</string>
|
||||
<string name="deleted_items_banner_dismiss">Dismiss</string>
|
||||
|
||||
<string name="bookmark_lists">Bookmark Lists</string>
|
||||
<string name="bookmark_list_icon_label">Icon for bookmark list</string>
|
||||
<string name="bookmark_list_creation_screen_title">New Bookmark List</string>
|
||||
|
||||
+44
@@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
@@ -298,4 +300,46 @@ class BookmarkListState(
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeDeletedBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
): BookmarkListEvent? {
|
||||
val currentList = getBookmarkList() ?: return null
|
||||
if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return null
|
||||
|
||||
val newPublicTags = filterOutDeletedBookmarks(currentList.tags, deletedEventIds, deletedAddresses)
|
||||
val oldPrivateTags = currentList.privateTags(signer)
|
||||
|
||||
return if (oldPrivateTags == null) {
|
||||
if (newPublicTags.size == currentList.tags.size) return null
|
||||
BookmarkListEvent.resign(
|
||||
content = currentList.content,
|
||||
tags = newPublicTags,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
val newPrivateTags = filterOutDeletedBookmarks(oldPrivateTags, deletedEventIds, deletedAddresses)
|
||||
if (newPublicTags.size == currentList.tags.size && newPrivateTags.size == oldPrivateTags.size) return null
|
||||
BookmarkListEvent.resign(
|
||||
tags = newPublicTags,
|
||||
privateTags = newPrivateTags,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun filterOutDeletedBookmarks(
|
||||
tags: TagArray,
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
): TagArray =
|
||||
tags
|
||||
.filter { tag ->
|
||||
when (val bookmark = BookmarkIdTag.parse(tag)) {
|
||||
is EventBookmark -> bookmark.eventId !in deletedEventIds
|
||||
is AddressBookmark -> bookmark.address !in deletedAddresses
|
||||
null -> true
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
+29
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
@@ -210,4 +211,32 @@ class OldBookmarkListState(
|
||||
} else {
|
||||
publicBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
|
||||
suspend fun removeDeletedBookmarks(
|
||||
deletedEventIds: Set<String>,
|
||||
deletedAddresses: Set<Address>,
|
||||
): OldBookmarkListEvent? {
|
||||
val currentList = getBookmarkList() ?: return null
|
||||
if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return null
|
||||
|
||||
val newPublicTags = filterOutDeletedBookmarks(currentList.tags, deletedEventIds, deletedAddresses)
|
||||
val oldPrivateTags = currentList.privateTags(signer)
|
||||
|
||||
return if (oldPrivateTags == null) {
|
||||
if (newPublicTags.size == currentList.tags.size) return null
|
||||
OldBookmarkListEvent.resign(
|
||||
content = currentList.content,
|
||||
tags = newPublicTags,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
val newPrivateTags = filterOutDeletedBookmarks(oldPrivateTags, deletedEventIds, deletedAddresses)
|
||||
if (newPublicTags.size == currentList.tags.size && newPrivateTags.size == oldPrivateTags.size) return null
|
||||
OldBookmarkListEvent.resign(
|
||||
tags = newPublicTags,
|
||||
privateTags = newPrivateTags,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user