mirror of
https://github.com/dergigi/boris.git
synced 2026-01-17 22:04:32 +01:00
- Remove console.log statements from ContentPanel.tsx (archive/content related) - Remove console.log statements from readingProgressController.ts (reading progress related) - Remove console.log statements from reactionService.ts (reaction related) - Remove debug console.log block from Me.tsx (archive/me related) - Preserve all relay-related console.log statements in App.tsx and relayManager.ts
198 lines
6.5 KiB
TypeScript
198 lines
6.5 KiB
TypeScript
import { RelayPool } from 'applesauce-relay'
|
|
import { IEventStore } from 'applesauce-core'
|
|
import { NostrEvent } from 'nostr-tools'
|
|
import { queryEvents } from './dataFetch'
|
|
import { KINDS } from '../config/kinds'
|
|
import { ARCHIVE_EMOJI } from './reactionService'
|
|
import { nip19 } from 'nostr-tools'
|
|
|
|
type MarkedChangeCallback = (markedIds: Set<string>) => void
|
|
|
|
class ArchiveController {
|
|
private markedIds: Set<string> = new Set()
|
|
private lastLoadedPubkey: string | null = null
|
|
private listeners: MarkedChangeCallback[] = []
|
|
private generation = 0
|
|
private timelineSubscription: { unsubscribe: () => void } | null = null
|
|
private pendingEventIds: Set<string> = new Set()
|
|
|
|
onMarked(cb: MarkedChangeCallback): () => void {
|
|
this.listeners.push(cb)
|
|
// Emit current state immediately to new subscribers
|
|
cb(new Set(this.markedIds))
|
|
return () => {
|
|
this.listeners = this.listeners.filter(l => l !== cb)
|
|
}
|
|
}
|
|
|
|
private emit(): void {
|
|
const snapshot = new Set(this.markedIds)
|
|
this.listeners.forEach(cb => cb(snapshot))
|
|
}
|
|
|
|
mark(id: string): void {
|
|
if (!this.markedIds.has(id)) {
|
|
this.markedIds.add(id)
|
|
this.emit()
|
|
}
|
|
}
|
|
|
|
unmark(id: string): void {
|
|
if (this.markedIds.delete(id)) {
|
|
this.emit()
|
|
}
|
|
}
|
|
|
|
isMarked(id: string): boolean {
|
|
return this.markedIds.has(id)
|
|
}
|
|
|
|
getMarkedIds(): string[] {
|
|
return Array.from(this.markedIds)
|
|
}
|
|
|
|
isLoadedFor(pubkey: string): boolean {
|
|
return this.lastLoadedPubkey === pubkey
|
|
}
|
|
|
|
reset(): void {
|
|
this.generation++
|
|
if (this.timelineSubscription) {
|
|
try { this.timelineSubscription.unsubscribe() } catch { /* ignore */ }
|
|
this.timelineSubscription = null
|
|
}
|
|
this.markedIds = new Set()
|
|
this.pendingEventIds = new Set()
|
|
this.lastLoadedPubkey = null
|
|
this.emit()
|
|
}
|
|
|
|
async start(options: {
|
|
relayPool: RelayPool
|
|
eventStore: IEventStore
|
|
pubkey: string
|
|
force?: boolean
|
|
}): Promise<void> {
|
|
const { relayPool, eventStore, pubkey, force = false } = options
|
|
const startGen = this.generation
|
|
|
|
if (!force && this.isLoadedFor(pubkey)) {
|
|
return
|
|
}
|
|
|
|
// Mark as loaded immediately (fetch runs non-blocking)
|
|
this.lastLoadedPubkey = pubkey
|
|
|
|
// Handlers for streaming queries
|
|
const handleUrlReaction = (evt: NostrEvent) => {
|
|
if (evt.content !== ARCHIVE_EMOJI) return
|
|
const rTag = evt.tags.find(t => t[0] === 'r')?.[1]
|
|
if (!rTag) return
|
|
this.markedIds.add(rTag)
|
|
this.emit()
|
|
}
|
|
|
|
const handleEventReaction = (evt: NostrEvent) => {
|
|
if (evt.content !== ARCHIVE_EMOJI) return
|
|
// Direct coordinate tag ('a') - can be mapped immediately
|
|
const aTag = evt.tags.find(t => t[0] === 'a')?.[1]
|
|
if (aTag) {
|
|
try {
|
|
const [kindStr, pubkey, identifier] = aTag.split(':')
|
|
const kind = Number(kindStr)
|
|
if (kind === KINDS.BlogPost && pubkey && identifier) {
|
|
const naddr = nip19.naddrEncode({ kind, pubkey, identifier })
|
|
this.markedIds.add(naddr)
|
|
this.emit()
|
|
return
|
|
}
|
|
} catch { /* ignore malformed a-tag */ }
|
|
}
|
|
const eTag = evt.tags.find(t => t[0] === 'e')?.[1]
|
|
if (!eTag) return
|
|
this.pendingEventIds.add(eTag)
|
|
}
|
|
|
|
try {
|
|
// Stream kind:17 and kind:7 in parallel
|
|
const [kind17, kind7] = await Promise.all([
|
|
queryEvents(relayPool, { kinds: [17], authors: [pubkey] }, { onEvent: handleUrlReaction }),
|
|
queryEvents(relayPool, { kinds: [7], authors: [pubkey] }, { onEvent: handleEventReaction })
|
|
])
|
|
|
|
if (startGen !== this.generation) return
|
|
|
|
// Include EOSE events
|
|
kind17.forEach(handleUrlReaction)
|
|
kind7.forEach(handleEventReaction)
|
|
|
|
if (this.pendingEventIds.size > 0) {
|
|
// Fetch referenced articles (kind:30023) and map event IDs to naddr
|
|
const ids = Array.from(this.pendingEventIds)
|
|
const articleEvents = await queryEvents(relayPool, { kinds: [KINDS.BlogPost], ids })
|
|
for (const article of articleEvents) {
|
|
const dTag = article.tags.find(t => t[0] === 'd')?.[1]
|
|
if (!dTag) continue
|
|
try {
|
|
const naddr = nip19.naddrEncode({ kind: KINDS.BlogPost, pubkey: article.pubkey, identifier: dTag })
|
|
this.markedIds.add(naddr)
|
|
} catch {
|
|
// skip invalid
|
|
}
|
|
}
|
|
this.emit()
|
|
}
|
|
|
|
// Try immediate mapping via eventStore for any still-pending e-ids
|
|
if (this.pendingEventIds.size > 0) {
|
|
const stillPending = new Set<string>()
|
|
for (const eId of this.pendingEventIds) {
|
|
try {
|
|
const store = eventStore as unknown as { getEvent?: (id: string) => NostrEvent | undefined }
|
|
const evt: NostrEvent | undefined = typeof store.getEvent === 'function' ? store.getEvent(eId) : undefined
|
|
if (evt && evt.kind === KINDS.BlogPost) {
|
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1]
|
|
if (dTag) {
|
|
const naddr = nip19.naddrEncode({ kind: KINDS.BlogPost, pubkey: evt.pubkey, identifier: dTag })
|
|
this.markedIds.add(naddr)
|
|
}
|
|
} else {
|
|
stillPending.add(eId)
|
|
}
|
|
} catch (e) { stillPending.add(eId) }
|
|
}
|
|
this.pendingEventIds = stillPending
|
|
if (stillPending.size > 0) {
|
|
// Subscribe to future 30023 arrivals to finalize mapping
|
|
if (this.timelineSubscription) {
|
|
try { this.timelineSubscription.unsubscribe() } catch { /* ignore */ }
|
|
this.timelineSubscription = null
|
|
}
|
|
const sub$ = eventStore.timeline({ kinds: [KINDS.BlogPost] })
|
|
const genAtSub = this.generation
|
|
this.timelineSubscription = sub$.subscribe((events: NostrEvent[]) => {
|
|
if (genAtSub !== this.generation) return
|
|
for (const evt of events) {
|
|
if (!this.pendingEventIds.has(evt.id)) continue
|
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1]
|
|
if (!dTag) continue
|
|
try {
|
|
const naddr = nip19.naddrEncode({ kind: KINDS.BlogPost, pubkey: evt.pubkey, identifier: dTag })
|
|
this.markedIds.add(naddr)
|
|
this.pendingEventIds.delete(evt.id)
|
|
this.emit()
|
|
} catch { /* ignore */ }
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Non-blocking fetch; ignore errors here
|
|
}
|
|
}
|
|
}
|
|
|
|
export const archiveController = new ArchiveController()
|
|
|
|
|