Browse Source

more bug-fixes

imwald
Silberengel 5 months ago
parent
commit
255ec55cc6
  1. 2
      src/components/Embedded/EmbeddedNote.tsx
  2. 47
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  3. 95
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  4. 2
      src/components/Note/index.tsx
  5. 14
      src/components/NoteCard/MainNoteCard.tsx
  6. 2
      src/components/ParentNotePreview/index.tsx
  7. 2
      src/components/PostEditor/index.tsx
  8. 9
      src/components/RelayInfo/RelayReviewCard.tsx
  9. 10
      src/components/ReplyNote/index.tsx
  10. 30
      src/components/ReplyNoteList/index.tsx
  11. 4
      src/lib/nostr-parser.tsx
  12. 9
      src/services/relay-selection.service.ts

2
src/components/Embedded/EmbeddedNote.tsx

@ -51,7 +51,7 @@ export function EmbeddedNote({ noteId, className }: { noteId: string; className?
} }
return ( return (
<div data-embedded-note> <div data-embedded-note onClick={(e) => e.stopPropagation()}>
<MainNoteCard <MainNoteCard
className={cn('w-full', className)} className={cn('w-full', className)}
event={finalEvent} event={finalEvent}

47
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -383,27 +383,34 @@ export default function AsciidocArticle({
</div> </div>
)} )}
{/* Hashtags */} {/* Hashtags - only show t-tags that don't appear as #hashtag in content */}
{parsedContent?.hashtags?.length > 0 && ( {(() => {
<div className="p-4 bg-muted rounded-lg"> // Get content hashtags from parsedContent (hashtags extracted from content as #hashtag)
<h4 className="text-sm font-semibold mb-3">Tags:</h4> // Normalize to lowercase for comparison
<div className="flex gap-2 flex-wrap"> const contentHashtags = new Set((parsedContent?.hashtags || []).map(t => t.toLowerCase()))
{parsedContent?.hashtags?.map((tag) => ( // Filter metadata.tags (t-tags from event) to exclude those already in content
<div const tagsToShow = (metadata.tags || []).filter(tag => !contentHashtags.has(tag.toLowerCase()))
key={tag} return tagsToShow.length > 0 && (
title={tag} <div className="p-4 bg-muted rounded-lg">
className="flex items-center rounded-full px-3 py-1 bg-background text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors" <h4 className="text-sm font-semibold mb-3">Tags:</h4>
onClick={(e) => { <div className="flex gap-2 flex-wrap">
e.stopPropagation() {tagsToShow.map((tag) => (
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] })) <div
}} key={tag}
> title={tag}
#<span className="truncate">{tag}</span> className="flex items-center rounded-full px-3 py-1 bg-background text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors"
</div> onClick={(e) => {
))} e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
</div>
</div> </div>
</div> )
)} })()}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
)} )}

