- {#if PLATFORM_RELAYS.length > 0}
-
- {/if}
-
+
+
+ {bookmarkListCount}
+
{#if $userProfile?.picture}
@@ -83,7 +175,11 @@
-
+
+
+ {bookmarkListCount}
+
{#if PLATFORM_RELAYS.length !== 1}
diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ThreadActions.svelte
index d34dedea..2752e16c 100644
--- a/src/app/components/ThreadActions.svelte
+++ b/src/app/components/ThreadActions.svelte
@@ -7,6 +7,7 @@
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
+ import BookmarkEventMenuItem from "@app/components/BookmarkEventMenuItem.svelte"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeThreadPath, makeSpacePath} from "@app/util/routes"
@@ -41,5 +42,9 @@
{#if showActivity}
{/if}
-
+
+ {#snippet customActions()}
+
+ {/snippet}
+
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts
index 7d3015e7..5daa2fd3 100644
--- a/src/app/core/commands.ts
+++ b/src/app/core/commands.ts
@@ -14,6 +14,7 @@ import {
simpleCache,
normalizeUrl,
nthNe,
+ randomId,
} from "@welshman/lib"
import {Nip01Signer} from "@welshman/signer"
import type {UploadTask} from "@welshman/editor"
@@ -33,11 +34,11 @@ import {
ROOMS,
COMMENT,
APP_DATA,
- asDecryptedEvent,
isSignedEvent,
makeEvent,
normalizeRelayUrl,
makeList,
+ Address,
addToListPublicly,
removeFromListByPredicate,
updateList,
@@ -49,7 +50,6 @@ import {
toNostrURI,
RelayMode,
getTagValues,
- readList,
uploadBlob,
canUploadBlob,
encryptFile,
@@ -86,10 +86,14 @@ import {
import {compressFile} from "@lib/html"
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
import {
+ BOOKMARKS,
+ BOOKMARK_LISTS,
SETTINGS,
PROTECTED,
INDEXER_RELAYS,
DEFAULT_BLOSSOM_SERVERS,
+ getBookmarkList,
+ getBookmarkCollection,
userSpaceUrls,
userSettingsValues,
getSetting,
@@ -171,59 +175,51 @@ export const broadcastUserData = async (relays: string[]) => {
// List updates
-const BOOKMARKS = 10003
-const BOOKMARK_LISTS = 30003
-
-const parseListKey = (key: string) => {
- const [kind, ...rest] = key.split(":")
- return {
- kind: parseInt(kind),
- d: rest.join(":"),
+const getSavedItemsList = (owner = pubkey.get() || "") => {
+ if (!owner) {
+ return makeList({kind: BOOKMARKS})
}
+
+ return getBookmarkList().get(owner) || makeList({kind: BOOKMARKS})
}
-const getUserBookmarkList = (key = `${BOOKMARKS}:`) => {
- const author = pubkey.get()
- const {kind, d} = parseListKey(key)
+const getBookmarkListFromAddress = (address: string) => {
+ const parsed = Address.from(address)
- if (!author) {
- return makeList({kind})
+ if (parsed.kind === BOOKMARKS) {
+ return getSavedItemsList(parsed.pubkey)
}
- const latest = first(
- repository
- .query([{kinds: [kind], authors: [author]}])
- .filter(event => getTagValue("d", event.tags) === d),
- )
+ if (parsed.kind === BOOKMARK_LISTS) {
+ return getBookmarkCollection().get(address)
+ }
- return latest ? readList(asDecryptedEvent(latest)) : makeList({kind})
+ return undefined
}
export const createBookmarkList = async (title: string) => {
- const d = title.trim()
+ const label = title.trim()
- if (!d) {
- return
- }
-
- const existing = getUserBookmarkList(`${BOOKMARK_LISTS}:${d}`)
-
- if (existing.event?.id) {
+ if (!label) {
return
}
const list = makeList({kind: BOOKMARK_LISTS})
- const event = await updateList(list, {publicTags: [["d", d]]}).reconcile(nip44EncryptToSelf)
- const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
+ const event = await updateList(list, {
+ publicTags: [
+ ["d", randomId()],
+ ["title", label],
+ ],
+ }).reconcile(nip44EncryptToSelf)
+ const relays = Router.get().FromUser().getUrls()
return publishThunk({event, relays})
}
-export const addEventBookmark = async (target: TrustedEvent, key = `${BOOKMARKS}:`) => {
- const list = getUserBookmarkList(key)
- const {d} = parseListKey(key)
+export const addEventBookmark = async (target: TrustedEvent, address?: string) => {
+ const list = address ? getBookmarkListFromAddress(address) : getSavedItemsList()
- if (d && !list.event?.id) {
+ if (!list) {
return
}
@@ -234,20 +230,15 @@ export const addEventBookmark = async (target: TrustedEvent, key = `${BOOKMARKS}
}
const event = await addToListPublicly(list, ["e", target.id]).reconcile(nip44EncryptToSelf)
- const relays = uniq([
- ...Router.get().FromUser().getUrls(),
- ...Router.get().Event(target).limit(3).getUrls(),
- ...getRelayTagValues(event.tags),
- ])
+ const relays = Router.get().FromUser().getUrls()
return publishThunk({event, relays})
}
-export const removeEventBookmark = async (target: TrustedEvent, key = `${BOOKMARKS}:`) => {
- const list = getUserBookmarkList(key)
- const {d} = parseListKey(key)
+export const removeEventBookmark = async (target: TrustedEvent, address?: string) => {
+ const list = address ? getBookmarkListFromAddress(address) : getSavedItemsList()
- if (d && !list.event?.id) {
+ if (!list) {
return
}
@@ -269,51 +260,46 @@ export const removeEventBookmark = async (target: TrustedEvent, key = `${BOOKMAR
(tag[0] === "e" && tag[1] === target.id) ||
(targetAddress !== undefined && tag[0] === "a" && tag[1] === targetAddress),
).reconcile(nip44EncryptToSelf)
- const relays = uniq([
- ...INDEXER_RELAYS,
- ...getRelayTagValues(event.tags),
- ...Router.get().Event(target).limit(3).getUrls(),
- ])
+ const relays = Router.get().FromUser().getUrls()
return publishThunk({event, relays})
}
-export const deleteBookmarkList = async (key: string) => {
- const list = getUserBookmarkList(key)
- const {kind, d} = parseListKey(key)
+export const deleteBookmarkList = async (address: string) => {
+ const list = getBookmarkCollection().get(address)
+ const {kind} = Address.from(address)
- if (kind !== BOOKMARK_LISTS || !d || !list.event) {
+ if (kind !== BOOKMARK_LISTS || !list?.event) {
return
}
- const relays = uniq([...INDEXER_RELAYS, ...getRelayTagValues(list.event.tags)])
- const address = `${kind}:${list.event.pubkey}:${d}`
+ const relays = Router.get().FromUser().getUrls()
return publishDelete({protect: false, event: list.event, tags: [["a", address]], relays})
}
-export const renameBookmarkList = async (key: string, title: string) => {
- const list = getUserBookmarkList(key)
- const {kind, d} = parseListKey(key)
- const nextD = title.trim()
+export const renameBookmarkList = async (address: string, title: string) => {
+ const list = getBookmarkCollection().get(address)
+ const {kind} = Address.from(address)
+ const nextTitle = title.trim()
- if (kind !== BOOKMARK_LISTS || !d || !nextD || !list.event) {
+ if (kind !== BOOKMARK_LISTS || !nextTitle || !list?.event) {
return
}
- if (nextD === d) {
+ const currentTitle =
+ getTagValue("title", list.event.tags) || getTagValue("d", list.event.tags) || ""
+
+ if (nextTitle === currentTitle) {
return
}
- const existing = getUserBookmarkList(`${BOOKMARK_LISTS}:${nextD}`)
-
- if (existing.event?.id && existing.event.id !== list.event.id) {
- return
- }
-
- const publicTags = [["d", nextD], ...list.publicTags.filter(tag => tag[0] !== "d")]
+ const publicTags = [
+ ["d", getTagValue("d", list.event.tags) || randomId()],
+ ["title", nextTitle],
+ ]
const event = await updateList(list, {publicTags}).reconcile(nip44EncryptToSelf)
- const relays = uniq([...INDEXER_RELAYS, ...getRelayTagValues(list.event.tags)])
+ const relays = Router.get().FromUser().getUrls()
return publishThunk({event, relays})
}
diff --git a/src/app/core/state.ts b/src/app/core/state.ts
index 6d2ad6fb..4ddef3d3 100644
--- a/src/app/core/state.ts
+++ b/src/app/core/state.ts
@@ -161,6 +161,10 @@ export const PROTECTED = ["-"]
export const MESSAGE_KINDS = [MESSAGE]
+export const BOOKMARKS = 10003
+
+export const BOOKMARK_LISTS = 30003
+
export const IMAGE_CONTENT_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]
export const VIDEO_CONTENT_TYPES = ["video/quicktime", "video/webm", "video/mp4"]
@@ -706,6 +710,50 @@ export const deriveOtherVoiceRooms = (url: string) =>
// User space/room lists
+export const bookmarkListsByPubkey = deriveItemsByKey({
+ repository,
+ filters: [{kinds: [BOOKMARKS]}],
+ getKey: list => list.event.pubkey,
+ eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
+})
+
+export const getBookmarkList = getter(bookmarkListsByPubkey)
+
+export const loadBookmarkList = makeLoadItem(makeOutboxLoader(BOOKMARKS), getBookmarkList)
+
+export const userBookmarkList = makeUserData(bookmarkListsByPubkey, loadBookmarkList)
+
+export const loadUserBookmarkList = makeUserLoader(loadBookmarkList)
+
+export const bookmarkCollectionsByAddress = deriveItemsByKey({
+ repository,
+ filters: [{kinds: [BOOKMARK_LISTS]}],
+ getKey: list => getAddress(list.event),
+ eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
+})
+
+export const getBookmarkCollection = getter(bookmarkCollectionsByAddress)
+
+export const bookmarkCollections = deriveItems(bookmarkCollectionsByAddress)
+
+export const bookmarkCollectionsByPubkey = derived(bookmarkCollections, $lists =>
+ groupBy(list => list.event.pubkey, $lists),
+)
+
+export const getBookmarkCollections = getter(bookmarkCollectionsByPubkey)
+
+export const loadBookmarkCollections = makeLoadItem(
+ makeOutboxLoader(BOOKMARK_LISTS),
+ getBookmarkCollections,
+)
+
+export const userBookmarkCollections = makeUserData(
+ bookmarkCollectionsByPubkey,
+ loadBookmarkCollections,
+)
+
+export const loadUserBookmarkCollections = makeUserLoader(loadBookmarkCollections)
+
export const groupListsByPubkey = deriveItemsByKey({
repository,
filters: [{kinds: [ROOMS]}],
diff --git a/src/routes/bookmarks/+page.svelte b/src/routes/bookmarks/+page.svelte
index 0ec98f90..c630dfa6 100644
--- a/src/routes/bookmarks/+page.svelte
+++ b/src/routes/bookmarks/+page.svelte
@@ -1,45 +1,31 @@
-
{
- closeListContextMenu()
- closeBookmarkMenu()
- }}
- onkeydown={handleWindowKeydown} />
-
-
-
-
-
-
- Bookmarks
-
- {totalListCount}
-
-
- My Lists
-
-
-
-
- {#each sidebarListsWithDisplayCount as list (list.key)}
-
onListContextMenu(event, list.key, list.label)}>
-
-
-
-
-
- {list.label}
-
-
-
{list.displayCount}
-
-
-
- {:else}
-
No lists found yet.
- {/each}
-
-
- {#if menuOpen}
-
-
-
-
-
- {#if menuListKey !== "10003:"}
-
-
-
-
-
-
- {/if}
-
-
- {/if}
-
- {#if listDialogMode}
-
-
-
-
- {#if listDialogMode === "rename"}
-
-
Rename List
-
Choose a new name for "{dialogListLabel}".
-
-
-
-
-
-
-
-
- {:else}
-
-
Delete List?
-
- This will remove "{dialogListLabel}" and cannot be undone.
-
-
-
-
-
-
-
- {/if}
-
-
-
- {/if}
-
+ goto(selectedListHref(key), {replaceState: true, noScroll: true})}
+ onRename={renameListFromSidebar}
+ onDelete={deleteListFromSidebar} />
-
{selectedList?.label || "Saved Items"}
+
+ {#if listsReady}
+ {selectedList?.label || "Saved Items"}
+ {:else}
+ Loading bookmarks...
+ {/if}
+
{filteredItems.length}
@@ -810,6 +561,12 @@
+
+ {filteredItems.length} items
+ •
+ {listsReady ? selectedList?.label || "Saved Items" : "Loading"}
+
+
{#if showSearch}
{/if}
-
- {#each filteredItems as item (item.key)}
-
-
-
-
-
-
- {displayProfileByPubkey(item.event.pubkey)}
-
-
- {displayPubkey(item.event.pubkey)} · {formatTimestamp(item.event.created_at)}
-
-
-
-
-
- {#if openBookmarkMenuKey === item.key}
-
- -
-
-
- -
-
-
- {#if selectedKey !== "10003:"}
- -
-
-
- {/if}
- -
-
-
-
- {/if}
-
-
-
- {#if item.image}
- {#if asLinkValue(item.image)}
-
- {/if}
- {:else if item.video}
-
- {/if}
-
-
{item.preview}
-
- {:else}
-
- No items match your current filters.
-
- {/each}
-
+ {#if listsReady}
+
+ {:else}
+
Loading bookmark lists...
+ {/if}