mirror of
https://github.com/dergigi/boris.git
synced 2026-02-06 23:54:21 +01:00
feat(me): populate reads/links from bookmarks instantly
- Add deriveReadsFromBookmarks helper to convert 30023 bookmarks to ReadItems - Add deriveLinksFromBookmarks helper for web bookmarks (39701) and URLs - Update loadReadsTab to show bookmarked articles immediately, enrich in background - Update loadLinksTab to show bookmarked links immediately, enrich in background - Background enrichment merges reading progress only for displayed items - Preserve existing pull-to-refresh and empty state logic
This commit is contained in:
@@ -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<MeProps> = ({ 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<MeProps> = ({ 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)
|
||||
}
|
||||
}
|
||||
|
||||
69
src/utils/linksFromBookmarks.ts
Normal file
69
src/utils/linksFromBookmarks.ts
Normal file
@@ -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<string, ReadItem>()
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
47
src/utils/readsFromBookmarks.ts
Normal file
47
src/utils/readsFromBookmarks.ts
Normal file
@@ -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<string, ReadItem>()
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user