Browse Source

fixed hashtags

imwald
Silberengel 5 months ago
parent
commit
78b9a96704
  1. 31
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  2. 84
      src/components/NoteOptions/DesktopMenu.tsx
  3. 19
      src/components/NoteOptions/index.tsx
  4. 41
      src/components/Tabs/index.tsx
  5. 30
      src/components/ui/dropdown-menu.tsx
  6. 7
      src/lib/error-suppression.ts

31
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -118,24 +118,12 @@ 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}`
// 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 ( return (
<span <span
className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer [&]:inline [&]:m-0 [&]:p-0 [&]:leading-normal" className="inline text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline cursor-pointer"
style={{ display: 'inline', margin: 0, padding: 0 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
@ -385,7 +373,7 @@ export default function MarkdownArticle({
.hljs-strong { .hljs-strong {
font-weight: bold; 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 a[href^="notes?t="], .prose a[href^="notes?t="],
.prose span[role="button"][tabindex="0"] { .prose span[role="button"][tabindex="0"] {
@ -393,6 +381,19 @@ export default function MarkdownArticle({
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
line-height: inherit !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;
} }
`}</style> `}</style>
<div <div

84
src/components/NoteOptions/DesktopMenu.tsx

@ -10,54 +10,64 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { MenuAction } from './useMenuActions' import { MenuAction } from './useMenuActions'
import { memo } from 'react'
interface DesktopMenuProps { interface DesktopMenuProps {
menuActions: MenuAction[] menuActions: MenuAction[]
trigger: React.ReactNode trigger: React.ReactNode
} }
const MenuContent = memo(({ menuActions }: { menuActions: MenuAction[] }) => {
return (
<>
{menuActions.map((action, index) => {
const Icon = action.icon
return (
<div key={index}>
{action.separator && index > 0 && <DropdownMenuSeparator />}
{action.subMenu ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger className={action.className}>
<Icon />
{action.label}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="max-h-[50vh] overflow-y-auto"
showScrollButtons
>
{action.subMenu.map((subAction, subIndex) => (
<div key={subIndex}>
{subAction.separator && subIndex > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={subAction.onClick}
className={cn('w-64', subAction.className)}
>
{subAction.label}
</DropdownMenuItem>
</div>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : (
<DropdownMenuItem onClick={action.onClick} className={action.className}>
<Icon />
{action.label}
</DropdownMenuItem>
)}
</div>
)
})}
</>
)
})
MenuContent.displayName = 'MenuContent'
export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) { export function DesktopMenu({ menuActions, trigger }: DesktopMenuProps) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[50vh] overflow-y-auto"> <DropdownMenuContent className="max-h-[50vh] overflow-y-auto">
{menuActions.map((action, index) => { <MenuContent menuActions={menuActions} />
const Icon = action.icon
return (
<div key={index}>
{action.separator && index > 0 && <DropdownMenuSeparator />}
{action.subMenu ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger className={action.className}>
<Icon />
{action.label}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="max-h-[50vh] overflow-y-auto"
showScrollButtons
>
{action.subMenu.map((subAction, subIndex) => (
<div key={subIndex}>
{subAction.separator && subIndex > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={subAction.onClick}
className={cn('w-64', subAction.className)}
>
{subAction.label}
</DropdownMenuItem>
</div>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : (
<DropdownMenuItem onClick={action.onClick} className={action.className}>
<Icon />
{action.label}
</DropdownMenuItem>
)}
</div>
)
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) )

19
src/components/NoteOptions/index.tsx

@ -1,7 +1,7 @@
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Ellipsis } from 'lucide-react' import { Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useState } from 'react' import { useState, useMemo } from 'react'
import { DesktopMenu } from './DesktopMenu' import { DesktopMenu } from './DesktopMenu'
import { MobileMenu } from './MobileMenu' import { MobileMenu } from './MobileMenu'
import RawEventDialog from './RawEventDialog' import RawEventDialog from './RawEventDialog'
@ -41,13 +41,16 @@ export default function NoteOptions({ event, className }: { event: Event; classN
isSmallScreen isSmallScreen
}) })
const trigger = ( const trigger = useMemo(
<button () => (
className="flex items-center text-muted-foreground hover:text-foreground pl-2 h-full" <button
onClick={() => setIsDrawerOpen(true)} className="flex items-center text-muted-foreground hover:text-foreground pl-2 h-full"
> onClick={() => setIsDrawerOpen(true)}
<Ellipsis /> >
</button> <Ellipsis />
</button>
),
[]
) )
return ( return (

41
src/components/Tabs/index.tsx

@ -1,6 +1,6 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' 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' import { useTranslation } from 'react-i18next'
type TabDefinition = { type TabDefinition = {
@ -26,19 +26,38 @@ export default function Tabs({
const tabRefs = useRef<(HTMLDivElement | null)[]>([]) const tabRefs = useRef<(HTMLDivElement | null)[]>([])
const containerRef = useRef<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0 }) 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) const activeIndex = tabs.findIndex((tab) => tab.value === value)
if (activeIndex >= 0 && tabRefs.current[activeIndex]) { if (activeIndex >= 0 && tabRefs.current[activeIndex]) {
const activeTab = tabRefs.current[activeIndex] const activeTab = tabRefs.current[activeIndex]
const { offsetWidth, offsetLeft } = activeTab const { offsetWidth, offsetLeft } = activeTab
const padding = 24 // 12px padding on each side const padding = 24 // 12px padding on each side
setIndicatorStyle({ const newWidth = offsetWidth - padding
width: offsetWidth - padding, const newLeft = offsetLeft + padding / 2
left: 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(() => { useEffect(() => {
const animationId = requestAnimationFrame(() => { const animationId = requestAnimationFrame(() => {
@ -48,13 +67,15 @@ export default function Tabs({
return () => { return () => {
cancelAnimationFrame(animationId) cancelAnimationFrame(animationId)
} }
}, [tabs, value]) }, [updateIndicatorPosition])
useEffect(() => { useEffect(() => {
if (!containerRef.current) return if (!containerRef.current) return
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
updateIndicatorPosition() requestAnimationFrame(() => {
updateIndicatorPosition()
})
}) })
const intersectionObserver = new IntersectionObserver( const intersectionObserver = new IntersectionObserver(
@ -80,7 +101,7 @@ export default function Tabs({
resizeObserver.disconnect() resizeObserver.disconnect()
intersectionObserver.disconnect() intersectionObserver.disconnect()
} }
}, [tabs, value]) }, [updateIndicatorPosition])
return ( return (
<div <div

30
src/components/ui/dropdown-menu.tsx

@ -47,6 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
const [canScrollDown, setCanScrollDown] = React.useState(false) const [canScrollDown, setCanScrollDown] = React.useState(false)
const contentRef = React.useRef<HTMLDivElement>(null) const contentRef = React.useRef<HTMLDivElement>(null)
const scrollAreaRef = React.useRef<HTMLDivElement>(null) const scrollAreaRef = React.useRef<HTMLDivElement>(null)
const lastScrollStateRef = React.useRef({ canScrollUp: false, canScrollDown: false })
React.useImperativeHandle(ref, () => contentRef.current!) React.useImperativeHandle(ref, () => contentRef.current!)
@ -54,8 +55,18 @@ const DropdownMenuSubContent = React.forwardRef<
const scrollArea = scrollAreaRef.current const scrollArea = scrollAreaRef.current
if (!scrollArea) return if (!scrollArea) return
setCanScrollUp(scrollArea.scrollTop > 0) const newCanScrollUp = scrollArea.scrollTop > 0
setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight) 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 = () => { const scrollUp = () => {
@ -133,6 +144,7 @@ const DropdownMenuContent = React.forwardRef<
const [canScrollDown, setCanScrollDown] = React.useState(false) const [canScrollDown, setCanScrollDown] = React.useState(false)
const contentRef = React.useRef<HTMLDivElement>(null) const contentRef = React.useRef<HTMLDivElement>(null)
const scrollAreaRef = React.useRef<HTMLDivElement>(null) const scrollAreaRef = React.useRef<HTMLDivElement>(null)
const lastScrollStateRef = React.useRef({ canScrollUp: false, canScrollDown: false })
React.useImperativeHandle(ref, () => contentRef.current!) React.useImperativeHandle(ref, () => contentRef.current!)
@ -140,8 +152,18 @@ const DropdownMenuContent = React.forwardRef<
const scrollArea = scrollAreaRef.current const scrollArea = scrollAreaRef.current
if (!scrollArea) return if (!scrollArea) return
setCanScrollUp(scrollArea.scrollTop > 0) const newCanScrollUp = scrollArea.scrollTop > 0
setCanScrollDown(scrollArea.scrollTop < scrollArea.scrollHeight - scrollArea.clientHeight) 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 = () => { const scrollUp = () => {

7
src/lib/error-suppression.ts

@ -53,6 +53,13 @@ export function suppressExpectedErrors() {
return 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 // Suppress Workbox precaching errors for development modules
if (message.includes('Precaching did not find a match') && ( if (message.includes('Precaching did not find a match') && (
message.includes('@vite/client') || message.includes('@vite/client') ||

Loading…
Cancel
Save