mirror of
https://github.com/dergigi/boris.git
synced 2026-02-09 17:14:58 +01:00
219 lines
8.1 KiB
TypeScript
219 lines
8.1 KiB
TypeScript
import { RelayPool } from 'applesauce-relay'
|
|
import { completeOnEose } from 'applesauce-relay'
|
|
import { getParsedContent } from 'applesauce-content/text'
|
|
import { Helpers } from 'applesauce-core'
|
|
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
|
|
import { Bookmark, IndividualBookmark, ParsedContent, ActiveAccount } from '../types/bookmarks'
|
|
|
|
interface BookmarkData {
|
|
id?: string
|
|
content?: string
|
|
created_at?: number
|
|
kind?: number
|
|
tags?: string[][]
|
|
}
|
|
|
|
interface ApplesauceBookmarks {
|
|
notes?: BookmarkData[]
|
|
articles?: BookmarkData[]
|
|
hashtags?: BookmarkData[]
|
|
urls?: BookmarkData[]
|
|
}
|
|
|
|
interface AccountWithExtension {
|
|
pubkey: string
|
|
signer?: unknown
|
|
[key: string]: unknown // Allow any properties from the full account object
|
|
}
|
|
|
|
// Type guard to check if an object has the required properties
|
|
function isAccountWithExtension(account: unknown): account is AccountWithExtension {
|
|
return typeof account === 'object' && account !== null && 'pubkey' in account
|
|
}
|
|
|
|
function isEncryptedContent(content: string | undefined): boolean {
|
|
if (!content) return false
|
|
return (
|
|
content.startsWith('nip44:') ||
|
|
content.startsWith('nip04:') ||
|
|
content.includes('?iv=') ||
|
|
content.includes('?version=')
|
|
)
|
|
}
|
|
|
|
const processApplesauceBookmarks = (
|
|
bookmarks: unknown,
|
|
activeAccount: ActiveAccount,
|
|
isPrivate: boolean
|
|
): IndividualBookmark[] => {
|
|
if (!bookmarks) return []
|
|
|
|
// Handle applesauce structure: {notes: [], articles: [], hashtags: [], urls: []}
|
|
if (typeof bookmarks === 'object' && bookmarks !== null && !Array.isArray(bookmarks)) {
|
|
const applesauceBookmarks = bookmarks as ApplesauceBookmarks
|
|
const allItems: BookmarkData[] = []
|
|
|
|
if (applesauceBookmarks.notes) allItems.push(...applesauceBookmarks.notes)
|
|
if (applesauceBookmarks.articles) allItems.push(...applesauceBookmarks.articles)
|
|
if (applesauceBookmarks.hashtags) allItems.push(...applesauceBookmarks.hashtags)
|
|
if (applesauceBookmarks.urls) allItems.push(...applesauceBookmarks.urls)
|
|
|
|
return allItems.map((bookmark: BookmarkData) => ({
|
|
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
|
content: bookmark.content || '',
|
|
created_at: bookmark.created_at || Date.now(),
|
|
pubkey: activeAccount.pubkey,
|
|
kind: bookmark.kind || 30001,
|
|
tags: bookmark.tags || [],
|
|
parsedContent: bookmark.content ? getParsedContent(bookmark.content) as ParsedContent : undefined,
|
|
type: 'event' as const,
|
|
isPrivate
|
|
}))
|
|
}
|
|
|
|
// Fallback: map array-like bookmarks
|
|
const bookmarkArray = Array.isArray(bookmarks) ? bookmarks : [bookmarks]
|
|
return bookmarkArray.map((bookmark: BookmarkData) => ({
|
|
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
|
content: bookmark.content || '',
|
|
created_at: bookmark.created_at || Date.now(),
|
|
pubkey: activeAccount.pubkey,
|
|
kind: bookmark.kind || 30001,
|
|
tags: bookmark.tags || [],
|
|
parsedContent: bookmark.content ? getParsedContent(bookmark.content) as ParsedContent : undefined,
|
|
type: 'event' as const,
|
|
isPrivate
|
|
}))
|
|
}
|
|
|
|
|
|
|
|
export const fetchBookmarks = async (
|
|
relayPool: RelayPool,
|
|
activeAccount: unknown, // Full account object with extension capabilities
|
|
setBookmarks: (bookmarks: Bookmark[]) => void,
|
|
setLoading: (loading: boolean) => void,
|
|
timeoutId: number
|
|
) => {
|
|
try {
|
|
setLoading(true)
|
|
|
|
// Type check the account object
|
|
if (!isAccountWithExtension(activeAccount)) {
|
|
throw new Error('Invalid account object provided')
|
|
}
|
|
|
|
console.log('🚀 Using applesauce bookmark helpers for pubkey:', activeAccount.pubkey)
|
|
|
|
// Get relay URLs from the pool
|
|
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
|
|
|
|
// Fetch bookmark lists (10003) and bookmarksets (30001)
|
|
const rawEvents = await lastValueFrom(
|
|
relayPool.req(relayUrls, {
|
|
kinds: [10003, 30001],
|
|
authors: [activeAccount.pubkey],
|
|
limit: 50
|
|
}).pipe(completeOnEose(), takeUntil(timer(10000)), toArray())
|
|
)
|
|
// Deduplicate by id
|
|
const bookmarkListEvents = Array.from(new Map(rawEvents.map((e: any) => [e.id, e])).values())
|
|
|
|
if (bookmarkListEvents.length === 0) {
|
|
setBookmarks([])
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
// Aggregate across all events
|
|
const maybeAccount = activeAccount as any
|
|
const signerCandidate = typeof maybeAccount?.signEvent === 'function' ? maybeAccount : maybeAccount?.signer
|
|
const publicItemsAll: IndividualBookmark[] = []
|
|
const privateItemsAll: IndividualBookmark[] = []
|
|
let newestCreatedAt = 0
|
|
let latestContent = ''
|
|
let allTags: string[][] = []
|
|
for (const evt of bookmarkListEvents) {
|
|
const hasHiddenBefore = Helpers.hasHiddenTags(evt)
|
|
const lockedBefore = Helpers.isHiddenTagsLocked(evt)
|
|
console.log('[bookmarks] evt', evt.id, 'kind', evt.kind, 'hidden?', hasHiddenBefore, 'locked?', lockedBefore)
|
|
newestCreatedAt = Math.max(newestCreatedAt, evt.created_at || 0)
|
|
if (!latestContent && evt.content && !isEncryptedContent(evt.content)) latestContent = evt.content
|
|
if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags)
|
|
// public
|
|
const pub = Helpers.getPublicBookmarks(evt)
|
|
publicItemsAll.push(...processApplesauceBookmarks(pub, activeAccount, false))
|
|
// hidden
|
|
try {
|
|
const hasHidden = Helpers.hasHiddenTags(evt)
|
|
const locked = Helpers.isHiddenTagsLocked(evt)
|
|
if (hasHidden && locked && signerCandidate) {
|
|
console.log('[bookmarks] unlocking hidden tags for', evt.id)
|
|
try {
|
|
await Helpers.unlockHiddenTags(evt, signerCandidate)
|
|
} catch {
|
|
// Fallback to nip44 if default fails
|
|
await Helpers.unlockHiddenTags(evt, signerCandidate as any, 'nip44' as any)
|
|
}
|
|
}
|
|
const lockedAfter = Helpers.isHiddenTagsLocked(evt)
|
|
const priv = Helpers.getHiddenBookmarks(evt)
|
|
console.log('[bookmarks] unlocked?', !lockedAfter, 'private items present?', !!priv && (
|
|
(priv as any).notes?.length || (priv as any).articles?.length || (priv as any).hashtags?.length || (priv as any).urls?.length
|
|
))
|
|
privateItemsAll.push(...processApplesauceBookmarks(priv, activeAccount, true))
|
|
} catch {
|
|
// ignore per-event failures
|
|
}
|
|
}
|
|
|
|
// Hydrate note pointers (ids) into full events so content renders
|
|
const allItems = [...publicItemsAll, ...privateItemsAll]
|
|
const noteIds = Array.from(new Set(allItems.map(i => i.id).filter(Boolean)))
|
|
let idToEvent: Map<string, any> = new Map()
|
|
if (noteIds.length > 0) {
|
|
try {
|
|
const events = await lastValueFrom(
|
|
relayPool.req(relayUrls, { ids: noteIds }).pipe(completeOnEose(), takeUntil(timer(10000)), toArray())
|
|
)
|
|
idToEvent = new Map(events.map((e: any) => [e.id, e]))
|
|
} catch {}
|
|
}
|
|
const hydrateItems = (items: IndividualBookmark[]): IndividualBookmark[] => items.map(item => {
|
|
const ev = idToEvent.get(item.id)
|
|
if (!ev) return item
|
|
return {
|
|
...item,
|
|
content: ev.content || item.content || '',
|
|
created_at: ev.created_at || item.created_at,
|
|
kind: ev.kind || item.kind,
|
|
tags: ev.tags || item.tags,
|
|
parsedContent: ev.content ? getParsedContent(ev.content) as ParsedContent : item.parsedContent
|
|
}
|
|
})
|
|
const allBookmarks = [...hydrateItems(publicItemsAll), ...hydrateItems(privateItemsAll)]
|
|
|
|
const bookmark: Bookmark = {
|
|
id: `${activeAccount.pubkey}-bookmarks`,
|
|
title: `Bookmarks (${allBookmarks.length})`,
|
|
url: '',
|
|
content: latestContent,
|
|
created_at: newestCreatedAt || Date.now(),
|
|
tags: allTags,
|
|
bookmarkCount: allBookmarks.length,
|
|
eventReferences: allTags.filter(tag => tag[0] === 'e').map(tag => tag[1]),
|
|
individualBookmarks: allBookmarks,
|
|
isPrivate: privateItemsAll.length > 0,
|
|
encryptedContent: undefined
|
|
}
|
|
|
|
setBookmarks([bookmark])
|
|
clearTimeout(timeoutId)
|
|
setLoading(false)
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch bookmarks:', error)
|
|
clearTimeout(timeoutId)
|
|
setLoading(false)
|
|
}
|
|
} |