95
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -37,6 +37,18 @@ export default function MarkdownArticle({
const allImages = useMemo(() => extractAllImagesFromEvent(event), [event]) const allImages = useMemo(() => extractAllImagesFromEvent(event), [event])
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
// Extract hashtags that are actually present in the content (as literal #hashtag)
// This ensures we only render green links for hashtags that are in the content, not from t-tags
const contentHashtags = useMemo(() => {
const hashtags = new Set<string>()
const hashtagRegex = /#(\w+)/g
let match
while ((match = hashtagRegex.exec(event.content)) !== null) {
hashtags.add(match[1].toLowerCase())
}
return hashtags
}, [event.content])
// Extract, normalize, and deduplicate all media URLs (images, audio, video) // Extract, normalize, and deduplicate all media URLs (images, audio, video)
// from content, imeta tags, and image tags // from content, imeta tags, and image tags
const mediaUrls = useMemo(() => { const mediaUrls = useMemo(() => {
@ -158,8 +170,20 @@ export default function MarkdownArticle({
// Handle hashtag links (format: /notes?t=tag) // Handle hashtag links (format: /notes?t=tag)
if (href.startsWith('/notes?t=') || href.startsWith('notes?t=')) { if (href.startsWith('/notes?t=') || href.startsWith('notes?t=')) {
// Extract the hashtag from the href
const hashtagMatch = href.match(/[?=]([^&]+)/)
const hashtag = hashtagMatch ? hashtagMatch[1].toLowerCase() : ''
// Only render as green link if this hashtag is actually in the content
// If not in content, suppress the link and render as plain text (hashtags are handled by split-based approach)
if (!contentHashtags.has(hashtag)) {
// Hashtag not in content, render as plain text (not a link at all)
return <span className="break-words">{children}</span>
}
// Normalize href to include leading slash if missing // Normalize href to include leading slash if missing
const normalizedHref = href.startsWith('/') ? href : `/${href}` const normalizedHref = href.startsWith('/') ? href : `/${href}`
// Inline hashtags from content should always be green
return ( return (
<SecondaryPageLink <SecondaryPageLink
to={normalizedHref} to={normalizedHref}
@ -262,21 +286,12 @@ export default function MarkdownArticle({
return <>{children}</> return <>{children}</>
} }
// Handle hashtags and wikilinks // Don't process hashtags in text component - they're already handled by split-based approach
const hashtagRegex = /#(\w+)/g // Only handle wikilinks here
const wikilinkRegex = /\[\[([^\]]+)\]\]/g const wikilinkRegex = /\[\[([^\]]+)\]\]/g
const allMatches: Array<{index: number, end: number, type: 'hashtag' | 'wikilink', data: any}> = [] const allMatches: Array<{index: number, end: number, type: 'wikilink', data: any}> = []
let match let match
while ((match = hashtagRegex.exec(children)) !== null) {
allMatches.push({
index: match.index,
end: match.index + match[0].length,
type: 'hashtag',
data: match[1]
})
}
while ((match = wikilinkRegex.exec(children)) !== null) { while ((match = wikilinkRegex.exec(children)) !== null) {
const content = match[1] const content = match[1]
let target = content.includes('|') ? content.split('|')[0].trim() : content.trim() let target = content.includes('|') ? content.split('|')[0].trim() : content.trim()
@ -308,15 +323,7 @@ export default function MarkdownArticle({
parts.push(children.slice(lastIndex, match.index)) parts.push(children.slice(lastIndex, match.index))
} }
if (match.type === 'hashtag') { parts.push(<Wikilink key={`w-${match.index}`} dTag={match.data.dtag} displayText={match.data.displayText} />)
parts.push(
<SecondaryPageLink key={`h-${match.index}`} to={`/notes?t=${match.data.toLowerCase()}`} className="text-green-600 dark:text-green-400 hover:underline">
#{match.data}
</SecondaryPageLink>
)
} else {
parts.push(<Wikilink key={`w-${match.index}`} dTag={match.data.dtag} displayText={match.data.displayText} />)
}
lastIndex = match.end lastIndex = match.end
} }
@ -344,7 +351,7 @@ export default function MarkdownArticle({
) )
} }
}) as Components, }) as Components,
[showImageGallery, event.pubkey, mediaUrls, event.kind] [showImageGallery, event.pubkey, mediaUrls, event.kind, contentHashtags]
) )
return ( return (
@ -439,6 +446,13 @@ export default function MarkdownArticle({
// Check if this part is a hashtag // Check if this part is a hashtag
if (part.match(/^#\w+$/)) { if (part.match(/^#\w+$/)) {
const hashtag = part.slice(1) const hashtag = part.slice(1)
const normalizedHashtag = hashtag.toLowerCase()
// Only render as green link if this hashtag is actually in the content
if (!contentHashtags.has(normalizedHashtag)) {
// Hashtag not in content, render as plain text
return <span key={`hashtag-plain-${index}`}>{part}</span>
}
// Add spaces before and after unless at start/end of line // Add spaces before and after unless at start/end of line
const isStartOfLine = index === 0 || array[index - 1].match(/^[\s]*$/) !== null const isStartOfLine = index === 0 || array[index - 1].match(/^[\s]*$/) !== null
@ -447,16 +461,17 @@ export default function MarkdownArticle({
const beforeSpace = isStartOfLine ? '' : ' ' const beforeSpace = isStartOfLine ? '' : ' '
const afterSpace = isEndOfLine ? '' : ' ' const afterSpace = isEndOfLine ? '' : ' '
// Inline hashtags from content should always be green
return ( return (
<span key={`hashtag-wrapper-${index}`}> <span key={`hashtag-wrapper-${index}`}>
{beforeSpace && beforeSpace} {beforeSpace && beforeSpace}
<a <a
href={`/notes?t=${hashtag.toLowerCase()}`} href={`/notes?t=${normalizedHashtag}`}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer" className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const url = `/notes?t=${hashtag.toLowerCase()}` const url = `/notes?t=${normalizedHashtag}`
console.log('[MarkdownArticle] Clicking hashtag, navigating to:', url) console.log('[MarkdownArticle] Clicking hashtag, navigating to:', url)
push(url) push(url)
}} }}
@ -519,21 +534,23 @@ export default function MarkdownArticle({
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
)} )}
{metadata.tags.length > 0 && ( {metadata.tags.filter(tag => !contentHashtags.has(tag.toLowerCase())).length > 0 && (
<div className="flex gap-2 flex-wrap pb-2"> <div className="flex gap-2 flex-wrap pb-2 mt-4">
{metadata.tags.map((tag) => ( {metadata.tags
<div .filter(tag => !contentHashtags.has(tag.toLowerCase()))
key={tag} .map((tag) => (
title={tag} <div
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground" key={tag}
onClick={(e) => { title={tag}
e.stopPropagation() className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] })) onClick={(e) => {
}} e.stopPropagation()
> push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
#<span className="truncate">{tag}</span> }}
</div> >
))} #<span className="truncate">{tag}</span>
</div>
))}
</div> </div>
)} )}
</div> </div>

2
src/components/Note/index.tsx

@ -180,7 +180,7 @@ export default function Note({
onClick={(e) => { onClick={(e) => {
// Don't navigate if clicking on interactive elements // Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]')) { if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]') || target.closest('[data-parent-note-preview]')) {
return return
} }
navigateToNote(toNote(event)) navigateToNote(toNote(event))

14
src/components/NoteCard/MainNoteCard.tsx

@ -26,8 +26,20 @@ export default function MainNoteCard({
<div <div
className={className} className={className}
onClick={(e) => { onClick={(e) => {
// Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-parent-note-preview]')) {
return
}
// For embedded notes, allow clicks (don't exclude [data-embedded-note])
// as embedded notes should be clickable to navigate to their page
if (!embedded && target.closest('[data-embedded-note]')) {
return
}
e.stopPropagation() e.stopPropagation()
navigateToNote(toNote(originalNoteId ?? event)) // Ensure navigation happens immediately
const noteUrl = toNote(originalNoteId ?? event)
navigateToNote(noteUrl)
}} }}
> >
<div className={`clickable ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3'}`}> <div className={`clickable ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3'}`}>

2
src/components/ParentNotePreview/index.tsx

@ -20,6 +20,7 @@ export default function ParentNotePreview({
if (isFetching) { if (isFetching) {
return ( return (
<div <div
data-parent-note-preview
className={cn( className={cn(
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-44 max-w-full text-muted-foreground', 'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-44 max-w-full text-muted-foreground',
className className
@ -36,6 +37,7 @@ export default function ParentNotePreview({
return ( return (
<div <div
data-parent-note-preview
className={cn( className={cn(
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground', 'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground',
event && 'hover:text-foreground cursor-pointer', event && 'hover:text-foreground cursor-pointer',

2
src/components/PostEditor/index.tsx

@ -42,7 +42,7 @@ export default function PostEditor({
openFrom={openFrom} openFrom={openFrom}
/> />
) )
}, []) }, [defaultContent, parentEvent, openFrom, setOpen])
if (isSmallScreen) { if (isSmallScreen) {
return ( return (

9
src/components/RelayInfo/RelayReviewCard.tsx

@ -26,7 +26,14 @@ export default function RelayReviewCard({
return ( return (
<div <div
className={cn('clickable border rounded-lg bg-muted/20 p-3 h-full', className)} className={cn('clickable border rounded-lg bg-muted/20 p-3 h-full', className)}
onClick={() => navigateToNote(toNote(event))} onClick={(e) => {
// Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-embedded-note]') || target.closest('[data-parent-note-preview]')) {
return
}
navigateToNote(toNote(event))
}}
> >
<div className="flex justify-between items-start gap-2"> <div className="flex justify-between items-start gap-2">
<div className="flex items-center space-x-2 flex-1"> <div className="flex items-center space-x-2 flex-1">

10
src/components/ReplyNote/index.tsx

@ -25,11 +25,13 @@ export default function ReplyNote({
event, event,
parentEventId, parentEventId,
onClickParent = () => {}, onClickParent = () => {},
onClickReply,
highlight = false highlight = false
}: { }: {
event: Event event: Event
parentEventId?: string parentEventId?: string
onClickParent?: () => void onClickParent?: () => void
onClickReply?: (event: Event) => void
highlight?: boolean highlight?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -58,10 +60,14 @@ export default function ReplyNote({
onClick={(e) => { onClick={(e) => {
// Don't navigate if clicking on interactive elements // Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a')) { if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-parent-note-preview]')) {
return return
} }
navigateToNote(toNote(event)) if (onClickReply) {
onClickReply(event)
} else {
navigateToNote(toNote(event))
}
}} }}
> >
<Collapsible> <Collapsible>

30
src/components/ReplyNoteList/index.tsx

@ -380,7 +380,10 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
if (scrollTo) { if (scrollTo) {
const ref = replyRefs.current[eventId] const ref = replyRefs.current[eventId]
if (ref) { if (ref) {
ref.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) // Use setTimeout to ensure DOM is updated before scrolling
setTimeout(() => {
ref.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 0)
} }
} }
setHighlightReplyId(eventId) setHighlightReplyId(eventId)
@ -416,6 +419,15 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
const parentETag = getParentETag(reply) const parentETag = getParentETag(reply)
const parentEventHexId = parentETag?.[1] const parentEventHexId = parentETag?.[1]
const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined const parentEventId = parentETag ? generateBech32IdFromETag(parentETag) : undefined
// Check if this reply belongs to the same thread as the root event
const replyRootId = getRootEventHexId(reply)
const belongsToSameThread = rootInfo && (
(rootInfo.type === 'E' && replyRootId === rootInfo.id) ||
(rootInfo.type === 'A' && getRootATag(reply)?.[1] === rootInfo.id) ||
(rootInfo.type === 'I' && reply.tags.find(tagNameEquals('I'))?.[1] === rootInfo.id)
)
return ( return (
<div <div
ref={(el) => (replyRefs.current[reply.id] = el)} ref={(el) => (replyRefs.current[reply.id] = el)}
@ -433,6 +445,22 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
} }
highlightReply(parentEventHexId) highlightReply(parentEventHexId)
}} }}
onClickReply={belongsToSameThread ? (replyEvent) => {
// Update URL without full navigation
const replyNoteUrl = toNote(replyEvent.id)
window.history.pushState(null, '', replyNoteUrl)
// Ensure the reply is visible by expanding the list if needed
const replyIndex = replies.findIndex(r => r.id === replyEvent.id)
if (replyIndex >= 0 && replyIndex >= showCount) {
setShowCount(replyIndex + 1)
}
// Highlight and scroll to the reply (use setTimeout to ensure DOM is updated)
setTimeout(() => {
highlightReply(replyEvent.id, true)
}, 50)
} : undefined}
highlight={highlightReplyId === reply.id} highlight={highlightReplyId === reply.id}
/> />
</div> </div>

4
src/lib/nostr-parser.tsx

@ -451,11 +451,13 @@ export function renderNostrContent(parsedContent: ParsedNostrContent, className?
if (element.type === 'hashtag' && element.hashtag) { if (element.type === 'hashtag' && element.hashtag) {
const normalizedHashtag = element.hashtag.toLowerCase() const normalizedHashtag = element.hashtag.toLowerCase()
// Only render as green link if this hashtag was parsed from the content
// (parseNostrContent already only extracts hashtags from content, not t-tags)
return ( return (
<a <a
key={index} key={index}
href={`/notes?t=${normalizedHashtag}`} href={`/notes?t=${normalizedHashtag}`}
className="text-primary hover:text-primary/80 hover:underline break-words" className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words cursor-pointer"
> >
#{element.hashtag} #{element.hashtag}
</a> </a>

9
src/services/relay-selection.service.ts

@ -278,6 +278,15 @@ class RelaySelectionService {
selectedRelays = Array.from(new Set(selectedRelays)) selectedRelays = Array.from(new Set(selectedRelays))
} }
// ALWAYS include cache relays (local network relays) in selected relays
// Cache relays are important for offline functionality
const cacheRelays = userWriteRelays.filter(url => isLocalNetworkUrl(url))
if (cacheRelays.length > 0) {
selectedRelays = [...selectedRelays, ...cacheRelays]
// Deduplicate after adding cache relays
selectedRelays = Array.from(new Set(selectedRelays))
}
// Filter out blocked relays // Filter out blocked relays
return this.filterBlockedRelays(selectedRelays, context.blockedRelays) return this.filterBlockedRelays(selectedRelays, context.blockedRelays)
} }

Loading…
Cancel
Save