mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 20:45:01 +01:00
- Add relayListService to load kind:10002 (user relay list) and kind:10006 (blocked relays) - Add relayManager to compute active relay set and dynamically manage pool membership - Update App.tsx to fetch and apply user relays on login, reset on logout - Replace all hardcoded RELAYS usages with dynamic getActiveRelayUrls() across services and components - Always preserve localhost relays (ws://localhost:10547, ws://localhost:4869) regardless of user blocks - Merge bunker relays, user relays, and hardcoded relays while excluding blocked relays - Update keep-alive subscription and address loaders to use dynamic relay set - Modified files: App.tsx, relayListService.ts (new), relayManager.ts (new), readsService.ts, readingProgressController.ts, archiveController.ts, libraryService.ts, reactionService.ts, writeService.ts, HighlightItem.tsx, ContentPanel.tsx, BookmarkList.tsx, Profile.tsx
153 lines
4.5 KiB
TypeScript
153 lines
4.5 KiB
TypeScript
import { RelayPool } from 'applesauce-relay'
|
|
import { NostrEvent } from 'nostr-tools'
|
|
import { Helpers } from 'applesauce-core'
|
|
import { KINDS } from '../config/kinds'
|
|
import { ARCHIVE_EMOJI } from './reactionService'
|
|
import { BlogPostPreview } from './exploreService'
|
|
import { queryEvents } from './dataFetch'
|
|
|
|
const { getArticleTitle, getArticleImage, getArticlePublished, getArticleSummary } = Helpers
|
|
|
|
export interface ReadArticle {
|
|
id: string
|
|
url?: string
|
|
eventId?: string
|
|
eventAuthor?: string
|
|
eventKind?: number
|
|
markedAt: number
|
|
reactionId: string
|
|
}
|
|
|
|
/**
|
|
* Fetches all articles that the user has marked as read
|
|
* Returns both nostr-native articles (kind:7) and external URLs (kind:17)
|
|
*/
|
|
export async function fetchReadArticles(
|
|
relayPool: RelayPool,
|
|
userPubkey: string
|
|
): Promise<ReadArticle[]> {
|
|
try {
|
|
// Fetch kind:7 and kind:17 reactions in parallel
|
|
const [kind7Events, kind17Events] = await Promise.all([
|
|
queryEvents(relayPool, { kinds: [KINDS.ReactionToEvent], authors: [userPubkey] }),
|
|
queryEvents(relayPool, { kinds: [KINDS.ReactionToUrl], authors: [userPubkey] })
|
|
])
|
|
|
|
const readArticles: ReadArticle[] = []
|
|
|
|
// Process kind:7 reactions (nostr-native articles)
|
|
for (const event of kind7Events) {
|
|
if (event.content === ARCHIVE_EMOJI) {
|
|
const eTag = event.tags.find((t) => t[0] === 'e')
|
|
const pTag = event.tags.find((t) => t[0] === 'p')
|
|
const kTag = event.tags.find((t) => t[0] === 'k')
|
|
|
|
if (eTag && eTag[1]) {
|
|
readArticles.push({
|
|
id: eTag[1],
|
|
eventId: eTag[1],
|
|
eventAuthor: pTag?.[1],
|
|
eventKind: kTag?.[1] ? parseInt(kTag[1]) : undefined,
|
|
markedAt: event.created_at,
|
|
reactionId: event.id
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process kind:17 reactions (external URLs)
|
|
for (const event of kind17Events) {
|
|
if (event.content === ARCHIVE_EMOJI) {
|
|
const rTag = event.tags.find((t) => t[0] === 'r')
|
|
|
|
if (rTag && rTag[1]) {
|
|
readArticles.push({
|
|
id: rTag[1],
|
|
url: rTag[1],
|
|
markedAt: event.created_at,
|
|
reactionId: event.id
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by markedAt (most recent first) and dedupe
|
|
const deduped = new Map<string, ReadArticle>()
|
|
readArticles
|
|
.sort((a, b) => b.markedAt - a.markedAt)
|
|
.forEach((article) => {
|
|
if (!deduped.has(article.id)) {
|
|
deduped.set(article.id, article)
|
|
}
|
|
})
|
|
|
|
return Array.from(deduped.values())
|
|
} catch (error) {
|
|
console.error('Failed to fetch read articles:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches full article data for read nostr-native articles
|
|
* and converts them to BlogPostPreview format for rendering
|
|
*/
|
|
export async function fetchReadArticlesWithData(
|
|
relayPool: RelayPool,
|
|
userPubkey: string
|
|
): Promise<BlogPostPreview[]> {
|
|
try {
|
|
// First get all read articles
|
|
const readArticles = await fetchReadArticles(relayPool, userPubkey)
|
|
|
|
// Filter to only nostr-native articles (kind 30023)
|
|
const nostrArticles = readArticles.filter(
|
|
article => article.eventKind === KINDS.BlogPost && article.eventId
|
|
)
|
|
|
|
if (nostrArticles.length === 0) {
|
|
return []
|
|
}
|
|
|
|
// Fetch the actual article events
|
|
const eventIds = nostrArticles.map(a => a.eventId!).filter(Boolean)
|
|
|
|
const articleEvents = await queryEvents(
|
|
relayPool,
|
|
{ kinds: [KINDS.BlogPost], ids: eventIds }
|
|
)
|
|
|
|
// Deduplicate article events by ID
|
|
const uniqueArticleEvents = new Map<string, NostrEvent>()
|
|
articleEvents.forEach(event => {
|
|
if (!uniqueArticleEvents.has(event.id)) {
|
|
uniqueArticleEvents.set(event.id, event)
|
|
}
|
|
})
|
|
|
|
// Convert to BlogPostPreview format
|
|
const blogPosts: BlogPostPreview[] = Array.from(uniqueArticleEvents.values()).map(event => ({
|
|
event,
|
|
title: getArticleTitle(event) || 'Untitled Article',
|
|
summary: getArticleSummary(event),
|
|
image: getArticleImage(event),
|
|
published: getArticlePublished(event),
|
|
author: event.pubkey
|
|
}))
|
|
|
|
// Sort by when they were marked as read (most recent first)
|
|
const articlesMap = new Map(nostrArticles.map(a => [a.eventId, a]))
|
|
blogPosts.sort((a, b) => {
|
|
const markedAtA = articlesMap.get(a.event.id)?.markedAt || 0
|
|
const markedAtB = articlesMap.get(b.event.id)?.markedAt || 0
|
|
return markedAtB - markedAtA
|
|
})
|
|
|
|
return blogPosts
|
|
} catch (error) {
|
|
console.error('Failed to fetch read articles with data:', error)
|
|
return []
|
|
}
|
|
}
|
|
|