From 78b9a967047855af958ab352b1342652ed0fe423 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 31 Oct 2025 18:49:25 +0100 Subject: [PATCH] fixed hashtags --- .../Note/MarkdownArticle/MarkdownArticle.tsx | 31 +++---- src/components/NoteOptions/DesktopMenu.tsx | 84 +++++++++++-------- src/components/NoteOptions/index.tsx | 19 +++-- src/components/Tabs/index.tsx | 41 ++++++--- src/components/ui/dropdown-menu.tsx | 30 ++++++- src/lib/error-suppression.ts | 7 ++ 6 files changed, 138 insertions(+), 74 deletions(-) diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 1eed75c..1c89d00 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -118,24 +118,12 @@ 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 {children} - } - // Normalize href to include leading slash if missing const normalizedHref = href.startsWith('/') ? href : `/${href}` - // Render hashtags as inline span elements - force inline display with no margins + // Render hashtags as inline green links - remarkHashtags only processes hashtags in content return ( { e.stopPropagation() e.preventDefault() @@ -385,7 +373,7 @@ export default function MarkdownArticle({ .hljs-strong { font-weight: bold; } - /* Force hashtag links to stay inline - override prose styles */ + /* Force hashtag links to stay inline and green - override prose styles */ .prose a[href^="/notes?t="], .prose a[href^="notes?t="], .prose span[role="button"][tabindex="0"] { @@ -393,6 +381,19 @@ export default function MarkdownArticle({ margin: 0 !important; padding: 0 !important; line-height: inherit !important; + color: #16a34a !important; /* Tailwind green-600 */ + text-decoration: none !important; + } + .prose span[role="button"][tabindex="0"]:hover { + color: #15803d !important; /* Tailwind green-700 */ + text-decoration: underline !important; + } + .dark .prose span[role="button"][tabindex="0"] { + color: #4ade80 !important; /* Tailwind green-400 */ + } + .dark .prose span[role="button"][tabindex="0"]:hover { + color: #86efac !important; /* Tailwind green-300 */ + text-decoration: underline !important; } `}
{ + return ( + <> + {menuActions.map((action, index) => { + const Icon = action.icon + return ( +
+ {action.separator && index > 0 && } + {action.subMenu ? ( + + + + {action.label} + + + {action.subMenu.map((subAction, subIndex) => ( +
+ {subAction.separator && subIndex > 0 && } + + {subAction.label} + +
+ ))} +
+
+ ) : ( + + + {action.label} + + )} +
+ ) + })} + + ) +}) +MenuContent.displayName = 'MenuContent' + export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) { return ( {trigger} - {menuActions.map((action, index) => { - const Icon = action.icon - return ( -
- {action.separator && index > 0 && } - {action.subMenu ? ( - - - - {action.label} - - - {action.subMenu.map((subAction, subIndex) => ( -
- {subAction.separator && subIndex > 0 && } - - {subAction.label} - -
- ))} -
-
- ) : ( - - - {action.label} - - )} -
- ) - })} +
) diff --git a/src/components/NoteOptions/index.tsx b/src/components/NoteOptions/index.tsx index eb4b7a6..ea1c659 100644 --- a/src/components/NoteOptions/index.tsx +++ b/src/components/NoteOptions/index.tsx @@ -1,7 +1,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Ellipsis } from 'lucide-react' import { Event } from 'nostr-tools' -import { useState } from 'react' +import { useState, useMemo } from 'react' import { DesktopMenu } from './DesktopMenu' import { MobileMenu } from './MobileMenu' import RawEventDialog from './RawEventDialog' @@ -41,13 +41,16 @@ export default function NoteOptions({ event, className }: { event: Event; classN isSmallScreen }) - const trigger = ( - + const trigger = useMemo( + () => ( + + ), + [] ) return ( diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx index c5d5154..8ca6cde 100644 --- a/src/components/Tabs/index.tsx +++ b/src/components/Tabs/index.tsx @@ -1,6 +1,6 @@ import { cn } from '@/lib/utils' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' -import { ReactNode, useEffect, useRef, useState } from 'react' +import { ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' type TabDefinition = { @@ -26,19 +26,38 @@ export default function Tabs({ const tabRefs = useRef<(HTMLDivElement | null)[]>([]) const containerRef = useRef(null) const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0 }) + const isUpdatingRef = useRef(false) + const lastStyleRef = useRef({ width: 0, left: 0 }) - const updateIndicatorPosition = () => { + const updateIndicatorPosition = useCallback(() => { + // Prevent multiple simultaneous updates + if (isUpdatingRef.current) return + const activeIndex = tabs.findIndex((tab) => tab.value === value) if (activeIndex >= 0 && tabRefs.current[activeIndex]) { const activeTab = tabRefs.current[activeIndex] const { offsetWidth, offsetLeft } = activeTab const padding = 24 // 12px padding on each side - setIndicatorStyle({ - width: offsetWidth - padding, - left: offsetLeft + padding / 2 - }) + const newWidth = offsetWidth - padding + const newLeft = offsetLeft + padding / 2 + + // Only update if values actually changed + if ( + lastStyleRef.current.width !== newWidth || + lastStyleRef.current.left !== newLeft + ) { + isUpdatingRef.current = true + lastStyleRef.current = { width: newWidth, left: newLeft } + + setIndicatorStyle({ width: newWidth, left: newLeft }) + + // Reset flag after state update completes + requestAnimationFrame(() => { + isUpdatingRef.current = false + }) + } } - } + }, [tabs, value]) useEffect(() => { const animationId = requestAnimationFrame(() => { @@ -48,13 +67,15 @@ export default function Tabs({ return () => { cancelAnimationFrame(animationId) } - }, [tabs, value]) + }, [updateIndicatorPosition]) useEffect(() => { if (!containerRef.current) return const resizeObserver = new ResizeObserver(() => { - updateIndicatorPosition() + requestAnimationFrame(() => { + updateIndicatorPosition() + }) }) const intersectionObserver = new IntersectionObserver( @@ -80,7 +101,7 @@ export default function Tabs({ resizeObserver.disconnect() intersectionObserver.disconnect() } - }, [tabs, value]) + }, [updateIndicatorPosition]) return (
(null) const scrollAreaRef = React.useRef(null) + const lastScrollStateRef = React.useRef({ canScrollUp: false, canScrollDown: false }) React.useImperativeHandle(ref, () => contentRef.current!) @@ -54,8 +55,18 @@ const DropdownMenuSubContent = React.forwardRef< const scrollArea = scrollAreaRef.current if (!scrollArea) return - setCanScrollUp(scrollArea.scrollTop > 0) - setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight) + const newCanScrollUp = scrollArea.scrollTop > 0 + const newCanScrollDown = scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight + + // Only update state if values actually changed to prevent infinite loops + if (newCanScrollUp !== lastScrollStateRef.current.canScrollUp) { + lastScrollStateRef.current.canScrollUp = newCanScrollUp + setCanScrollUp(newCanScrollUp) + } + if (newCanScrollDown !== lastScrollStateRef.current.canScrollDown) { + lastScrollStateRef.current.canScrollDown = newCanScrollDown + setCanScrollDown(newCanScrollDown) + } }, []) const scrollUp = () => { @@ -133,6 +144,7 @@ const DropdownMenuContent = React.forwardRef< const [canScrollDown, setCanScrollDown] = React.useState(false) const contentRef = React.useRef(null) const scrollAreaRef = React.useRef(null) + const lastScrollStateRef = React.useRef({ canScrollUp: false, canScrollDown: false }) React.useImperativeHandle(ref, () => contentRef.current!) @@ -140,8 +152,18 @@ const DropdownMenuContent = React.forwardRef< const scrollArea = scrollAreaRef.current if (!scrollArea) return - setCanScrollUp(scrollArea.scrollTop > 0) - setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight) + const newCanScrollUp = scrollArea.scrollTop > 0 + const newCanScrollDown = scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight + + // Only update state if values actually changed to prevent infinite loops + if (newCanScrollUp !== lastScrollStateRef.current.canScrollUp) { + lastScrollStateRef.current.canScrollUp = newCanScrollUp + setCanScrollUp(newCanScrollUp) + } + if (newCanScrollDown !== lastScrollStateRef.current.canScrollDown) { + lastScrollStateRef.current.canScrollDown = newCanScrollDown + setCanScrollDown(newCanScrollDown) + } }, []) const scrollUp = () => { diff --git a/src/lib/error-suppression.ts b/src/lib/error-suppression.ts index cb84ddd..518c689 100644 --- a/src/lib/error-suppression.ts +++ b/src/lib/error-suppression.ts @@ -53,6 +53,13 @@ export function suppressExpectedErrors() { return } + // Suppress React "Maximum update depth exceeded" warnings + // These are often caused by third-party libraries (e.g., Radix UI Popper) + // where we cannot modify the source code directly + if (message.includes('Maximum update depth exceeded')) { + return + } + // Suppress Workbox precaching errors for development modules if (message.includes('Precaching did not find a match') && ( message.includes('@vite/client') ||