Compare commits

...

4 Commits

Author SHA1 Message Date
Gigi
49c5f0c3ad chore: bump version to 0.8.6 2025-10-20 18:07:24 +02:00
Gigi
dbed4ad253 fix: revert to inline mount tracking with useRef
- Replace useMountedState custom hook with inline useRef approach
- Set mountedRef.current = true at start of each effect run
- Ensures proper reset when navigating between articles
- Simpler and more reliable than custom hook approach
2025-10-20 18:05:02 +02:00
Gigi
b117b1e6cf fix: remove isMounted from useEffect dependencies
- isMounted is a stable function from useMountedState and shouldn't be in deps
- Including it was preventing effects from running correctly
- Fixes issue where articles wouldn't load (stuck on spinner)
2025-10-20 17:46:41 +02:00
Gigi
627ffd6c5d fix: resolve React Hooks violation in NostrMentionLink component
- Move useEventModel hook call to top level (Rules of Hooks)
- Extract pubkey before calling the hook
- Profile resolution now works correctly for npub and nprofile mentions
- Fixes issue where profiles weren't being fetched and displayed
2025-10-20 16:36:52 +02:00
4 changed files with 129 additions and 103 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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])
}

View File

@@ -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(() => {