mirror of
https://github.com/dergigi/boris.git
synced 2026-01-18 06:14:27 +01:00
170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { RelayPool } from 'applesauce-relay'
|
|
import { IEventStore, Helpers } from 'applesauce-core'
|
|
import { NostrEvent } from 'nostr-tools'
|
|
import { KINDS } from '../config/kinds'
|
|
import { queryEvents } from './dataFetch'
|
|
import { BlogPostPreview } from './exploreService'
|
|
|
|
const { getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished } = Helpers
|
|
|
|
type WritingsCallback = (posts: BlogPostPreview[]) => void
|
|
type LoadingCallback = (loading: boolean) => void
|
|
|
|
const LAST_SYNCED_KEY = 'nostrverse_writings_last_synced'
|
|
|
|
function toPreview(event: NostrEvent): BlogPostPreview {
|
|
return {
|
|
event,
|
|
title: getArticleTitle(event) || 'Untitled',
|
|
summary: getArticleSummary(event),
|
|
image: getArticleImage(event),
|
|
published: getArticlePublished(event),
|
|
author: event.pubkey
|
|
}
|
|
}
|
|
|
|
function sortPosts(posts: BlogPostPreview[]): BlogPostPreview[] {
|
|
return posts.slice().sort((a, b) => {
|
|
const timeA = a.published || a.event.created_at
|
|
const timeB = b.published || b.event.created_at
|
|
return timeB - timeA
|
|
})
|
|
}
|
|
|
|
class NostrverseWritingsController {
|
|
private writingsListeners: WritingsCallback[] = []
|
|
private loadingListeners: LoadingCallback[] = []
|
|
|
|
private currentPosts: BlogPostPreview[] = []
|
|
private loaded = false
|
|
private generation = 0
|
|
|
|
onWritings(cb: WritingsCallback): () => void {
|
|
this.writingsListeners.push(cb)
|
|
return () => {
|
|
this.writingsListeners = this.writingsListeners.filter(l => l !== cb)
|
|
}
|
|
}
|
|
|
|
onLoading(cb: LoadingCallback): () => void {
|
|
this.loadingListeners.push(cb)
|
|
return () => {
|
|
this.loadingListeners = this.loadingListeners.filter(l => l !== cb)
|
|
}
|
|
}
|
|
|
|
private setLoading(loading: boolean): void {
|
|
this.loadingListeners.forEach(cb => cb(loading))
|
|
}
|
|
|
|
private emitWritings(posts: BlogPostPreview[]): void {
|
|
this.writingsListeners.forEach(cb => cb(posts))
|
|
}
|
|
|
|
getWritings(): BlogPostPreview[] {
|
|
return [...this.currentPosts]
|
|
}
|
|
|
|
isLoaded(): boolean {
|
|
return this.loaded
|
|
}
|
|
|
|
private getLastSyncedAt(): number | null {
|
|
try {
|
|
const raw = localStorage.getItem(LAST_SYNCED_KEY)
|
|
if (!raw) return null
|
|
const parsed = JSON.parse(raw)
|
|
return typeof parsed?.ts === 'number' ? parsed.ts : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
private setLastSyncedAt(ts: number): void {
|
|
try { localStorage.setItem(LAST_SYNCED_KEY, JSON.stringify({ ts })) } catch { /* ignore */ }
|
|
}
|
|
|
|
async start(options: {
|
|
relayPool: RelayPool
|
|
eventStore: IEventStore
|
|
force?: boolean
|
|
}): Promise<void> {
|
|
const { relayPool, eventStore, force = false } = options
|
|
|
|
if (!force && this.loaded) {
|
|
this.emitWritings(this.currentPosts)
|
|
return
|
|
}
|
|
|
|
this.generation++
|
|
const currentGeneration = this.generation
|
|
this.setLoading(true)
|
|
|
|
try {
|
|
const seenIds = new Set<string>()
|
|
const uniqueByReplaceable = new Map<string, BlogPostPreview>()
|
|
|
|
const lastSyncedAt = force ? null : this.getLastSyncedAt()
|
|
const filter: { kinds: number[]; since?: number } = { kinds: [KINDS.BlogPost] }
|
|
if (lastSyncedAt) filter.since = lastSyncedAt
|
|
|
|
const events = await queryEvents(
|
|
relayPool,
|
|
filter,
|
|
{
|
|
onEvent: (evt) => {
|
|
if (currentGeneration !== this.generation) return
|
|
if (seenIds.has(evt.id)) return
|
|
seenIds.add(evt.id)
|
|
|
|
eventStore.add(evt)
|
|
|
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1] || ''
|
|
const key = `${evt.pubkey}:${dTag}`
|
|
const preview = toPreview(evt)
|
|
const existing = uniqueByReplaceable.get(key)
|
|
if (!existing || evt.created_at > existing.event.created_at) {
|
|
uniqueByReplaceable.set(key, preview)
|
|
const sorted = sortPosts(Array.from(uniqueByReplaceable.values()))
|
|
this.currentPosts = sorted
|
|
this.emitWritings(sorted)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
if (currentGeneration !== this.generation) return
|
|
|
|
events.forEach(evt => eventStore.add(evt))
|
|
|
|
events.forEach(evt => {
|
|
const dTag = evt.tags.find(t => t[0] === 'd')?.[1] || ''
|
|
const key = `${evt.pubkey}:${dTag}`
|
|
const existing = uniqueByReplaceable.get(key)
|
|
if (!existing || evt.created_at > existing.event.created_at) {
|
|
uniqueByReplaceable.set(key, toPreview(evt))
|
|
}
|
|
})
|
|
|
|
const sorted = sortPosts(Array.from(uniqueByReplaceable.values()))
|
|
this.currentPosts = sorted
|
|
this.loaded = true
|
|
this.emitWritings(sorted)
|
|
|
|
if (sorted.length > 0) {
|
|
const newest = Math.max(...sorted.map(p => p.event.created_at))
|
|
this.setLastSyncedAt(newest)
|
|
}
|
|
} catch {
|
|
this.currentPosts = []
|
|
this.emitWritings(this.currentPosts)
|
|
} finally {
|
|
if (currentGeneration === this.generation) this.setLoading(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const nostrverseWritingsController = new NostrverseWritingsController()
|
|
|
|
|