mirror of
https://github.com/dergigi/boris.git
synced 2026-02-17 04:54:56 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49c5f0c3ad | ||
|
|
dbed4ad253 | ||
|
|
b117b1e6cf | ||
|
|
627ffd6c5d |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.6",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
|
||||
@@ -18,93 +18,29 @@ const NostrMentionLink: React.FC<NostrMentionLinkProps> = ({
|
||||
onClick,
|
||||
className = 'highlight-comment-link'
|
||||
}) => {
|
||||
// Decode the nostr URI first
|
||||
let decoded: ReturnType<typeof nip19.decode> | null = null
|
||||
let pubkey: string | undefined
|
||||
|
||||
try {
|
||||
// Remove nostr: prefix
|
||||
const identifier = nostrUri.replace(/^nostr:/, '')
|
||||
const decoded = nip19.decode(identifier)
|
||||
decoded = nip19.decode(identifier)
|
||||
|
||||
switch (decoded.type) {
|
||||
case 'npub': {
|
||||
const pubkey = decoded.data
|
||||
// Fetch profile in the background
|
||||
const profile = useEventModel(Models.ProfileModel, [pubkey])
|
||||
const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pubkey.slice(0, 8)}...`
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/p/${nip19.npubEncode(pubkey)}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
@{displayName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
case 'nprofile': {
|
||||
const { pubkey } = decoded.data
|
||||
// Fetch profile in the background
|
||||
const profile = useEventModel(Models.ProfileModel, [pubkey])
|
||||
const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pubkey.slice(0, 8)}...`
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/p/${npub}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
@{displayName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
case 'naddr': {
|
||||
const { kind, pubkey, identifier: addrIdentifier } = decoded.data
|
||||
// Check if it's a blog post (kind:30023)
|
||||
if (kind === 30023) {
|
||||
const naddr = nip19.naddrEncode({ kind, pubkey, identifier: addrIdentifier })
|
||||
return (
|
||||
<a
|
||||
href={`/a/${naddr}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{addrIdentifier || 'Article'}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
// For other kinds, show shortened identifier
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
nostr:{addrIdentifier.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'note': {
|
||||
const eventId = decoded.data
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
note:{eventId.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'nevent': {
|
||||
const { id } = decoded.data
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
event:{id.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
default:
|
||||
// Fallback for unrecognized types
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
{identifier.slice(0, 20)}...
|
||||
</span>
|
||||
)
|
||||
// Extract pubkey for profile fetching (works for npub and nprofile)
|
||||
if (decoded.type === 'npub') {
|
||||
pubkey = decoded.data
|
||||
} else if (decoded.type === 'nprofile') {
|
||||
pubkey = decoded.data.pubkey
|
||||
}
|
||||
} catch (error) {
|
||||
// If decoding fails, show shortened identifier
|
||||
// Decoding failed, will fallback to shortened identifier
|
||||
}
|
||||
|
||||
// Fetch profile at top level (Rules of Hooks)
|
||||
const profile = useEventModel(Models.ProfileModel, pubkey ? [pubkey] : null)
|
||||
|
||||
// If decoding failed, show shortened identifier
|
||||
if (!decoded) {
|
||||
const identifier = nostrUri.replace(/^nostr:/, '')
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
@@ -112,6 +48,86 @@ const NostrMentionLink: React.FC<NostrMentionLinkProps> = ({
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Render based on decoded type
|
||||
switch (decoded.type) {
|
||||
case 'npub': {
|
||||
const pk = decoded.data
|
||||
const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pk.slice(0, 8)}...`
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/p/${nip19.npubEncode(pk)}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
@{displayName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
case 'nprofile': {
|
||||
const { pubkey: pk } = decoded.data
|
||||
const displayName = profile?.name || profile?.display_name || profile?.nip05 || `${pk.slice(0, 8)}...`
|
||||
const npub = nip19.npubEncode(pk)
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/p/${npub}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
@{displayName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
case 'naddr': {
|
||||
const { kind, pubkey: pk, identifier: addrIdentifier } = decoded.data
|
||||
// Check if it's a blog post (kind:30023)
|
||||
if (kind === 30023) {
|
||||
const naddr = nip19.naddrEncode({ kind, pubkey: pk, identifier: addrIdentifier })
|
||||
return (
|
||||
<a
|
||||
href={`/a/${naddr}`}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{addrIdentifier || 'Article'}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
// For other kinds, show shortened identifier
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
nostr:{addrIdentifier.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'note': {
|
||||
const eventId = decoded.data
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
note:{eventId.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'nevent': {
|
||||
const { id } = decoded.data
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
event:{id.slice(0, 12)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
// Fallback for unrecognized types
|
||||
const identifier = nostrUri.replace(/^nostr:/, '')
|
||||
return (
|
||||
<span className="highlight-comment-nostr-id">
|
||||
{identifier.slice(0, 20)}...
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrMentionLink
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, Dispatch, SetStateAction } from 'react'
|
||||
import { useEffect, useRef, Dispatch, SetStateAction } from 'react'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { fetchArticleByNaddr } from '../services/articleService'
|
||||
import { fetchHighlightsForArticle } from '../services/highlightService'
|
||||
@@ -6,7 +6,6 @@ import { ReadableContent } from '../services/readerService'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
import { useMountedState } from './useMountedState'
|
||||
|
||||
interface UseArticleLoaderProps {
|
||||
naddr: string | undefined
|
||||
@@ -37,13 +36,15 @@ export function useArticleLoader({
|
||||
setCurrentArticle,
|
||||
settings
|
||||
}: UseArticleLoaderProps) {
|
||||
const isMounted = useMountedState()
|
||||
const mountedRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
|
||||
if (!relayPool || !naddr) return
|
||||
|
||||
const loadArticle = async () => {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setReaderLoading(true)
|
||||
setReaderContent(undefined)
|
||||
@@ -53,7 +54,7 @@ export function useArticleLoader({
|
||||
try {
|
||||
const article = await fetchArticleByNaddr(relayPool, naddr, false, settings)
|
||||
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setReaderContent({
|
||||
title: article.title,
|
||||
@@ -74,7 +75,7 @@ export function useArticleLoader({
|
||||
|
||||
// Fetch highlights asynchronously without blocking article display
|
||||
try {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setHighlightsLoading(true)
|
||||
setHighlights([])
|
||||
@@ -84,7 +85,7 @@ export function useArticleLoader({
|
||||
articleCoordinate,
|
||||
article.event.id,
|
||||
(highlight) => {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setHighlights((prev: Highlight[]) => {
|
||||
if (prev.some((h: Highlight) => h.id === highlight.id)) return prev
|
||||
@@ -97,13 +98,13 @@ export function useArticleLoader({
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch highlights:', err)
|
||||
} finally {
|
||||
if (isMounted()) {
|
||||
if (mountedRef.current) {
|
||||
setHighlightsLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load article:', err)
|
||||
if (isMounted()) {
|
||||
if (mountedRef.current) {
|
||||
setReaderContent({
|
||||
title: 'Error Loading Article',
|
||||
html: `<p>Failed to load article: ${err instanceof Error ? err.message : 'Unknown error'}</p>`,
|
||||
@@ -115,7 +116,11 @@ export function useArticleLoader({
|
||||
}
|
||||
|
||||
loadArticle()
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false
|
||||
}
|
||||
// Intentionally excluding setter functions from dependencies to prevent race conditions
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [naddr, relayPool, settings, isMounted])
|
||||
}, [naddr, relayPool, settings])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect, useRef, useMemo } from 'react'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { IEventStore } from 'applesauce-core'
|
||||
import { fetchReadableContent, ReadableContent } from '../services/readerService'
|
||||
@@ -7,7 +7,6 @@ import { Highlight } from '../types/highlights'
|
||||
import { useStoreTimeline } from './useStoreTimeline'
|
||||
import { eventToHighlight } from '../services/highlightEventProcessor'
|
||||
import { KINDS } from '../config/kinds'
|
||||
import { useMountedState } from './useMountedState'
|
||||
|
||||
// Helper to extract filename from URL
|
||||
function getFilenameFromUrl(url: string): string {
|
||||
@@ -49,7 +48,7 @@ export function useExternalUrlLoader({
|
||||
setCurrentArticleCoordinate,
|
||||
setCurrentArticleEventId
|
||||
}: UseExternalUrlLoaderProps) {
|
||||
const isMounted = useMountedState()
|
||||
const mountedRef = useRef(true)
|
||||
|
||||
// Load cached URL-specific highlights from event store
|
||||
const urlFilter = useMemo(() => {
|
||||
@@ -66,10 +65,12 @@ export function useExternalUrlLoader({
|
||||
|
||||
// Load content and start streaming highlights when URL changes
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
|
||||
if (!relayPool || !url) return
|
||||
|
||||
const loadExternalUrl = async () => {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setReaderLoading(true)
|
||||
setReaderContent(undefined)
|
||||
@@ -81,14 +82,14 @@ export function useExternalUrlLoader({
|
||||
try {
|
||||
const content = await fetchReadableContent(url)
|
||||
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setReaderContent(content)
|
||||
setReaderLoading(false)
|
||||
|
||||
// Fetch highlights for this URL asynchronously
|
||||
try {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
setHighlightsLoading(true)
|
||||
|
||||
@@ -112,7 +113,7 @@ export function useExternalUrlLoader({
|
||||
relayPool,
|
||||
url,
|
||||
(highlight) => {
|
||||
if (!isMounted()) return
|
||||
if (!mountedRef.current) return
|
||||
|
||||
if (seen.has(highlight.id)) return
|
||||
seen.add(highlight.id)
|
||||
@@ -130,13 +131,13 @@ export function useExternalUrlLoader({
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch highlights:', err)
|
||||
} finally {
|
||||
if (isMounted()) {
|
||||
if (mountedRef.current) {
|
||||
setHighlightsLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load external URL:', err)
|
||||
if (isMounted()) {
|
||||
if (mountedRef.current) {
|
||||
const filename = getFilenameFromUrl(url)
|
||||
setReaderContent({
|
||||
title: filename,
|
||||
@@ -149,9 +150,13 @@ export function useExternalUrlLoader({
|
||||
}
|
||||
|
||||
loadExternalUrl()
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false
|
||||
}
|
||||
// Intentionally excluding setter functions from dependencies to prevent race conditions
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url, relayPool, eventStore, cachedUrlHighlights, isMounted])
|
||||
}, [url, relayPool, eventStore, cachedUrlHighlights])
|
||||
|
||||
// Keep UI highlights synced with cached store updates without reloading content
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user