diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 91f54cdd..52d5acc9 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -29,6 +29,9 @@ import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters' import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier' import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters' import { filterByReadingProgress } from '../utils/readingProgressUtils' +import { deriveReadsFromBookmarks } from '../utils/readsFromBookmarks' +import { deriveLinksFromBookmarks } from '../utils/linksFromBookmarks' +import { mergeReadItem } from '../utils/readItemMerge' interface MeProps { relayPool: RelayPool @@ -134,30 +137,39 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr try { if (!hasBeenLoaded) setLoading(true) - // Fetch bookmarks first (needed for reads) - let fetchedBookmarks: Bookmark[] = [] - try { - await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { - fetchedBookmarks = newBookmarks - setBookmarks(newBookmarks) - }) - } catch (err) { - console.warn('Failed to load bookmarks:', err) - fetchedBookmarks = [] + // Ensure bookmarks are loaded + let fetchedBookmarks: Bookmark[] = bookmarks + if (bookmarks.length === 0) { + try { + await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { + fetchedBookmarks = newBookmarks + setBookmarks(newBookmarks) + }) + } catch (err) { + console.warn('Failed to load bookmarks:', err) + fetchedBookmarks = [] + } } - // Fetch all reads with streaming - const tempMap = new Map(readsMap) - await fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks, (item) => { - tempMap.set(item.id, item) - setReadsMap(new Map(tempMap)) - setReads(Array.from(tempMap.values())) - }) - + // Derive reads from bookmarks immediately + const initialReads = deriveReadsFromBookmarks(fetchedBookmarks) + const tempMap = new Map(initialReads.map(item => [item.id, item])) + setReadsMap(tempMap) + setReads(initialReads) setLoadedTabs(prev => new Set(prev).add('reads')) + if (!hasBeenLoaded) setLoading(false) + + // Background enrichment: merge reading progress and mark-as-read + // Only update items that are already in our map + fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks, (item) => { + if (tempMap.has(item.id) && mergeReadItem(tempMap, item)) { + setReadsMap(new Map(tempMap)) + setReads(Array.from(tempMap.values())) + } + }).catch(err => console.warn('Failed to enrich reads:', err)) + } catch (err) { console.error('Failed to load reads:', err) - } finally { if (!hasBeenLoaded) setLoading(false) } } @@ -170,18 +182,39 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr try { if (!hasBeenLoaded) setLoading(true) - // Fetch links with streaming - const tempMap = new Map(linksMap) - await fetchLinks(relayPool, viewingPubkey, (item) => { - tempMap.set(item.id, item) - setLinksMap(new Map(tempMap)) - setLinks(Array.from(tempMap.values())) - }) - + // Ensure bookmarks are loaded + let fetchedBookmarks: Bookmark[] = bookmarks + if (bookmarks.length === 0) { + try { + await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => { + fetchedBookmarks = newBookmarks + setBookmarks(newBookmarks) + }) + } catch (err) { + console.warn('Failed to load bookmarks:', err) + fetchedBookmarks = [] + } + } + + // Derive links from bookmarks immediately + const initialLinks = deriveLinksFromBookmarks(fetchedBookmarks) + const tempMap = new Map(initialLinks.map(item => [item.id, item])) + setLinksMap(tempMap) + setLinks(initialLinks) setLoadedTabs(prev => new Set(prev).add('links')) + if (!hasBeenLoaded) setLoading(false) + + // Background enrichment: merge reading progress and mark-as-read + // Only update items that are already in our map + fetchLinks(relayPool, viewingPubkey, (item) => { + if (tempMap.has(item.id) && mergeReadItem(tempMap, item)) { + setLinksMap(new Map(tempMap)) + setLinks(Array.from(tempMap.values())) + } + }).catch(err => console.warn('Failed to enrich links:', err)) + } catch (err) { console.error('Failed to load links:', err) - } finally { if (!hasBeenLoaded) setLoading(false) } } diff --git a/src/utils/linksFromBookmarks.ts b/src/utils/linksFromBookmarks.ts new file mode 100644 index 00000000..156f5794 --- /dev/null +++ b/src/utils/linksFromBookmarks.ts @@ -0,0 +1,69 @@ +import { Bookmark } from '../types/bookmarks' +import { ReadItem } from '../services/readsService' +import { KINDS } from '../config/kinds' +import { fallbackTitleFromUrl } from './readItemMerge' + +/** + * Derives ReadItems from bookmarks for external URLs: + * - Web bookmarks (kind:39701) + * - Any bookmark with http(s) URLs in content or urlReferences + */ +export function deriveLinksFromBookmarks(bookmarks: Bookmark[]): ReadItem[] { + const linksMap = new Map() + + const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) + + for (const bookmark of allBookmarks) { + const urls: string[] = [] + + // Web bookmarks (kind:39701) - extract from 'd' tag + if (bookmark.kind === KINDS.WebBookmark) { + const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] + if (dTag) { + const url = dTag.startsWith('http') ? dTag : `https://${dTag}` + urls.push(url) + } + } + + // Extract URLs from urlReferences (pre-extracted by bookmarkService) + if (bookmark.urlReferences && bookmark.urlReferences.length > 0) { + urls.push(...bookmark.urlReferences) + } + + // Extract URLs from content if not already captured + if (bookmark.content) { + const urlRegex = /(https?:\/\/[^\s]+)/g + const matches = bookmark.content.match(urlRegex) + if (matches) { + urls.push(...matches) + } + } + + // Create ReadItem for each unique URL + for (const url of [...new Set(urls)]) { + if (!linksMap.has(url)) { + const item: ReadItem = { + id: url, + source: 'bookmark', + type: 'external', + url, + title: bookmark.title || fallbackTitleFromUrl(url), + summary: bookmark.summary, + image: bookmark.image, + readingProgress: 0, + readingTimestamp: bookmark.added_at || bookmark.created_at + } + + linksMap.set(url, item) + } + } + } + + // Sort by most recent bookmark activity + return Array.from(linksMap.values()).sort((a, b) => { + const timeA = a.readingTimestamp || 0 + const timeB = b.readingTimestamp || 0 + return timeB - timeA + }) +} + diff --git a/src/utils/readsFromBookmarks.ts b/src/utils/readsFromBookmarks.ts new file mode 100644 index 00000000..033ef479 --- /dev/null +++ b/src/utils/readsFromBookmarks.ts @@ -0,0 +1,47 @@ +import { Bookmark, IndividualBookmark } from '../types/bookmarks' +import { ReadItem } from '../services/readsService' +import { classifyBookmarkType } from './bookmarkTypeClassifier' +import { KINDS } from '../config/kinds' + +/** + * Derives ReadItems from bookmarks for Nostr articles (kind:30023). + * Returns items with type='article', using hydrated event data when available. + */ +export function deriveReadsFromBookmarks(bookmarks: Bookmark[]): ReadItem[] { + const readsMap = new Map() + + const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) + + for (const bookmark of allBookmarks) { + const bookmarkType = classifyBookmarkType(bookmark) + + // Only include articles (kind:30023) + if (bookmarkType === 'article' && bookmark.kind === KINDS.BlogPost) { + const coordinate = bookmark.id // Already in coordinate format + + const item: ReadItem = { + id: coordinate, + source: 'bookmark', + type: 'article', + readingProgress: 0, + readingTimestamp: bookmark.added_at || bookmark.created_at, + event: bookmark.event, + title: bookmark.title, + summary: bookmark.summary, + image: bookmark.image, + author: bookmark.pubkey, + url: bookmark.url + } + + readsMap.set(coordinate, item) + } + } + + // Sort by most recent bookmark activity + return Array.from(readsMap.values()).sort((a, b) => { + const timeA = a.readingTimestamp || 0 + const timeB = b.readingTimestamp || 0 + return timeB - timeA + }) +} +