Browse Source

fix markdown for hashtags and nostr addresses

end search loop
imwald
Silberengel 1 month ago
parent
commit
d9558982e5
  1. 39
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  2. 22
      src/components/ParentNotePreview/index.tsx
  3. 4
      src/services/client.service.ts

39
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) // 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) // 5. There's text before it (even on previous lines, as long as no paragraph break)
shouldMergeHashtag = lineHasOnlyHashtags || hasOtherHashtagsOnLine || hasHashtagsOnAdjacentLines || hasTextOnSameLine || hasTextBefore shouldMergeHashtag = lineHasOnlyHashtags || hasOtherHashtagsOnLine || hasHashtagsOnAdjacentLines || hasTextOnSameLine || hasTextBefore
// If none of the above, but there's text after the hashtag on the same line, also merge // Always compute — merge branch 2 below needs this even when shouldMergeHashtag was already
// This handles cases where hashtag is at start of line but followed by text (e.g. "#pyramid 1.1 has...") // true from hasOtherHashtagsOnLine (e.g. "#a #b word" is not "only hashtags" so branch 1 skips,
if (!shouldMergeHashtag) { // and without hasTextAfterOnSameLine branch 2 would not run → spurious line break before <p>).
const textAfterOnSameLine = content.substring(pattern.end, lineEndIndex) const textAfterOnSameLineRaw = content.substring(pattern.end, lineEndIndex)
hasTextAfterOnSameLine = textAfterOnSameLine.trim().length > 0 hasTextAfterOnSameLine = textAfterOnSameLineRaw.trim().length > 0
if (hasTextAfterOnSameLine) { if (!shouldMergeHashtag && hasTextAfterOnSameLine) {
shouldMergeHashtag = true shouldMergeHashtag = true
}
} }
} }
@ -1483,23 +1482,21 @@ function parseMarkdownContent(
// Mark this pattern as merged so we don't render it separately later // Mark this pattern as merged so we don't render it separately later
mergedPatterns.add(patternIdx) mergedPatterns.add(patternIdx)
} else if (pattern.type === 'nostr' && (hasTextOnSameLine || hasTextBefore)) { } else if (pattern.type === 'nostr') {
// Only merge profile types (npub/nprofile) inline; event types (note/nevent/naddr) remain block-level // 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 <span> and the rest in <p>, which looks
// like a spurious hard return (block <p> after inline-block mention).
const bech32Id = pattern.data const bech32Id = pattern.data
const isProfileType = bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile') const isProfileType = bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')
const hasTextAfterNostrOnSameLine =
if (isProfileType) { isProfileType && content.substring(pattern.end, lineEndIndex).trim().length > 0
// Get the original pattern syntax from the content
if (isProfileType && (hasTextOnSameLine || hasTextBefore || hasTextAfterNostrOnSameLine)) {
const patternMarkdown = content.substring(pattern.index, pattern.end) const patternMarkdown = content.substring(pattern.index, pattern.end)
// Get text after the pattern on the same line
const textAfterPattern = content.substring(pattern.end, lineEndIndex) 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 text = text + patternMarkdown + textAfterPattern
textEndIndex = lineEndIndex === content.length ? content.length : lineEndIndex + 1 textEndIndex = lineEndIndex === content.length ? content.length : lineEndIndex + 1
// Mark this pattern as merged so we don't render it separately later
mergedPatterns.add(patternIdx) mergedPatterns.add(patternIdx)
} }
} }
@ -2155,7 +2152,7 @@ function parseMarkdownContent(
// Check if it's a profile type (mentions/handles should be inline) // Check if it's a profile type (mentions/handles should be inline)
if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) { if (bech32Id.startsWith('npub') || bech32Id.startsWith('nprofile')) {
parts.push( parts.push(
<span key={`nostr-${patternIdx}`} className="inline-block"> <span key={`nostr-${patternIdx}`} className="inline">
<EmbeddedMention userId={bech32Id} /> <EmbeddedMention userId={bech32Id} />
</span> </span>
) )

22
src/components/ParentNotePreview/index.tsx

@ -4,7 +4,7 @@ import { useFetchEvent } from '@/hooks'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import client from '@/services/client.service' import client from '@/services/client.service'
import { useTranslation } from 'react-i18next' 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 { Event, nip19 } from 'nostr-tools'
import ContentPreview from '../ContentPreview' import ContentPreview from '../ContentPreview'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
@ -23,6 +23,8 @@ export default function ParentNotePreview({
const { event, isFetching } = useFetchEvent(eventId) const { event, isFetching } = useFetchEvent(eventId)
const [fallbackEvent, setFallbackEvent] = useState<Event | undefined>(undefined) const [fallbackEvent, setFallbackEvent] = useState<Event | undefined>(undefined)
const [isFetchingFallback, setIsFetchingFallback] = useState(false) 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 // Helper function to decode event ID
const getHexEventId = (id: string): string | null => { const getHexEventId = (id: string): string | null => {
@ -62,10 +64,22 @@ export default function ParentNotePreview({
} }
}, [eventId]) }, [eventId])
// If the initial fetch fails, try fetching from searchable relays automatically
useEffect(() => { useEffect(() => {
if (!isFetching && !event && !fallbackEvent && !isFetchingFallback && eventId) { autoSearchableAttemptedRef.current = false
fetchFromSearchableRelays() }, [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]) }, [isFetching, event, eventId, fallbackEvent, isFetchingFallback, fetchFromSearchableRelays])

4
src/services/client.service.ts

@ -2720,7 +2720,7 @@ class ClientService extends EventTarget {
return undefined return undefined
} }
logger.info('fetchEventWithExternalRelays: Starting search', { logger.debug('fetchEventWithExternalRelays: Starting search', {
eventId: eventId.substring(0, 8), eventId: eventId.substring(0, 8),
relayCount: externalRelays.length, relayCount: externalRelays.length,
relays: externalRelays relays: externalRelays
@ -2741,7 +2741,7 @@ class ClientService extends EventTarget {
) )
const duration = Date.now() - startTime const duration = Date.now() - startTime
logger.info('fetchEventWithExternalRelays: Search completed', { logger.debug('fetchEventWithExternalRelays: Search completed', {
eventId: eventId.substring(0, 8), eventId: eventId.substring(0, 8),
relayCount: externalRelays.length, relayCount: externalRelays.length,
eventsFound: events.length, eventsFound: events.length,

Loading…
Cancel
Save