Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
1c7c3b701c
  1. 17
      src/components/FavoriteRelaysFeedPicker/index.tsx
  2. 34
      src/components/NoteOptions/useMenuActions.tsx
  3. 25
      src/constants.ts
  4. 28
      src/hooks/useContainerWidth.ts

17
src/components/FavoriteRelaysFeedPicker/index.tsx

@ -14,14 +14,17 @@ import { toRelaySettings } from '@/lib/link'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay' import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContainerWidth } from '@/hooks/useContainerWidth'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { SquarePen } from 'lucide-react' import { SquarePen } from 'lucide-react'
import { useMemo } from 'react' import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' 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__' const ALL_FAVORITES_VALUE = '__all_favorites__'
function relaySetToSelectValue(id: string) { 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. */ /** Top-of-feed control: all favorites, Wisp trending (nostrarchives), relay sets, then single relays. */
export default function FavoriteRelaysFeedPicker() { export default function FavoriteRelaysFeedPicker() {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const containerRef = useRef<HTMLDivElement>(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 { push } = useSecondaryPage()
const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays() const { favoriteRelays, blockedRelays, relaySets } = useFavoriteRelays()
const { feedInfo, switchFeed } = useFeed() const { feedInfo, switchFeed } = useFeed()
@ -166,9 +173,10 @@ export default function FavoriteRelaysFeedPicker() {
</Button> </Button>
) )
if (isSmallScreen) { if (isNarrow) {
return ( return (
<div <div
ref={containerRef}
className="flex w-full min-w-0 items-center gap-1.5 border-b border-border/80 bg-background px-2 py-1.5" className="flex w-full min-w-0 items-center gap-1.5 border-b border-border/80 bg-background px-2 py-1.5"
aria-label={t('Favorite Relays')} aria-label={t('Favorite Relays')}
> >
@ -234,6 +242,7 @@ export default function FavoriteRelaysFeedPicker() {
return ( return (
<div <div
ref={containerRef}
className="flex w-full min-w-0 items-center gap-1.5 border-b border-border/80 bg-background px-2 py-1.5" className="flex w-full min-w-0 items-center gap-1.5 border-b border-border/80 bg-background px-2 py-1.5"
role="toolbar" role="toolbar"
aria-label={t('Favorite Relays')} aria-label={t('Favorite Relays')}

34
src/components/NoteOptions/useMenuActions.tsx

@ -50,7 +50,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect, useContext } from 'react' import { useMemo, useState, useEffect, useRef, useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import RelayIcon from '../RelayIcon' import RelayIcon from '../RelayIcon'
@ -139,7 +139,16 @@ export function useMenuActions({
// Check if event is pinned // Check if event is pinned
const [isPinned, setIsPinned] = useState(false) const [isPinned, setIsPinned] = useState(false)
// Keep refs so the effect can read the latest relay lists without making them
// part of the dependency array. Including live array references as deps causes
// an infinite loop: relay fetch → NoteList re-render → new array refs →
// effect re-fires for every visible note → relay fetch → …
const currentBrowsingRelayUrlsRef = useRef(currentBrowsingRelayUrls)
currentBrowsingRelayUrlsRef.current = currentBrowsingRelayUrls
const favoriteRelaysRef = useRef(favoriteRelays)
favoriteRelaysRef.current = favoriteRelays
useEffect(() => { useEffect(() => {
const checkIfPinned = async () => { const checkIfPinned = async () => {
if (!pubkey) { if (!pubkey) {
@ -147,21 +156,15 @@ export function useMenuActions({
return return
} }
try { try {
// Build comprehensive relay list for pin status check
const allRelays = [ const allRelays = [
...(currentBrowsingRelayUrls || []), ...(currentBrowsingRelayUrlsRef.current || []),
...(favoriteRelays || []), ...(favoriteRelaysRef.current || []),
...FAST_READ_RELAY_URLS,
...FAST_READ_RELAY_URLS, ...FAST_READ_RELAY_URLS,
...FAST_WRITE_RELAY_URLS ...FAST_WRITE_RELAY_URLS
] ]
const comprehensiveRelays = Array.from(
const normalizedRelays = allRelays new Set(allRelays.map(url => normalizeUrl(url)).filter((url): url is string => !!url))
.map(url => normalizeUrl(url)) )
.filter((url): url is string => !!url)
const comprehensiveRelays = Array.from(new Set(normalizedRelays))
const pinListEvent = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays) const pinListEvent = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays)
if (pinListEvent) { if (pinListEvent) {
setIsPinned(isEventInPinList(pinListEvent, event)) setIsPinned(isEventInPinList(pinListEvent, event))
@ -174,7 +177,10 @@ export function useMenuActions({
} }
} }
checkIfPinned() 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 () => { const handlePinNote = async () => {
if (!pubkey) return if (!pubkey) return

25
src/constants.ts

@ -31,22 +31,35 @@ export const READ_ALOUD_TTS_URL =
export const HIVETALK_BASE_URL = export const HIVETALK_BASE_URL =
(import.meta.env.VITE_HIVETALK_BASE_URL as string | undefined) ?? 'https://vanilla.hivetalk.org' (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.). * URL for a file from `public/` (banner, favicon, payto logos, etc.).
* Uses Vite `base`: `/` on the web, `./` when built for Electron (`loadFile` + `file:`). * 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 * 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 * 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. * bogus path and 404.
* One `..` reaches `dist/` (sibling of `assets/`); `../..` would miss `public/` copies 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 { export function publicAssetUrl(assetPath: string): string {
const trimmed = assetPath.replace(/^\//, '') const trimmed = assetPath.replace(/^\//, '')
if (typeof window !== 'undefined' && window.location.protocol === 'file:') { if (typeof window !== 'undefined' && window.location.protocol === 'file:') {
try { const assetsIdx = _moduleHref.lastIndexOf('/assets/')
return new URL(`../${trimmed}`, import.meta.url).href if (assetsIdx !== -1) {
} catch { // e.g. "file:///path/to/dist/" + "banner.png"
// fall through to BASE_URL return _moduleHref.slice(0, assetsIdx + 1) + trimmed
} }
} }
return `${import.meta.env.BASE_URL}${trimmed}` return `${import.meta.env.BASE_URL}${trimmed}`

28
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<Element | null>): number | undefined {
const [width, setWidth] = useState<number | undefined>(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
}
Loading…
Cancel
Save