feat: migrate BookmarkListEvent from kind 30001 to 10003
Rename the existing BookmarkListEvent (kind 30001) to OldBookmarkListEvent and create a new BookmarkListEvent with kind 10003 as per the updated spec. - OldBookmarkListEvent (kind 30001): Kept as-is for backward compatibility, displayed as "Old Bookmarks" in the Bookmark Lists screen - BookmarkListEvent (kind 10003): New replaceable event, all new bookmark operations (save/check/remove) use this kind - Both kinds are displayed as separate items in the Bookmark Lists screen - Old Bookmarks screen includes a "Move All to New Bookmarks" button that migrates public bookmarks to public and private to private - Created PrivateReplaceableTagArrayEvent base class for replaceable events with encrypted private tags (kind 10003 is in the 10000-19999 range) - Updated all relay filters, search queries, and profile views to fetch both kinds - Updated Android and Desktop modules https://claude.ai/code/session_01U9sjQHQMVVHxYiesoXjqop
This commit is contained in:
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
@@ -176,6 +177,7 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
@@ -320,6 +322,7 @@ class Account(
|
||||
val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings)
|
||||
|
||||
val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope)
|
||||
val oldBookmarkState = OldBookmarkListState(signer, cache, scope)
|
||||
val bookmarkState = BookmarkListState(signer, cache, scope)
|
||||
val pinState = PinListState(signer, cache, scope)
|
||||
val emoji = EmojiPackState(signer, cache, scope)
|
||||
@@ -1815,6 +1818,25 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun migrateOldBookmarksToNew() {
|
||||
if (!isWriteable()) return
|
||||
|
||||
val oldList = oldBookmarkState.getBookmarkList() ?: return
|
||||
val publicBookmarks = oldList.publicBookmarks()
|
||||
val privateBookmarks = oldList.privateBookmarks(signer) ?: emptyList()
|
||||
|
||||
if (publicBookmarks.isEmpty() && privateBookmarks.isEmpty()) return
|
||||
|
||||
val newEvent =
|
||||
BookmarkListEvent.create(
|
||||
publicBookmarks = publicBookmarks,
|
||||
privateBookmarks = privateBookmarks,
|
||||
signer = signer,
|
||||
)
|
||||
|
||||
sendMyPublicAndPrivateOutbox(newEvent)
|
||||
}
|
||||
|
||||
suspend fun addPin(note: Note) {
|
||||
if (!isWriteable() || note.isDraft()) return
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEv
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
@@ -580,6 +581,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
fun consumeRegularEvent(
|
||||
event: Event,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -2555,6 +2557,7 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
is BlossomServersEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is BroadcastRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is BookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is OldBookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is CalendarEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is CalendarDateSlotEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
is CalendarTimeSlotEvent -> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
+2
@@ -21,3 +21,5 @@
|
||||
package com.vitorpamplona.amethyst.model.nip51Lists
|
||||
|
||||
typealias BookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
|
||||
|
||||
typealias OldBookmarkListState = com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
@@ -34,6 +35,7 @@ val ReportsAndBookmarksFromKeyKinds =
|
||||
listOf(
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
OldBookmarkListEvent.KIND,
|
||||
LabeledBookmarkListEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
RequestToVanishEvent.KIND,
|
||||
|
||||
+23
-2
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -261,7 +262,7 @@ fun observeUserBookmarkCount(
|
||||
UserFinderFilterAssemblerSubscription(user, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow =
|
||||
val newFlow =
|
||||
remember(user) {
|
||||
accountViewModel
|
||||
.bookmarks(user)
|
||||
@@ -274,7 +275,27 @@ fun observeUserBookmarkCount(
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
return flow.collectAsStateWithLifecycle(0)
|
||||
val oldFlow =
|
||||
remember(user) {
|
||||
accountViewModel
|
||||
.oldBookmarks(user)
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
.sample(200)
|
||||
.mapLatest { noteState ->
|
||||
(noteState.note.event as? OldBookmarkListEvent)?.countBookmarks() ?: 0
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
val combined =
|
||||
remember(user) {
|
||||
kotlinx.coroutines.flow.combine(newFlow, oldFlow) { newCount, oldCount ->
|
||||
newCount + oldCount
|
||||
}
|
||||
}
|
||||
|
||||
return combined.collectAsStateWithLifecycle(0)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
|
||||
+2
@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
@@ -61,6 +62,7 @@ val SearchPostsByTextKinds1 =
|
||||
BadgeDefinitionEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
OldBookmarkListEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -439,6 +440,7 @@ fun Event.toKindName(): String =
|
||||
is VoiceEvent -> stringRes(R.string.voice_post)
|
||||
is VoiceReplyEvent -> stringRes(R.string.voice_reply)
|
||||
is BookmarkListEvent -> stringRes(R.string.bookmarks)
|
||||
is OldBookmarkListEvent -> stringRes(R.string.bookmarks)
|
||||
else -> stringRes(R.string.post)
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.ListOfB
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen
|
||||
@@ -210,6 +211,7 @@ fun BuildNavigation(
|
||||
composableFromEnd<Route.NamecoinSettings> { NamecoinSettingsScreen(nav) }
|
||||
composableFromEnd<Route.OtsSettings> { OtsSettingsScreen(nav) }
|
||||
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.OldBookmarks> { OldBookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.WebBookmarks> { WebBookmarksScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -65,6 +65,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object Bookmarks : Route()
|
||||
|
||||
@Serializable object OldBookmarks : Route()
|
||||
|
||||
@Serializable object BookmarkGroups : Route()
|
||||
|
||||
@Serializable object ImportFollowsSelectUser : Route()
|
||||
|
||||
+3
@@ -134,6 +134,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
@@ -850,6 +851,8 @@ class AccountViewModel(
|
||||
|
||||
fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||
|
||||
fun oldBookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(OldBookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||
|
||||
fun pinnedNotes(user: User): Note = LocalCache.getOrCreateAddressableNote(PinListEvent.createPinAddress(user.pubkeyHex))
|
||||
|
||||
fun addPin(note: Note) {
|
||||
|
||||
+52
@@ -43,6 +43,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -55,8 +56,10 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
@Composable
|
||||
fun ListOfBookmarkGroupsFeedView(
|
||||
defaultBookmarks: BookmarkListState,
|
||||
oldBookmarks: OldBookmarkListState,
|
||||
groupListFeedSource: StateFlow<List<LabeledBookmarkList>>,
|
||||
openDefaultBookmarks: () -> Unit,
|
||||
openOldBookmarks: () -> Unit,
|
||||
onOpenItem: (String, BookmarkType) -> Unit,
|
||||
onRenameItem: (targetBookmarkGroup: LabeledBookmarkList) -> Unit,
|
||||
onItemDescriptionChange: (bookmarkGroup: LabeledBookmarkList) -> Unit,
|
||||
@@ -74,6 +77,11 @@ fun ListOfBookmarkGroupsFeedView(
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
|
||||
item {
|
||||
OldBookmarkList(oldBookmarks, openOldBookmarks)
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
bookmarkGroupFeedState,
|
||||
key = { _: Int, item: LabeledBookmarkList -> item.identifier },
|
||||
@@ -135,3 +143,47 @@ fun DefaultBookmarkList(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OldBookmarkList(
|
||||
oldBookmarks: OldBookmarkListState,
|
||||
openOldBookmarks: () -> Unit,
|
||||
) {
|
||||
val bookmarkState by oldBookmarks.bookmarks.collectAsStateWithLifecycle()
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = openOldBookmarks),
|
||||
headlineContent = {
|
||||
Text(stringRes(R.string.old_bookmarks_title), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
supportingContent = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
stringRes(R.string.old_bookmarks_explainer),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.BookmarkBorder,
|
||||
contentDescription = stringRes(R.string.bookmark_list_icon_label),
|
||||
modifier = Size40Modifier,
|
||||
)
|
||||
Spacer(StdVertSpacer)
|
||||
BookmarkMembershipStatusAndNumberDisplay(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
postBookmarksSize = bookmarkState.public.size + bookmarkState.private.size,
|
||||
articleBookmarksSize = 0,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+7
@@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
@@ -51,8 +52,10 @@ fun ListOfBookmarkGroupsScreen(
|
||||
) {
|
||||
ListOfBookmarkGroupsFeed(
|
||||
defaultBookmarks = accountViewModel.account.bookmarkState,
|
||||
oldBookmarks = accountViewModel.account.oldBookmarkState,
|
||||
listSource = accountViewModel.account.labeledBookmarkLists.listFeedFlow,
|
||||
openDefaultBookmarks = { nav.nav(Route.Bookmarks) },
|
||||
openOldBookmarks = { nav.nav(Route.OldBookmarks) },
|
||||
addBookmarkGroup = { nav.nav(Route.BookmarkGroupMetadataEdit()) },
|
||||
openBookmarkGroup = { identifier, bookmarkType ->
|
||||
nav.nav(Route.BookmarkGroupView(identifier, bookmarkType))
|
||||
@@ -88,8 +91,10 @@ fun ListOfBookmarkGroupsScreen(
|
||||
@Composable
|
||||
fun ListOfBookmarkGroupsFeed(
|
||||
defaultBookmarks: BookmarkListState,
|
||||
oldBookmarks: OldBookmarkListState,
|
||||
listSource: StateFlow<List<LabeledBookmarkList>>,
|
||||
openDefaultBookmarks: () -> Unit,
|
||||
openOldBookmarks: () -> Unit,
|
||||
addBookmarkGroup: () -> Unit,
|
||||
openBookmarkGroup: (identifier: String, bookmarkType: BookmarkType) -> Unit,
|
||||
renameBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit,
|
||||
@@ -115,8 +120,10 @@ fun ListOfBookmarkGroupsFeed(
|
||||
) {
|
||||
ListOfBookmarkGroupsFeedView(
|
||||
defaultBookmarks = defaultBookmarks,
|
||||
oldBookmarks = oldBookmarks,
|
||||
groupListFeedSource = listSource,
|
||||
openDefaultBookmarks = openDefaultBookmarks,
|
||||
openOldBookmarks = openOldBookmarks,
|
||||
onOpenItem = openBookmarkGroup,
|
||||
onRenameItem = renameBookmarkGroup,
|
||||
onItemDescriptionChange = changeBookmarkGroupDescription,
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.old
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPrivateFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPublicFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OldBookmarkListScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val publicFeedViewModel: OldBookmarkPublicFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrOldBookmarkPublicFeedViewModel",
|
||||
factory = OldBookmarkPublicFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
val privateFeedViewModel: OldBookmarkPrivateFeedViewModel =
|
||||
viewModel(
|
||||
key = "NostrOldBookmarkPrivateFeedViewModel",
|
||||
factory = OldBookmarkPrivateFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
val bookmarkState by accountViewModel.account.oldBookmarkState.bookmarks
|
||||
.collectAsStateWithLifecycle(null)
|
||||
|
||||
LaunchedEffect(bookmarkState) {
|
||||
publicFeedViewModel.invalidateData()
|
||||
privateFeedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
RenderOldBookmarkScreen(publicFeedViewModel, privateFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun RenderOldBookmarkScreen(
|
||||
publicFeedViewModel: OldBookmarkPublicFeedViewModel,
|
||||
privateFeedViewModel: OldBookmarkPrivateFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
Column {
|
||||
TopBarWithBackButton(stringRes(id = R.string.old_bookmarks_title), nav::popBack)
|
||||
SecondaryScrollableTabRow(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
edgePadding = 8.dp,
|
||||
modifier = TabRowHeight,
|
||||
) {
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 0,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||
text = { Text(text = stringRes(R.string.private_bookmarks)) },
|
||||
)
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 1,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = { Text(text = stringRes(R.string.public_bookmarks)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringRes(R.string.migrate_bookmarks_button)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.DriveFileMove,
|
||||
contentDescription = stringRes(R.string.migrate_bookmarks_button),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
accountViewModel.launchSigner {
|
||||
accountViewModel.account.migrateOldBookmarksToNew()
|
||||
coroutineScope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.migrate_bookmarks_success),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
RefresheableFeedView(
|
||||
privateFeedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
RefresheableFeedView(
|
||||
publicFeedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.old.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
|
||||
class OldBookmarkPrivateFeedFilter(
|
||||
val account: Account,
|
||||
) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String =
|
||||
account.oldBookmarkState.bookmarks.value
|
||||
.hashCode()
|
||||
.toString()
|
||||
|
||||
override fun feed(): List<Note> = account.oldBookmarkState.bookmarks.value.private
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.old.dal
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
@Stable
|
||||
class OldBookmarkPrivateFeedViewModel(
|
||||
val account: Account,
|
||||
) : AndroidFeedViewModel(OldBookmarkPrivateFeedFilter(account)) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = OldBookmarkPrivateFeedViewModel(account) as T
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.old.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
|
||||
class OldBookmarkPublicFeedFilter(
|
||||
val account: Account,
|
||||
) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String =
|
||||
account.oldBookmarkState.bookmarks.value
|
||||
.hashCode()
|
||||
.toString()
|
||||
|
||||
override fun feed(): List<Note> = account.oldBookmarkState.bookmarks.value.public
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.old.dal
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
|
||||
@Stable
|
||||
class OldBookmarkPublicFeedViewModel(
|
||||
val account: Account,
|
||||
) : AndroidFeedViewModel(OldBookmarkPublicFeedFilter(account)) {
|
||||
class Factory(
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = OldBookmarkPublicFeedViewModel(account) as T
|
||||
}
|
||||
}
|
||||
+24
-10
@@ -26,7 +26,9 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
|
||||
class UserProfileBookmarksFeedFilter(
|
||||
@@ -36,17 +38,29 @@ class UserProfileBookmarksFeedFilter(
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val newBookmarks = getBookmarksFromNew()
|
||||
val oldBookmarks = getBookmarksFromOld()
|
||||
|
||||
return (newBookmarks + oldBookmarks).distinctBy { it.idHex }.reversed()
|
||||
}
|
||||
|
||||
private fun getBookmarksFromNew(): List<Note> {
|
||||
val note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||
val noteEvent = note.event as? BookmarkListEvent ?: return emptyList()
|
||||
|
||||
val notes =
|
||||
noteEvent.publicBookmarks().mapNotNull {
|
||||
when (it) {
|
||||
is AddressBookmark -> LocalCache.getOrCreateAddressableNote(it.address)
|
||||
is EventBookmark -> LocalCache.checkGetOrCreateNote(it.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
return notes.reversed()
|
||||
return resolveBookmarks(noteEvent.publicBookmarks())
|
||||
}
|
||||
|
||||
private fun getBookmarksFromOld(): List<Note> {
|
||||
val note = LocalCache.getOrCreateAddressableNote(OldBookmarkListEvent.createBookmarkAddress(user.pubkeyHex))
|
||||
val noteEvent = note.event as? OldBookmarkListEvent ?: return emptyList()
|
||||
return resolveBookmarks(noteEvent.publicBookmarks())
|
||||
}
|
||||
|
||||
private fun resolveBookmarks(bookmarks: List<BookmarkIdTag>): List<Note> =
|
||||
bookmarks.mapNotNull {
|
||||
when (it) {
|
||||
is AddressBookmark -> LocalCache.getOrCreateAddressableNote(it.address)
|
||||
is EventBookmark -> LocalCache.checkGetOrCreateNote(it.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
@@ -34,6 +35,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendatio
|
||||
val UserProfileListKinds =
|
||||
listOf(
|
||||
BookmarkListEvent.KIND,
|
||||
OldBookmarkListEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
FollowListEvent.KIND,
|
||||
|
||||
+2
@@ -205,6 +205,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEv
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
@@ -569,6 +570,7 @@ fun kindDisplayName(kind: Int): Int =
|
||||
BlossomAuthorizationEvent.KIND -> R.string.kind_blossom_auth
|
||||
BroadcastRelayListEvent.KIND -> R.string.kind_broadcast_relays
|
||||
BookmarkListEvent.KIND -> R.string.kind_bookmark_list
|
||||
OldBookmarkListEvent.KIND -> R.string.kind_old_bookmark_list
|
||||
CalendarDateSlotEvent.KIND -> R.string.kind_day_appointment
|
||||
CalendarEvent.KIND -> R.string.kind_calendar
|
||||
CalendarTimeSlotEvent.KIND -> R.string.kind_appointment
|
||||
|
||||
@@ -408,8 +408,12 @@
|
||||
|
||||
<string name="manual_zaps">Manual Zap Splits</string>
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
<string name="bookmarks_title">Default Bookmarks</string>
|
||||
<string name="bookmarks_explainer">Your default Bookmarks that many clients support</string>
|
||||
<string name="bookmarks_title">Bookmarks</string>
|
||||
<string name="bookmarks_explainer">Your bookmarks (kind 10003)</string>
|
||||
<string name="old_bookmarks_title">Old Bookmarks</string>
|
||||
<string name="old_bookmarks_explainer">Your old bookmarks (kind 30001). Migrate them to the new format.</string>
|
||||
<string name="migrate_bookmarks_button">Move All to New Bookmarks</string>
|
||||
<string name="migrate_bookmarks_success">Bookmarks migrated successfully</string>
|
||||
<string name="drafts">Drafts</string>
|
||||
<string name="polls">Polls</string>
|
||||
<string name="private_bookmarks">Private Bookmarks</string>
|
||||
@@ -1742,6 +1746,7 @@
|
||||
<string name="kind_blossom_auth">Blossom Auth</string>
|
||||
<string name="kind_broadcast_relays">Broadcast Relays</string>
|
||||
<string name="kind_bookmark_list">Bookmark List</string>
|
||||
<string name="kind_old_bookmark_list">Old Bookmark List</string>
|
||||
<string name="kind_day_appointment">Day Appointment</string>
|
||||
<string name="kind_calendar">Calendar</string>
|
||||
<string name="kind_appointment">Appointment</string>
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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.commons.model.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@Stable
|
||||
class OldBookmarkListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: ICacheProvider,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
class BookmarkList(
|
||||
val public: List<Note> = emptyList(),
|
||||
val private: List<Note> = emptyList(),
|
||||
)
|
||||
|
||||
val bookmarkList = cache.getOrCreateAddressableNote(getBookmarkListAddress())
|
||||
|
||||
fun getBookmarkListAddress() = OldBookmarkListEvent.createBookmarkAddress(signer.pubKey)
|
||||
|
||||
fun getBookmarkListFlow(): StateFlow<NoteState> = bookmarkList.flow().metadata.stateFlow
|
||||
|
||||
fun getBookmarkList(): OldBookmarkListEvent? = bookmarkList.event as? OldBookmarkListEvent
|
||||
|
||||
fun publicBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? OldBookmarkListEvent
|
||||
return noteEvent?.publicBookmarks() ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun privateBookmarks(note: Note): List<BookmarkIdTag> {
|
||||
val noteEvent = note.event as? OldBookmarkListEvent
|
||||
return noteEvent?.privateBookmarks(signer) ?: emptyList()
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val publicBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
publicBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(publicBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val privateBookmarks: StateFlow<List<BookmarkIdTag>> =
|
||||
getBookmarkListFlow()
|
||||
.map { noteState ->
|
||||
privateBookmarks(noteState.note)
|
||||
}.onStart {
|
||||
emit(privateBookmarks(bookmarkList))
|
||||
}.debounce(100)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkEventIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val publicBookmarkAddressIdSet =
|
||||
publicBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkEventIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is EventBookmark) it.eventId else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val privateBookmarkAddressIdSet =
|
||||
privateBookmarks
|
||||
.map { bookmark ->
|
||||
bookmark
|
||||
.mapNotNull {
|
||||
if (it is AddressBookmark) it.address else null
|
||||
}.toSet()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
fun bookmarkList(
|
||||
privateBookmarks: List<BookmarkIdTag>,
|
||||
publicBookmarks: List<BookmarkIdTag>,
|
||||
): BookmarkList =
|
||||
BookmarkList(
|
||||
public =
|
||||
publicBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
private =
|
||||
privateBookmarks
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is EventBookmark -> cache.checkGetOrCreateNote(it.eventId)
|
||||
is AddressBookmark -> cache.getOrCreateAddressableNote(it.address)
|
||||
}
|
||||
}.reversed(),
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val bookmarks: StateFlow<BookmarkList> =
|
||||
combineTransform(privateBookmarks, publicBookmarks) { private, public ->
|
||||
emit(bookmarkList(private, public))
|
||||
}.onStart {
|
||||
emit(bookmarkList(privateBookmarks.value, publicBookmarks.value))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
BookmarkList(),
|
||||
)
|
||||
|
||||
fun isInPrivateBookmarks(note: Note): Boolean {
|
||||
if (!signer.isWriteable()) return false
|
||||
|
||||
return if (note is AddressableNote) {
|
||||
privateBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
privateBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInPublicBookmarks(note: Note): Boolean =
|
||||
if (note is AddressableNote) {
|
||||
publicBookmarkAddressIdSet.value.contains(note.address)
|
||||
} else {
|
||||
publicBookmarkEventIdSet.value.contains(note.idHex)
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -45,6 +45,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
@@ -213,6 +214,10 @@ class DesktopLocalCache : ICacheProvider {
|
||||
consumeBookmarkList(event)
|
||||
}
|
||||
|
||||
is OldBookmarkListEvent -> {
|
||||
consumeOldBookmarkList(event)
|
||||
}
|
||||
|
||||
is CommentEvent -> {
|
||||
consumeComment(event, relay)
|
||||
}
|
||||
@@ -419,6 +424,19 @@ class DesktopLocalCache : ICacheProvider {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun consumeOldBookmarkList(event: OldBookmarkListEvent): Boolean {
|
||||
val address = event.address()
|
||||
val addressableNote = getOrCreateAddressableNote(address)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
// Only update if newer
|
||||
val existingEvent = addressableNote.event
|
||||
if (existingEvent != null && existingEvent.createdAt >= event.createdAt) return false
|
||||
|
||||
addressableNote.loadEvent(event, author, emptyList())
|
||||
return true
|
||||
}
|
||||
|
||||
// ----- NWC Payment operations -----
|
||||
|
||||
/**
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.nip02FollowList.Kind3FollowListRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.nip02FollowList.Kind3FollowListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip65RelayList.Nip65RelayListRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList
|
||||
@@ -76,6 +77,7 @@ class DesktopIAccount(
|
||||
|
||||
// ----- State Classes (pin important notes via strong refs for GC retention) -----
|
||||
|
||||
val oldBookmarkState = OldBookmarkListState(signer, localCache, scope)
|
||||
val bookmarkState = BookmarkListState(signer, localCache, scope)
|
||||
|
||||
val kind3FollowList =
|
||||
|
||||
+2
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
@@ -58,6 +59,7 @@ object SearchFilterFactory {
|
||||
BadgeDefinitionEvent.KIND,
|
||||
PeopleListEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
OldBookmarkListEvent.KIND,
|
||||
AudioHeaderEvent.KIND,
|
||||
AudioTrackEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.quartz.nip51Lists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
@Immutable
|
||||
abstract class PrivateReplaceableTagArrayEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: TagArray,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
suspend fun decrypt(signer: NostrSigner): TagArray {
|
||||
if (signer.pubKey != pubKey) throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
|
||||
return PrivateTagsInContent.decrypt(content, signer)
|
||||
}
|
||||
|
||||
suspend fun privateTags(signer: NostrSigner): TagArray? {
|
||||
if (signer.pubKey != pubKey) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
PrivateTagsInContent.decrypt(content, signer)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-10
@@ -31,10 +31,9 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateReplaceableTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
@@ -51,7 +50,7 @@ class BookmarkListEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
) : PrivateReplaceableTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
|
||||
@@ -71,11 +70,10 @@ class BookmarkListEvent(
|
||||
suspend fun privateBookmarks(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30001
|
||||
const val KIND = 10003
|
||||
const val ALT = "List of bookmarks"
|
||||
const val DEFAULT_D_TAG_BOOKMARKS = "bookmark"
|
||||
|
||||
fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, DEFAULT_D_TAG_BOOKMARKS)
|
||||
fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, "")
|
||||
|
||||
suspend fun create(
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
@@ -195,11 +193,10 @@ class BookmarkListEvent(
|
||||
title: String = "",
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): BookmarkListEvent {
|
||||
val template = build(title, publicBookmarks, privateBookmarks, signer, dTag, createdAt)
|
||||
val template = build(title, publicBookmarks, privateBookmarks, signer, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
@@ -208,7 +205,6 @@ class BookmarkListEvent(
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<BookmarkListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
@@ -216,7 +212,6 @@ class BookmarkListEvent(
|
||||
description = PrivateTagsInContent.encryptNip44(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
dTag(dTag)
|
||||
alt(ALT)
|
||||
title(title)
|
||||
bookmarks(publicBookmarks)
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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.quartz.nip51Lists.bookmarkList
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.fastAny
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
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.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class OldBookmarkListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider {
|
||||
override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId)
|
||||
|
||||
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun countBookmarks() = tags.count(BookmarkIdTag::isTagged)
|
||||
|
||||
fun publicBookmarks(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse)
|
||||
|
||||
suspend fun privateBookmarks(signer: NostrSigner): List<BookmarkIdTag>? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse)
|
||||
|
||||
companion object {
|
||||
const val KIND = 30001
|
||||
const val ALT = "List of bookmarks"
|
||||
const val DEFAULT_D_TAG_BOOKMARKS = "bookmark"
|
||||
|
||||
fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, DEFAULT_D_TAG_BOOKMARKS)
|
||||
|
||||
suspend fun create(
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
create(
|
||||
publicBookmarks = emptyList(),
|
||||
privateBookmarks = listOf(bookmarkIdTag),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
publicBookmarks = listOf(bookmarkIdTag),
|
||||
privateBookmarks = emptyList(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun add(
|
||||
earlierVersion: OldBookmarkListEvent,
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
tags = earlierVersion.tags,
|
||||
privateTags = privateTags.plus(bookmarkIdTag.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(bookmarkIdTag.toTagArray()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: OldBookmarkListEvent,
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
isPrivate: Boolean,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent =
|
||||
if (isPrivate) {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
resign(
|
||||
privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
tags = earlierVersion.tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
} else {
|
||||
resign(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun remove(
|
||||
earlierVersion: OldBookmarkListEvent,
|
||||
bookmarkIdTag: BookmarkIdTag,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun resign(
|
||||
tags: TagArray,
|
||||
privateTags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
) = resign(
|
||||
content = PrivateTagsInContent.encryptNip44(privateTags, signer),
|
||||
tags = tags,
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
suspend fun resign(
|
||||
content: String,
|
||||
tags: TagArray,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent {
|
||||
val newTags =
|
||||
if (tags.fastAny(AltTag::match)) {
|
||||
tags
|
||||
} else {
|
||||
tags + AltTag.assemble(ALT)
|
||||
}
|
||||
|
||||
return signer.sign(createdAt, KIND, newTags, content)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
title: String = "",
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): OldBookmarkListEvent {
|
||||
val template = build(title, publicBookmarks, privateBookmarks, signer, dTag, createdAt)
|
||||
return signer.sign(template)
|
||||
}
|
||||
|
||||
suspend fun build(
|
||||
title: String = "",
|
||||
publicBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
privateBookmarks: List<BookmarkIdTag> = emptyList(),
|
||||
signer: NostrSigner,
|
||||
dTag: String = DEFAULT_D_TAG_BOOKMARKS,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<OldBookmarkListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(
|
||||
kind = KIND,
|
||||
description = PrivateTagsInContent.encryptNip44(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer),
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
dTag(dTag)
|
||||
alt(ALT)
|
||||
addUnique(TitleTag.assemble(title))
|
||||
addAll(publicBookmarks.map { it.toTagArray() })
|
||||
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
@@ -288,6 +289,7 @@ class EventFactory {
|
||||
BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BroadcastRelayListEvent.KIND -> BroadcastRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
OldBookmarkListEvent.KIND -> OldBookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
Reference in New Issue
Block a user