From 46f64fa793cbd22766bfd771c8e072aba7e14f57 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 23 May 2026 06:57:32 +0200 Subject: [PATCH] make entire app responsive to changes in font size of the computer --- src/App.tsx | 2 +- src/PageManager.tsx | 13 +++---- .../MetadataRelaysOnlySetting/index.tsx | 3 +- src/components/Titlebar/index.tsx | 2 +- src/i18n/locales/de.ts | 4 +-- src/i18n/locales/en.ts | 4 +-- src/index.css | 8 +++++ src/layouts/PrimaryPageLayout/index.tsx | 15 +++----- src/layouts/SecondaryPageLayout/index.tsx | 2 +- src/lib/metadata-policy-curated-relays.ts | 21 +++++++++++ src/lib/read-only-relay-personal.test.ts | 11 +++++- src/lib/read-only-relay-personal.ts | 36 +++---------------- src/lib/viewport-height.ts | 29 +++++++++++++++ src/main.tsx | 9 ++--- src/pages/primary/NoteListPage/index.tsx | 2 +- src/providers/FontSizeProvider.tsx | 2 ++ src/services/client.service.ts | 21 +++++++++++ 17 files changed, 117 insertions(+), 67 deletions(-) create mode 100644 src/lib/viewport-height.ts diff --git a/src/App.tsx b/src/App.tsx index 2103d8a7..519f30ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,7 +40,7 @@ export default function App(): JSX.Element { -
+
diff --git a/src/PageManager.tsx b/src/PageManager.tsx index ded7bc0a..9c80d694 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -1054,8 +1054,8 @@ function MainContentArea({ // flex + min-h-0 + min-w-0 so primary pages get a real height in flex parents and can shrink horizontally (double-pane). return ( -
-
+
+
{primaryNoteView ? ( // Show note view with back button
@@ -2432,12 +2432,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { }} > -
+
@@ -2459,7 +2456,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { />
{/* Right: secondary stack — max width so left pane keeps space on small desktops */} -
+
{secondaryStack.length > 0 ? (
{t( - 'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists (plus profile and search index relays). Publishing is unchanged. Relay explore and Search pages are exempt.' + 'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.' )}
diff --git a/src/components/Titlebar/index.tsx b/src/components/Titlebar/index.tsx index c1a843e0..50802256 100644 --- a/src/components/Titlebar/index.tsx +++ b/src/components/Titlebar/index.tsx @@ -12,7 +12,7 @@ export function Titlebar({ return (
-
+
{hasTitlebarRow ? ( {children}
@@ -216,14 +210,13 @@ function PrimaryPageTitlebar({ return ( -
+
-
{children}
+
{children}
{showTrailingActiveRelays ? : null}
diff --git a/src/layouts/SecondaryPageLayout/index.tsx b/src/layouts/SecondaryPageLayout/index.tsx index 1200d89a..c2c53df7 100644 --- a/src/layouts/SecondaryPageLayout/index.tsx +++ b/src/layouts/SecondaryPageLayout/index.tsx @@ -180,7 +180,7 @@ function SecondaryPageTitlebar({ hideBottomBorder={hideBottomBorder} > -
{titlebar}
+
{titlebar}
{isSmallScreen ? : null} ) diff --git a/src/lib/metadata-policy-curated-relays.ts b/src/lib/metadata-policy-curated-relays.ts index 1a7a0469..cbff8af1 100644 --- a/src/lib/metadata-policy-curated-relays.ts +++ b/src/lib/metadata-policy-curated-relays.ts @@ -50,7 +50,28 @@ export function isMetadataPolicyCuratedRelay(url: string): boolean { return key.length > 0 && getCuratedRelayKeySet().has(key) } +let profileRelayKeySet: ReadonlySet | null = null + +function getProfileRelayKeySet(): ReadonlySet { + if (!profileRelayKeySet) { + const out = new Set() + for (const u of PROFILE_RELAY_URLS) { + const key = relayKeyForCuratedSet(u) + if (key) out.add(key) + } + profileRelayKeySet = out + } + return profileRelayKeySet +} + +/** {@link PROFILE_RELAY_URLS} — kind-0 / profile hydration mirrors allowed under metadata-only reads. */ +export function isMetadataPolicyProfileRelay(url: string): boolean { + const key = relayKeyForCuratedSet(url) + return key.length > 0 && getProfileRelayKeySet().has(key) +} + /** For tests: reset lazy-built key set after constant changes. */ export function resetMetadataPolicyCuratedRelayKeysForTests(): void { curatedRelayKeySet = null + profileRelayKeySet = null } diff --git a/src/lib/read-only-relay-personal.test.ts b/src/lib/read-only-relay-personal.test.ts index 75f5b809..0a84b18a 100644 --- a/src/lib/read-only-relay-personal.test.ts +++ b/src/lib/read-only-relay-personal.test.ts @@ -73,7 +73,7 @@ describe('read-only-relay-personal', () => { expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual(urls) }) - it('metadata-only policy blocks ad-hoc reads at network level, not in sanitizeRelayUrlsForFetch', () => { + it('metadata-only policy blocks ad-hoc feed relays but allows profile mirrors at connect time', () => { setRestrictConnectionsToMetadataRelaysOnly(true) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://relay.example.com/']), { viewerActive: true }) const urls = [ @@ -84,6 +84,8 @@ describe('read-only-relay-personal', () => { ] expect(sanitizeRelayUrlsForFetch(urls)).toEqual(urls) expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true) + expect(isRelayConnectionAllowedForViewer('wss://relay.damus.io/')).toBe(true) + expect(isRelayConnectionAllowedForViewer('wss://relay.example.com/')).toBe(true) expect(isRelayConnectionAllowedForViewer('wss://theforest.nostr1.com/')).toBe(false) expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false) }) @@ -102,6 +104,13 @@ describe('read-only-relay-personal', () => { expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false) }) + it('metadata-only policy allows profile bootstrap relays at connect time', () => { + setRestrictConnectionsToMetadataRelaysOnly(true) + setViewerPersonalRelayKeys(new Set(), { viewerActive: true }) + expect(isRelayConnectionAllowedForViewer('wss://relay.damus.io/')).toBe(true) + expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true) + }) + it('metadata-only bypass allows relays outside personal lists', () => { setRestrictConnectionsToMetadataRelaysOnly(true) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true }) diff --git a/src/lib/read-only-relay-personal.ts b/src/lib/read-only-relay-personal.ts index f7bfb517..db9b0ba6 100644 --- a/src/lib/read-only-relay-personal.ts +++ b/src/lib/read-only-relay-personal.ts @@ -1,10 +1,7 @@ import { - DEFAULT_FAVORITE_RELAYS, - FAST_READ_RELAY_URLS, - FAST_WRITE_RELAY_URLS, READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS } from '@/constants' -import { isMetadataPolicyCuratedRelay } from '@/lib/metadata-policy-curated-relays' +import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays' import { filterAggrNostrLandUnlessViewerEligible, getViewerRelayStackNostrLandAggrEligible, @@ -47,29 +44,7 @@ export function isMetadataRelaysOnlyBypassActive(): boolean { return metadataRelaysOnlyBypassDepth > 0 } -let metadataPolicyBootstrapBlockedKeys: ReadonlySet | null = null - -function getMetadataPolicyBootstrapBlockedKeys(): ReadonlySet { - if (!metadataPolicyBootstrapBlockedKeys) { - const out = new Set() - for (const list of [FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, DEFAULT_FAVORITE_RELAYS]) { - for (const u of list) { - const key = relayUrlKey(u) - if (key) out.add(key) - } - } - metadataPolicyBootstrapBlockedKeys = out - } - return metadataPolicyBootstrapBlockedKeys -} - -/** True when URL is only a generic bootstrap mirror (FAST_READ / FAST_WRITE / default favorites). */ -export function isMetadataPolicyBootstrapRelay(url: string): boolean { - const key = relayUrlKey(url) - return key.length > 0 && getMetadataPolicyBootstrapBlockedKeys().has(key) -} - -/** Logged-in viewer with metadata-only mode: block FAST_READ widening, keep curated stacks. */ +/** Logged-in viewer with metadata-only mode: only connect reads to the viewer's relay lists. */ export function isMetadataRelaysOnlyPolicyActive(): boolean { return ( restrictConnectionsToMetadataRelaysOnly && @@ -84,14 +59,13 @@ export function isRelayUrlInViewerMetadataLists(url: string): boolean { } /** - * Under metadata-only policy: viewer lists, Nostr Land aggr, and {@link isMetadataPolicyCuratedRelay} - * (profile / read-only / searchable / document stacks). Blocks ad-hoc relays and FAST_READ bootstrap only. + * Under metadata-only policy: viewer NIP-65 / favorites / cache / HTTP lists, plus aggr.nostr.land when + * wss://nostr.land is listed, plus {@link PROFILE_RELAY_URLS} for kind-0 / profile hydration. */ export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean { if (isRelayUrlInViewerMetadataLists(url)) return true if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true - if (isMetadataPolicyCuratedRelay(url)) return true - if (isMetadataPolicyBootstrapRelay(url)) return false + if (isMetadataPolicyProfileRelay(url)) return true return false } diff --git a/src/lib/viewport-height.ts b/src/lib/viewport-height.ts new file mode 100644 index 00000000..7aaaa40b --- /dev/null +++ b/src/lib/viewport-height.ts @@ -0,0 +1,29 @@ +/** Sync `--vh` to the visible viewport (px). Used for mobile min-height and legacy call sites. */ +export function syncViewportHeightCssVar(): void { + const h = window.visualViewport?.height ?? window.innerHeight + document.documentElement.style.setProperty('--vh', `${h}px`) +} + +/** Keep `--vh` aligned when the window, visual viewport, or root layout (e.g. font scaling) changes. */ +export function installViewportHeightListeners(): () => void { + const sync = () => syncViewportHeightCssVar() + + window.addEventListener('resize', sync) + window.addEventListener('orientationchange', sync) + window.visualViewport?.addEventListener('resize', sync) + + let ro: ResizeObserver | undefined + if (typeof ResizeObserver !== 'undefined') { + ro = new ResizeObserver(sync) + ro.observe(document.documentElement) + } + + sync() + + return () => { + window.removeEventListener('resize', sync) + window.removeEventListener('orientationchange', sync) + window.visualViewport?.removeEventListener('resize', sync) + ro?.disconnect() + } +} diff --git a/src/main.tsx b/src/main.tsx index 89345278..5edcfe96 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,9 +14,11 @@ import { initI18n } from './i18n' import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service' import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery' import { initPwaUpdate } from './lib/pwa-update' +import { installViewportHeightListeners } from './lib/viewport-height' installStaleBuildChunkRecovery() initPwaUpdate() +installViewportHeightListeners() declare global { interface Window { @@ -24,13 +26,6 @@ declare global { } } -const setVh = () => { - document.documentElement.style.setProperty('--vh', `${window.innerHeight}px`) -} -window.addEventListener('resize', setVh) -window.addEventListener('orientationchange', setVh) -setVh() - const SESSION_STORAGE_KEY = 'jumble:session' async function bootstrap() { diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 7fc51f4a..7c800487 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -67,7 +67,7 @@ const NoteListPage = forwardRef((_, ref) => { const subHeader = ( <> {isSmallScreen ? : null} -
+

{feedPageTitle}

{homeSubHeader} diff --git a/src/providers/FontSizeProvider.tsx b/src/providers/FontSizeProvider.tsx index 5a896cd6..7692849b 100644 --- a/src/providers/FontSizeProvider.tsx +++ b/src/providers/FontSizeProvider.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useEffect, useState } from 'react' +import { syncViewportHeightCssVar } from '@/lib/viewport-height' import storage from '@/services/local-storage.service' import { TFontSize } from '@/types' @@ -38,6 +39,7 @@ export function FontSizeProvider({ children }: { children: React.ReactNode }) { } root.style.setProperty('--content-font-size', sizes[fontSize]) + requestAnimationFrame(() => syncViewportHeightCssVar()) }, [fontSize]) const setFontSize = (newFontSize: TFontSize) => { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 10a7160e..de7c17ab 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -42,6 +42,8 @@ import { isReadOnlyIndexerRelay, isReadOnlyRelayAllowedForViewer, isRelayConnectionAllowedForViewer, + isMetadataRelaysOnlyPolicyActive, + isRestrictConnectionsToMetadataRelaysOnly, setViewerPersonalRelayKeys } from '@/lib/read-only-relay-personal' import { @@ -631,6 +633,10 @@ class ClientService extends EventTarget { setViewerBlockedRelayUrls([]) return } + /** Engage policy before any await so session hydrate cannot open PROFILE/FAST_WRITE stacks first. */ + if (isRestrictConnectionsToMetadataRelaysOnly()) { + setViewerPersonalRelayKeys(new Set(), { viewerActive: true }) + } try { const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS) setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null)) @@ -665,10 +671,25 @@ class ClientService extends EventTarget { } setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true }) syncViewerRelayStackNostrLandAggrEligible(urls) + this.closeMetadataPolicyDisallowedRelayConnections() + } + + /** Drop pooled WebSocket connections that violate the metadata-only read policy. */ + closeMetadataPolicyDisallowedRelayConnections(): void { + if (!isMetadataRelaysOnlyPolicyActive()) return + try { + const toClose = [...this.pool.listConnectionStatus().keys()].filter( + (url) => !isRelayConnectionAllowedForViewer(url) + ) + if (toClose.length > 0) this.pool.close(toClose) + } catch { + // ignore + } } /** NIP-66: fetch relay discovery events (30166) in background to supplement search/NIP support. */ private async fetchNip66RelayDiscovery(): Promise { + if (isMetadataRelaysOnlyPolicyActive()) return try { const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS])) const events = await this.queryService.query(