From 1c7c3b701c099b5d9c70250a3eb4cff08b1b4080 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 5 Apr 2026 18:29:35 +0200 Subject: [PATCH] bug-fixes --- .../FavoriteRelaysFeedPicker/index.tsx | 17 +++++++--- src/components/NoteOptions/useMenuActions.tsx | 34 +++++++++++-------- src/constants.ts | 25 ++++++++++---- src/hooks/useContainerWidth.ts | 28 +++++++++++++++ 4 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 src/hooks/useContainerWidth.ts diff --git a/src/components/FavoriteRelaysFeedPicker/index.tsx b/src/components/FavoriteRelaysFeedPicker/index.tsx index c42490ff..4afbdb76 100644 --- a/src/components/FavoriteRelaysFeedPicker/index.tsx +++ b/src/components/FavoriteRelaysFeedPicker/index.tsx @@ -14,14 +14,17 @@ import { toRelaySettings } from '@/lib/link' import { normalizeUrl, simplifyUrl } from '@/lib/url' import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' import { cn } from '@/lib/utils' +import { useContainerWidth } from '@/hooks/useContainerWidth' import { useSecondaryPage } from '@/PageManager' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' -import { useScreenSize } from '@/providers/ScreenSizeProvider' import { SquarePen } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' +/** Chips → dropdown below this container width (px). Matches Tailwind `sm` breakpoint. */ +const NARROW_THRESHOLD = 640 + const ALL_FAVORITES_VALUE = '__all_favorites__' function relaySetToSelectValue(id: string) { @@ -36,7 +39,11 @@ function selectValueToRelaySetId(v: string) { /** Top-of-feed control: all favorites, Wisp trending (nostrarchives), relay sets, then single relays. */ export default function FavoriteRelaysFeedPicker() { const { t } = useTranslation() - const { isSmallScreen } = useScreenSize() + const containerRef = useRef(null) + const containerWidth = useContainerWidth(containerRef) + // True when the component's own container is narrow — covers both mobile viewports + // and the left pane in double-pane desktop mode. + const isNarrow = containerWidth !== undefined ? containerWidth < NARROW_THRESHOLD : false const { push } = useSecondaryPage() const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays() const { feedInfo, switchFeed } = useFeed() @@ -166,9 +173,10 @@ export default function FavoriteRelaysFeedPicker() { ) - if (isSmallScreen) { + if (isNarrow) { return (
@@ -234,6 +242,7 @@ export default function FavoriteRelaysFeedPicker() { return (
{ const checkIfPinned = async () => { if (!pubkey) { @@ -147,21 +156,15 @@ export function useMenuActions({ return } try { - // Build comprehensive relay list for pin status check const allRelays = [ - ...(currentBrowsingRelayUrls || []), - ...(favoriteRelays || []), - ...FAST_READ_RELAY_URLS, + ...(currentBrowsingRelayUrlsRef.current || []), + ...(favoriteRelaysRef.current || []), ...FAST_READ_RELAY_URLS, ...FAST_WRITE_RELAY_URLS ] - - const normalizedRelays = allRelays - .map(url => normalizeUrl(url)) - .filter((url): url is string => !!url) - - const comprehensiveRelays = Array.from(new Set(normalizedRelays)) - + const comprehensiveRelays = Array.from( + new Set(allRelays.map(url => normalizeUrl(url)).filter((url): url is string => !!url)) + ) const pinListEvent = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays) if (pinListEvent) { setIsPinned(isEventInPinList(pinListEvent, event)) @@ -174,7 +177,10 @@ export function useMenuActions({ } } checkIfPinned() - }, [pubkey, event.id, currentBrowsingRelayUrls, favoriteRelays]) + // Only re-run when the user or the specific event changes, not on relay list + // reference churn (relay arrays are read via refs above). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pubkey, event.id]) const handlePinNote = async () => { if (!pubkey) return diff --git a/src/constants.ts b/src/constants.ts index db26ec95..2ffb7d10 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,22 +31,35 @@ export const READ_ALOUD_TTS_URL = export const HIVETALK_BASE_URL = (import.meta.env.VITE_HIVETALK_BASE_URL as string | undefined) ?? 'https://vanilla.hivetalk.org' +/** + * Stable reference to this module's URL at load time. + * `import.meta.url` alone is left untouched by Vite (only `new URL(path, import.meta.url)` + * with a literal/template path gets transformed into a static asset map). + * In the Electron build (dist/assets/*.js) this is something like: + * file:///path/to/dist/assets/index-abc.js + */ +const _moduleHref: string = import.meta.url + /** * URL for a file from `public/` (banner, favicon, payto logos, etc.). * Uses Vite `base`: `/` on the web, `./` when built for Electron (`loadFile` + `file:`). * * Electron packaged builds use `file:` + client-side history paths like `/notes/…`, which replace * the document URL with `file:///notes/…`. Relative `BASE_URL` links would then resolve next to that - * bogus path and 404. Resolve from this module's emitted chunk (`dist/assets/*.js`) instead. - * One `..` reaches `dist/` (sibling of `assets/`); `../..` would miss `public/` copies and 404. + * bogus path and 404. + * + * For `file:` we derive the `dist/` root from the chunk's own URL. The chunk lives at + * `dist/assets/*.js`, so `/assets/` marks the boundary: everything before it is the dist root. + * Vite would transform `new URL(\`../${dynamic}\`, import.meta.url)` into a static glob map that + * does NOT include `public/` copies, so we must NOT use that pattern here. */ export function publicAssetUrl(assetPath: string): string { const trimmed = assetPath.replace(/^\//, '') if (typeof window !== 'undefined' && window.location.protocol === 'file:') { - try { - return new URL(`../${trimmed}`, import.meta.url).href - } catch { - // fall through to BASE_URL + const assetsIdx = _moduleHref.lastIndexOf('/assets/') + if (assetsIdx !== -1) { + // e.g. "file:///path/to/dist/" + "banner.png" + return _moduleHref.slice(0, assetsIdx + 1) + trimmed } } return `${import.meta.env.BASE_URL}${trimmed}` diff --git a/src/hooks/useContainerWidth.ts b/src/hooks/useContainerWidth.ts new file mode 100644 index 00000000..cd9ca0fa --- /dev/null +++ b/src/hooks/useContainerWidth.ts @@ -0,0 +1,28 @@ +import { RefObject, useEffect, useState } from 'react' + +/** + * Tracks the rendered width of `ref`'s element via ResizeObserver. + * Returns `undefined` until the first measurement fires. + * Use this when you need a component to respond to its *own* container width + * rather than the viewport width (e.g. inside a split-pane layout where + * `isSmallScreen` is still `false` but the column is narrow). + */ +export function useContainerWidth(ref: RefObject): number | undefined { + const [width, setWidth] = useState(undefined) + + useEffect(() => { + const el = ref.current + if (!el) return + + const observer = new ResizeObserver((entries) => { + const entry = entries[0] + if (entry) setWidth(entry.contentRect.width) + }) + observer.observe(el) + // Initialise synchronously so there's no render flash + setWidth(el.getBoundingClientRect().width) + return () => observer.disconnect() + }, [ref]) + + return width +}