diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 1f0278ff..8d92efe6 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -1355,15 +1355,14 @@ function parseMarkdownContent( // 4. There's text on the same line before it (part of a sentence) // 5. There's text before it (even on previous lines, as long as no paragraph break) shouldMergeHashtag = lineHasOnlyHashtags || hasOtherHashtagsOnLine || hasHashtagsOnAdjacentLines || hasTextOnSameLine || hasTextBefore - - // If none of the above, but there's text after the hashtag on the same line, also merge - // This handles cases where hashtag is at start of line but followed by text (e.g. "#pyramid 1.1 has...") - if (!shouldMergeHashtag) { - const textAfterOnSameLine = content.substring(pattern.end, lineEndIndex) - hasTextAfterOnSameLine = textAfterOnSameLine.trim().length > 0 - if (hasTextAfterOnSameLine) { - shouldMergeHashtag = true - } + + // Always compute — merge branch 2 below needs this even when shouldMergeHashtag was already + // true from hasOtherHashtagsOnLine (e.g. "#a #b word" is not "only hashtags" so branch 1 skips, + // and without hasTextAfterOnSameLine branch 2 would not run → spurious line break before

). + const textAfterOnSameLineRaw = content.substring(pattern.end, lineEndIndex) + hasTextAfterOnSameLine = textAfterOnSameLineRaw.trim().length > 0 + if (!shouldMergeHashtag && hasTextAfterOnSameLine) { + shouldMergeHashtag = true } } @@ -1483,23 +1482,21 @@ function parseMarkdownContent( // Mark this pattern as merged so we don't render it separately later mergedPatterns.add(patternIdx) - } else if (pattern.type === 'nostr' && (hasTextOnSameLine || hasTextBefore)) { - // Only merge profile types (npub/nprofile) inline; event types (note/nevent/naddr) remain block-level + } else if (pattern.type === 'nostr') { + // Only merge profile types (npub/nprofile) inline; event types (note/nevent/naddr) remain block-level. + // Same idea as hashtags: if the mention is first on the line but more text follows on that line, + // merge into the paragraph — otherwise we emit a bare and the rest in

, which looks + // like a spurious hard return (block

after inline-block mention). const bech32Id = pattern.data const isProfileType = bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile') - - if (isProfileType) { - // Get the original pattern syntax from the content + const hasTextAfterNostrOnSameLine = + isProfileType && content.substring(pattern.end, lineEndIndex).trim().length > 0 + + if (isProfileType && (hasTextOnSameLine || hasTextBefore || hasTextAfterNostrOnSameLine)) { const patternMarkdown = content.substring(pattern.index, pattern.end) - - // Get text after the pattern on the same line const textAfterPattern = content.substring(pattern.end, lineEndIndex) - - // Extend the text to include the pattern and any text after it on the same line text = text + patternMarkdown + textAfterPattern textEndIndex = lineEndIndex === content.length ? content.length : lineEndIndex + 1 - - // Mark this pattern as merged so we don't render it separately later mergedPatterns.add(patternIdx) } } @@ -2155,7 +2152,7 @@ function parseMarkdownContent( // Check if it's a profile type (mentions/handles should be inline) if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) { parts.push( - + ) diff --git a/src/components/ParentNotePreview/index.tsx b/src/components/ParentNotePreview/index.tsx index b7f017de..601bae2e 100644 --- a/src/components/ParentNotePreview/index.tsx +++ b/src/components/ParentNotePreview/index.tsx @@ -4,7 +4,7 @@ import { useFetchEvent } from '@/hooks' import { cn } from '@/lib/utils' import client from '@/services/client.service' import { useTranslation } from 'react-i18next' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Event, nip19 } from 'nostr-tools' import ContentPreview from '../ContentPreview' import UserAvatar from '../UserAvatar' @@ -23,6 +23,8 @@ export default function ParentNotePreview({ const { event, isFetching } = useFetchEvent(eventId) const [fallbackEvent, setFallbackEvent] = useState(undefined) const [isFetchingFallback, setIsFetchingFallback] = useState(false) + /** One automatic searchable-relay attempt per eventId; without this, the effect re-fires forever after each 20s timeout. */ + const autoSearchableAttemptedRef = useRef(false) // Helper function to decode event ID const getHexEventId = (id: string): string | null => { @@ -62,10 +64,22 @@ export default function ParentNotePreview({ } }, [eventId]) - // If the initial fetch fails, try fetching from searchable relays automatically useEffect(() => { - if (!isFetching && !event && !fallbackEvent && !isFetchingFallback && eventId) { - fetchFromSearchableRelays() + autoSearchableAttemptedRef.current = false + }, [eventId]) + + // If the initial fetch fails, try searchable relays once (manual retry still works via onClick). + useEffect(() => { + if ( + !isFetching && + !event && + !fallbackEvent && + !isFetchingFallback && + eventId && + !autoSearchableAttemptedRef.current + ) { + autoSearchableAttemptedRef.current = true + void fetchFromSearchableRelays() } }, [isFetching, event, eventId, fallbackEvent, isFetchingFallback, fetchFromSearchableRelays]) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index c39c6fb7..5242f5de 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -2720,7 +2720,7 @@ class ClientService extends EventTarget { return undefined } - logger.info('fetchEventWithExternalRelays: Starting search', { + logger.debug('fetchEventWithExternalRelays: Starting search', { eventId: eventId.substring(0, 8), relayCount: externalRelays.length, relays: externalRelays @@ -2741,7 +2741,7 @@ class ClientService extends EventTarget { ) const duration = Date.now() - startTime - logger.info('fetchEventWithExternalRelays: Search completed', { + logger.debug('fetchEventWithExternalRelays: Search completed', { eventId: eventId.substring(0, 8), relayCount: externalRelays.length, eventsFound: events.length,