mirror of
https://github.com/dergigi/boris.git
synced 2025-12-29 20:44:37 +01:00
feat: add named kind constants, streaming updates, and fix reads/links tabs
- Create src/config/kinds.ts with named Nostr kind constants - Add streaming support to fetchAllReads and fetchLinks with onItem callbacks - Update all services to use KINDS constants instead of magic numbers - Add mergeReadItem utility for DRY state management - Add fallbackTitleFromUrl for external links without titles - Relax validation to allow external items without titles - Update Me.tsx to use streaming with Map-based state for reads/links - Fix refresh to merge new data instead of clearing state - Fix empty states for Reads and Links tabs (no more infinite skeletons) - Services updated: readsService, linksService, libraryService, bookmarkService, exploreService, highlights/fetchByAuthor
This commit is contained in:
74
src/utils/readItemMerge.ts
Normal file
74
src/utils/readItemMerge.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ReadItem } from '../services/readsService'
|
||||
|
||||
/**
|
||||
* Merges a ReadItem into a state map, returning whether the state changed.
|
||||
* Uses most recent reading activity to determine precedence.
|
||||
*/
|
||||
export function mergeReadItem(
|
||||
stateMap: Map<string, ReadItem>,
|
||||
incoming: ReadItem
|
||||
): boolean {
|
||||
const existing = stateMap.get(incoming.id)
|
||||
|
||||
if (!existing) {
|
||||
stateMap.set(incoming.id, incoming)
|
||||
return true
|
||||
}
|
||||
|
||||
// Merge by taking the most recent reading activity
|
||||
const existingTime = existing.readingTimestamp || existing.markedAt || 0
|
||||
const incomingTime = incoming.readingTimestamp || incoming.markedAt || 0
|
||||
|
||||
if (incomingTime > existingTime) {
|
||||
// Keep existing data, but update with newer reading metadata
|
||||
stateMap.set(incoming.id, {
|
||||
...existing,
|
||||
...incoming,
|
||||
// Preserve event data if incoming doesn't have it
|
||||
event: incoming.event || existing.event,
|
||||
title: incoming.title || existing.title,
|
||||
summary: incoming.summary || existing.summary,
|
||||
image: incoming.image || existing.image,
|
||||
published: incoming.published || existing.published,
|
||||
author: incoming.author || existing.author
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// If timestamps are equal but incoming has additional data, merge it
|
||||
if (incomingTime === existingTime && (!existing.event && incoming.event || !existing.title && incoming.title)) {
|
||||
stateMap.set(incoming.id, {
|
||||
...existing,
|
||||
...incoming,
|
||||
event: incoming.event || existing.event,
|
||||
title: incoming.title || existing.title,
|
||||
summary: incoming.summary || existing.summary,
|
||||
image: incoming.image || existing.image,
|
||||
published: incoming.published || existing.published,
|
||||
author: incoming.author || existing.author
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a readable title from a URL when no title is available.
|
||||
* Removes protocol, www, and shows domain + path.
|
||||
*/
|
||||
export function fallbackTitleFromUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
let title = parsed.hostname.replace(/^www\./, '')
|
||||
if (parsed.pathname && parsed.pathname !== '/') {
|
||||
const path = parsed.pathname.slice(0, 40)
|
||||
title += path.length < parsed.pathname.length ? path + '...' : path
|
||||
}
|
||||
return title
|
||||
} catch {
|
||||
// If URL parsing fails, just return the URL truncated
|
||||
return url.length > 50 ? url.slice(0, 47) + '...' : url
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user