{
+ 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') ||