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

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

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

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

@ -37,6 +37,18 @@ export default function MarkdownArticle({ @@ -37,6 +37,18 @@ export default function MarkdownArticle({
const allImages = useMemo(() => extractAllImagesFromEvent(event), [event])
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)
// from content, imeta tags, and image tags
const mediaUrls = useMemo(() => {
@ -158,8 +170,20 @@ export default function MarkdownArticle({ @@ -158,8 +170,20 @@ export default function MarkdownArticle({
// Handle hashtag links (format: /notes?t=tag)
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
const normalizedHref = href.startsWith('/') ? href : `/${href}`
// Inline hashtags from content should always be green
return (
<SecondaryPageLink
to={normalizedHref}
@ -262,21 +286,12 @@ export default function MarkdownArticle({ @@ -262,21 +286,12 @@ export default function MarkdownArticle({
return <>{children}</>
}
// Handle hashtags and wikilinks
const hashtagRegex = /#(\w+)/g
// Don't process hashtags in text component - they're already handled by split-based approach
// Only handle wikilinks here
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
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) {
const content = match[1]
let target = content.includes('|') ? content.split('|')[0].trim() : content.trim()
@ -308,15 +323,7 @@ export default function MarkdownArticle({ @@ -308,15 +323,7 @@ export default function MarkdownArticle({
parts.push(children.slice(lastIndex, match.index))
}
if (match.type === 'hashtag') {
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} />)
}
parts.push(<Wikilink key={`w-${match.index}`} dTag={match.data.dtag} displayText={match.data.displayText} />)
lastIndex = match.end
}
@ -344,7 +351,7 @@ export default function MarkdownArticle({ @@ -344,7 +351,7 @@ export default function MarkdownArticle({
)
}
}) as Components,
[showImageGallery, event.pubkey, mediaUrls, event.kind]
[showImageGallery, event.pubkey, mediaUrls, event.kind, contentHashtags]
)
return (
@ -439,6 +446,13 @@ export default function MarkdownArticle({ @@ -439,6 +446,13 @@ export default function MarkdownArticle({
// Check if this part is a hashtag
if (part.match(/^#\w+$/)) {
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
const isStartOfLine = index === 0 || array[index - 1].match(/^[\s]*$/) !== null
@ -447,16 +461,17 @@ export default function MarkdownArticle({ @@ -447,16 +461,17 @@ export default function MarkdownArticle({
const beforeSpace = isStartOfLine ? '' : ' '
const afterSpace = isEndOfLine ? '' : ' '
// Inline hashtags from content should always be green
return (
<span key={`hashtag-wrapper-${index}`}>
{beforeSpace && beforeSpace}
<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"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
const url = `/notes?t=${hashtag.toLowerCase()}`
const url = `/notes?t=${normalizedHashtag}`
console.log('[MarkdownArticle] Clicking hashtag, navigating to:', url)
push(url)
}}
@ -519,21 +534,23 @@ export default function MarkdownArticle({ @@ -519,21 +534,23 @@ export default function MarkdownArticle({
</CollapsibleContent>
</Collapsible>
)}
{metadata.tags.length > 0 && (
<div className="flex gap-2 flex-wrap pb-2">
{metadata.tags.map((tag) => (
<div
key={tag}
title={tag}
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"
onClick={(e) => {
e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
{metadata.tags.filter(tag => !contentHashtags.has(tag.toLowerCase())).length > 0 && (
<div className="flex gap-2 flex-wrap pb-2 mt-4">
{metadata.tags
.filter(tag => !contentHashtags.has(tag.toLowerCase()))
.map((tag) => (
<div
key={tag}
title={tag}
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"
onClick={(e) => {
e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
</div>
)}
</div>

2
src/components/Note/index.tsx

@ -180,7 +180,7 @@ export default function Note({ @@ -180,7 +180,7 @@ export default function Note({
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]')) {
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))

14
src/components/NoteCard/MainNoteCard.tsx

@ -26,8 +26,20 @@ export default function MainNoteCard({ @@ -26,8 +26,20 @@ export default function MainNoteCard({
<div
className={className}
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()
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'}`}>

2
src/components/ParentNotePreview/index.tsx

@ -20,6 +20,7 @@ export default function ParentNotePreview({ @@ -20,6 +20,7 @@ export default function ParentNotePreview({
if (isFetching) {
return (
<div
data-parent-note-preview
className={cn(
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-44 max-w-full text-muted-foreground',
className
@ -36,6 +37,7 @@ export default function ParentNotePreview({ @@ -36,6 +37,7 @@ export default function ParentNotePreview({
return (
<div
data-parent-note-preview
className={cn(
'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',

2
src/components/PostEditor/index.tsx

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

9
src/components/RelayInfo/RelayReviewCard.tsx

@ -26,7 +26,14 @@ export default function RelayReviewCard({ @@ -26,7 +26,14 @@ export default function RelayReviewCard({
return (
<div
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 items-center space-x-2 flex-1">

10
src/components/ReplyNote/index.tsx

@ -25,11 +25,13 @@ export default function ReplyNote({ @@ -25,11 +25,13 @@ export default function ReplyNote({
event,
parentEventId,
onClickParent = () => {},
onClickReply,
highlight = false
}: {
event: Event
parentEventId?: string
onClickParent?: () => void
onClickReply?: (event: Event) => void
highlight?: boolean
}) {
const { t } = useTranslation()
@ -58,10 +60,14 @@ export default function ReplyNote({ @@ -58,10 +60,14 @@ export default function ReplyNote({
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')) {
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-parent-note-preview]')) {
return
}
navigateToNote(toNote(event))
if (onClickReply) {
onClickReply(event)
} else {
navigateToNote(toNote(event))
}
}}
>
<Collapsible>

30
src/components/ReplyNoteList/index.tsx

@ -380,7 +380,10 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even @@ -380,7 +380,10 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
if (scrollTo) {
const ref = replyRefs.current[eventId]
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)
@ -416,6 +419,15 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even @@ -416,6 +419,15 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
const parentETag = getParentETag(reply)
const parentEventHexId = parentETag?.[1]
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 (
<div
ref={(el) => (replyRefs.current[reply.id] = el)}
@ -433,6 +445,22 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even @@ -433,6 +445,22 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
}
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}
/>
</div>

4
src/lib/nostr-parser.tsx

@ -451,11 +451,13 @@ export function renderNostrContent(parsedContent: ParsedNostrContent, className? @@ -451,11 +451,13 @@ export function renderNostrContent(parsedContent: ParsedNostrContent, className?
if (element.type === 'hashtag' && element.hashtag) {
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 (
<a
key={index}
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}
</a>

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

@ -278,6 +278,15 @@ class RelaySelectionService { @@ -278,6 +278,15 @@ class RelaySelectionService {
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
return this.filterBlockedRelays(selectedRelays, context.blockedRelays)
}

Loading…
Cancel
Save