diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 91230305..e2f0e092 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -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 = ({ // 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 = ({ { 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 => { diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 645391c4..888699c0 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -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 })) ) } diff --git a/src/types/bookmarks.ts b/src/types/bookmarks.ts index 7cb7c8ea..94d6ec05 100644 --- a/src/types/bookmarks.ts +++ b/src/types/bookmarks.ts @@ -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 { diff --git a/src/utils/bookmarkUtils.tsx b/src/utils/bookmarkUtils.tsx index 6c5b7775..14e0716d 100644 --- a/src/utils/bookmarkUtils.tsx +++ b/src/utils/bookmarkUtils.tsx @@ -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() + + 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)) +}