feat: add support for bookmark sets (kind 30003)

- Add setName, setTitle, setDescription, and setImage fields to IndividualBookmark type
- Extract d tag and metadata from kind 30003 events in bookmark processing
- Create helper functions to group bookmarks by set and extract set metadata
- Display bookmark sets as separate sections in BookmarkList UI
- Maintain existing content-type categorization alongside bookmark sets
This commit is contained in:
Gigi
2025-10-15 15:25:33 +02:00
parent 715fd8cf10
commit eaa590b8e2
4 changed files with 95 additions and 6 deletions

View File

@@ -12,7 +12,7 @@ import { ViewMode } from './Bookmarks'
import { usePullToRefresh } from 'use-pull-to-refresh'
import RefreshIndicator from './RefreshIndicator'
import { BookmarkSkeleton } from './Skeletons'
import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils'
import { groupIndividualBookmarks, hasContent, getBookmarkSets, getBookmarksWithoutSet } from '../utils/bookmarkUtils'
import { UserSettings } from '../services/settingsService'
interface BookmarkListProps {
@@ -71,7 +71,13 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
// Merge and flatten all individual bookmarks from all lists
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
.filter(hasContent)
const groups = groupIndividualBookmarks(allIndividualBookmarks)
// Separate bookmarks with setName (kind 30003) from regular bookmarks
const bookmarksWithoutSet = getBookmarksWithoutSet(allIndividualBookmarks)
const bookmarkSets = getBookmarkSets(allIndividualBookmarks)
// Group non-set bookmarks as before
const groups = groupIndividualBookmarks(bookmarksWithoutSet)
const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [
{ key: 'private', title: 'Private bookmarks', items: groups.privateItems },
{ key: 'public', title: 'Public bookmarks', items: groups.publicItems },
@@ -79,6 +85,15 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
{ key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst }
]
// Add bookmark sets as additional sections
bookmarkSets.forEach(set => {
sections.push({
key: `set-${set.name}`,
title: set.title || set.name,
items: set.bookmarks
})
})
if (isCollapsed) {
// Check if the selected URL is in bookmarks
const isBookmarked = selectedUrl && bookmarks.some(bookmark => {

View File

@@ -33,6 +33,12 @@ export async function collectBookmarksFromEvents(
if (!latestContent && evt.content && !Helpers.hasHiddenContent(evt)) latestContent = evt.content
if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags)
// Extract the 'd' tag and metadata for bookmark sets (kind 30003)
const dTag = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] : undefined
const setTitle = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'title')?.[1] : undefined
const setDescription = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'description')?.[1] : undefined
const setImage = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'image')?.[1] : undefined
// Handle web bookmarks (kind:39701) as individual bookmarks
if (evt.kind === 39701) {
publicItemsAll.push({
@@ -46,7 +52,11 @@ export async function collectBookmarksFromEvents(
type: 'web' as const,
isPrivate: false,
added_at: evt.created_at || Math.floor(Date.now() / 1000),
sourceKind: 39701
sourceKind: 39701,
setName: dTag,
setTitle,
setDescription,
setImage
})
continue
}
@@ -55,7 +65,11 @@ export async function collectBookmarksFromEvents(
publicItemsAll.push(
...processApplesauceBookmarks(pub, activeAccount, false).map(i => ({
...i,
sourceKind: evt.kind
sourceKind: evt.kind,
setName: dTag,
setTitle,
setDescription,
setImage
}))
)
@@ -103,7 +117,11 @@ export async function collectBookmarksFromEvents(
privateItemsAll.push(
...processApplesauceBookmarks(manualPrivate, activeAccount, true).map(i => ({
...i,
sourceKind: evt.kind
sourceKind: evt.kind,
setName: dTag,
setTitle,
setDescription,
setImage
}))
)
Reflect.set(evt, BookmarkHiddenSymbol, manualPrivate)
@@ -120,7 +138,11 @@ export async function collectBookmarksFromEvents(
privateItemsAll.push(
...processApplesauceBookmarks(priv, activeAccount, true).map(i => ({
...i,
sourceKind: evt.kind
sourceKind: evt.kind,
setName: dTag,
setTitle,
setDescription,
setImage
}))
)
}

View File

@@ -44,6 +44,12 @@ export interface IndividualBookmark {
added_at?: number
// The kind of the source list/set that produced this bookmark (e.g., 10003, 30003, 30001, or 39701 for web)
sourceKind?: number
// The 'd' tag value from kind 30003 bookmark sets
setName?: string
// Metadata from the bookmark set event (kind 30003)
setTitle?: string
setDescription?: string
setImage?: string
}
export interface ActiveAccount {

View File

@@ -104,3 +104,49 @@ export function groupIndividualBookmarks(items: IndividualBookmark[]) {
export function hasContent(bookmark: IndividualBookmark): boolean {
return !!(bookmark.content && bookmark.content.trim().length > 0)
}
// Bookmark sets helpers (kind 30003)
export interface BookmarkSet {
name: string
title?: string
description?: string
image?: string
bookmarks: IndividualBookmark[]
}
export function getBookmarkSets(items: IndividualBookmark[]): BookmarkSet[] {
// Group bookmarks by setName
const setMap = new Map<string, IndividualBookmark[]>()
items.forEach(bookmark => {
if (bookmark.setName) {
const existing = setMap.get(bookmark.setName) || []
existing.push(bookmark)
setMap.set(bookmark.setName, existing)
}
})
// Convert to array and extract metadata from the bookmarks
const sets: BookmarkSet[] = []
setMap.forEach((bookmarks, name) => {
// Get metadata from the first bookmark (all bookmarks in a set share the same metadata)
const firstBookmark = bookmarks[0]
const title = firstBookmark?.setTitle
const description = firstBookmark?.setDescription
const image = firstBookmark?.setImage
sets.push({
name,
title,
description,
image,
bookmarks: sortIndividualBookmarks(bookmarks)
})
})
return sets.sort((a, b) => a.name.localeCompare(b.name))
}
export function getBookmarksWithoutSet(items: IndividualBookmark[]): IndividualBookmark[] {
return sortIndividualBookmarks(items.filter(b => !b.setName))
}