Browse Source

make entire app responsive to changes in font size of the computer

imwald
Silberengel 3 weeks ago
parent
commit
46f64fa793
  1. 2
      src/App.tsx
  2. 13
      src/PageManager.tsx
  3. 3
      src/components/MetadataRelaysOnlySetting/index.tsx
  4. 2
      src/components/Titlebar/index.tsx
  5. 4
      src/i18n/locales/de.ts
  6. 4
      src/i18n/locales/en.ts
  7. 8
      src/index.css
  8. 15
      src/layouts/PrimaryPageLayout/index.tsx
  9. 2
      src/layouts/SecondaryPageLayout/index.tsx
  10. 21
      src/lib/metadata-policy-curated-relays.ts
  11. 11
      src/lib/read-only-relay-personal.test.ts
  12. 36
      src/lib/read-only-relay-personal.ts
  13. 29
      src/lib/viewport-height.ts
  14. 9
      src/main.tsx
  15. 2
      src/pages/primary/NoteListPage/index.tsx
  16. 2
      src/providers/FontSizeProvider.tsx
  17. 21
      src/services/client.service.ts

2
src/App.tsx

@ -40,7 +40,7 @@ export default function App(): JSX.Element {
<DeletedEventProvider> <DeletedEventProvider>
<NostrProvider> <NostrProvider>
<CacheBrowserProvider> <CacheBrowserProvider>
<div className="flex min-h-[100dvh] flex-col"> <div className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden max-md:h-auto max-md:max-h-none max-md:min-h-dvh max-md:overflow-visible">
<VersionUpdateBanner /> <VersionUpdateBanner />
<StartupSessionBanner /> <StartupSessionBanner />
<SlowConnectionHint /> <SlowConnectionHint />

13
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). // flex + min-h-0 + min-w-0 so primary pages get a real height in flex parents and can shrink horizontally (double-pane).
return ( return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col w-full pr-2 py-2"> <div className="flex min-h-0 min-w-0 flex-1 flex-col w-full px-2 pt-3 pb-2">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-border bg-card shadow-lg"> <div className="flex min-h-0 min-w-0 flex-1 flex-col rounded-lg border border-border bg-card shadow-lg">
{primaryNoteView ? ( {primaryNoteView ? (
// Show note view with back button // Show note view with back button
<div className="flex h-full min-h-0 min-w-0 w-full flex-col"> <div className="flex h-full min-h-0 min-w-0 w-full flex-col">
@ -2432,12 +2432,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}} }}
> >
<NoteDrawerContext.Provider value={{ openDrawer, closeDrawer, isDrawerOpen: drawerOpen, drawerNoteId, drawerInitialEvent }}> <NoteDrawerContext.Provider value={{ openDrawer, closeDrawer, isDrawerOpen: drawerOpen, drawerNoteId, drawerInitialEvent }}>
<div className="flex flex-col items-center bg-content-canvas"> <div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden bg-content-canvas">
<div <div
className="flex h-[var(--vh)] w-full bg-content-canvas" className="mx-auto flex h-full min-h-0 w-full max-w-[1920px] flex-1 bg-content-canvas"
style={{
maxWidth: '1920px'
}}
> >
<Suspense fallback={null}> <Suspense fallback={null}>
<SidebarLazy /> <SidebarLazy />
@ -2459,7 +2456,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
/> />
</div> </div>
{/* Right: secondary stack — max width so left pane keeps space on small desktops */} {/* Right: secondary stack — max width so left pane keeps space on small desktops */}
<div className="flex h-full min-h-0 w-[min(1042px,50vw)] shrink-0 flex-col overflow-hidden border-l border-border bg-muted/25"> <div className="flex h-full min-h-0 w-[min(1042px,50vw)] shrink-0 flex-col overflow-hidden border-l border-border bg-muted/25 px-2 pt-3 pb-2">
{secondaryStack.length > 0 ? ( {secondaryStack.length > 0 ? (
<TopSecondaryStackPane <TopSecondaryStackPane
item={secondaryStack[secondaryStack.length - 1]!} item={secondaryStack[secondaryStack.length - 1]!}

3
src/components/MetadataRelaysOnlySetting/index.tsx

@ -21,6 +21,7 @@ export default function MetadataRelaysOnlySetting() {
storage.setRestrictRelaysToMetadataLists(checked) storage.setRestrictRelaysToMetadataLists(checked)
setRestrictConnectionsToMetadataRelaysOnly(checked) setRestrictConnectionsToMetadataRelaysOnly(checked)
client.interruptBackgroundQueries({ closePooledRelayConnections: true }) client.interruptBackgroundQueries({ closePooledRelayConnections: true })
client.closeMetadataPolicyDisallowedRelayConnections()
} }
return ( return (
@ -31,7 +32,7 @@ export default function MetadataRelaysOnlySetting() {
</div> </div>
<div className="text-muted-foreground text-xs max-w-xl"> <div className="text-muted-foreground text-xs max-w-xl">
{t( {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.'
)} )}
</div> </div>
</div> </div>

2
src/components/Titlebar/index.tsx

@ -12,7 +12,7 @@ export function Titlebar({
return ( return (
<div <div
className={cn( className={cn(
'imwald-titlebar-fog sticky top-0 w-full h-12 z-40 bg-background [&_svg]:size-5 [&_svg]:shrink-0 select-none', 'imwald-titlebar-fog sticky top-0 z-40 flex w-full min-h-12 shrink-0 flex-col justify-center overflow-visible bg-background py-1.5 [&_svg]:size-5 [&_svg]:shrink-0 select-none',
!hideBottomBorder && 'border-b border-border', !hideBottomBorder && 'border-b border-border',
className className
)} )}

4
src/i18n/locales/de.ts

@ -108,8 +108,8 @@ export default {
"Relay Settings": "Relay-Einstellungen", "Relay Settings": "Relay-Einstellungen",
"Relays and Storage Settings": "Relays und Speicher", "Relays and Storage Settings": "Relays und Speicher",
"Only my relay lists": "Nur meine Relay-Listen", "Only my relay lists": "Nur meine Relay-Listen",
"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.":
"Wenn aktiv, werden nur noch Lese-Verbindungen zu Relays auf deinen Listen (plus Profil- und Suchindex-Relays) geöffnet. Veröffentlichen bleibt unverändert. Relay-Entdecken und Suche sind ausgenommen.", "Wenn aktiv, bleiben Lese-Verbindungen auf deinen Listen plus den eingebauten Profilindex-Relays (profiles.nostr1.com, relay.damus.io, …). Andere Relays für Feeds, Threads oder Suche werden nur bei Listeneintrag genutzt. Veröffentlichen bleibt unverändert. Relay-Entdecken und Suche sind ausgenommen.",
"Relay set name": "Relay-Set Name", "Relay set name": "Relay-Set Name",
"Add a new relay set": "Neues Relay-Set hinzufügen", "Add a new relay set": "Neues Relay-Set hinzufügen",
Add: "Hinzufügen", Add: "Hinzufügen",

4
src/i18n/locales/en.ts

@ -113,8 +113,8 @@ export default {
"Relay Settings": "Relays and Storage Settings", "Relay Settings": "Relays and Storage Settings",
"Relays and Storage Settings": "Relays and Storage Settings", "Relays and Storage Settings": "Relays and Storage Settings",
"Only my relay lists": "Only my relay lists", "Only my relay lists": "Only my relay lists",
"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.":
"When on, the app stops widening feeds to generic public read relays (FAST_READ) and random author or hint relays. Your relay lists, profile and search index relays, document relays, and aggr.nostr.land (with Nostr Land) still work. Publishing is unchanged. Relay explore and Search pages are exempt.", "When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.",
"Relay set name": "Relay set name", "Relay set name": "Relay set name",
"Add a new relay set": "Add a new relay set", "Add a new relay set": "Add a new relay set",
Add: "Add", Add: "Add",

8
src/index.css

@ -17,6 +17,14 @@
--bc-color-brand-button-text-dark: hsl(var(--primary-foreground)); --bc-color-brand-button-text-dark: hsl(var(--primary-foreground));
} }
@media (min-width: 769px) {
html,
body,
#root {
height: 100%;
}
}
input, input,
textarea, textarea,
button { button {

15
src/layouts/PrimaryPageLayout/index.tsx

@ -161,7 +161,7 @@ const PrimaryPageLayout = forwardRef(
active={current === pageName && display && !frozen} active={current === pageName && display && !frozen}
scrollAreaRef={scrollAreaRef} scrollAreaRef={scrollAreaRef}
> >
<div className="relative flex h-full min-h-0 min-w-0 flex-col"> <div className="flex h-full min-h-0 min-w-0 flex-col">
{hasTitlebarRow ? ( {hasTitlebarRow ? (
<PrimaryPageTitlebar <PrimaryPageTitlebar
hideBottomBorder={hideTitlebarBottomBorder} hideBottomBorder={hideTitlebarBottomBorder}
@ -176,13 +176,7 @@ const PrimaryPageLayout = forwardRef(
<div <div
ref={scrollAreaRef} ref={scrollAreaRef}
tabIndex={-1} tabIndex={-1}
className={ className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto"
subHeader
? 'min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto'
: hasTitlebarRow
? 'absolute bottom-0 left-0 right-0 top-12 min-w-0 overflow-y-auto overflow-x-auto'
: 'absolute bottom-0 left-0 right-0 top-0 min-w-0 overflow-y-auto overflow-x-auto'
}
> >
{children} {children}
<div className="h-4" /> <div className="h-4" />
@ -216,14 +210,13 @@ function PrimaryPageTitlebar({
return ( return (
<Titlebar <Titlebar
className={cn( className={cn(
'py-1',
isSmallScreen ? 'pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]' : 'px-1' isSmallScreen ? 'pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]' : 'px-1'
)} )}
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<div className="flex h-full w-full min-w-0 items-center gap-2"> <div className="flex w-full min-w-0 items-center gap-2">
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="relative min-h-0 min-w-0 flex-1 h-full">{children}</div> <div className="relative min-w-0 flex-1">{children}</div>
{showTrailingActiveRelays ? <ActiveRelaysTitlebarButton /> : null} {showTrailingActiveRelays ? <ActiveRelaysTitlebarButton /> : null}
</div> </div>
</Titlebar> </Titlebar>

2
src/layouts/SecondaryPageLayout/index.tsx

@ -180,7 +180,7 @@ function SecondaryPageTitlebar({
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="min-h-0 min-w-0 flex-1 h-full">{titlebar}</div> <div className="min-w-0 flex-1">{titlebar}</div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null} {isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</Titlebar> </Titlebar>
) )

21
src/lib/metadata-policy-curated-relays.ts

@ -50,7 +50,28 @@ export function isMetadataPolicyCuratedRelay(url: string): boolean {
return key.length > 0 && getCuratedRelayKeySet().has(key) return key.length > 0 && getCuratedRelayKeySet().has(key)
} }
let profileRelayKeySet: ReadonlySet<string> | null = null
function getProfileRelayKeySet(): ReadonlySet<string> {
if (!profileRelayKeySet) {
const out = new Set<string>()
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. */ /** For tests: reset lazy-built key set after constant changes. */
export function resetMetadataPolicyCuratedRelayKeysForTests(): void { export function resetMetadataPolicyCuratedRelayKeysForTests(): void {
curatedRelayKeySet = null curatedRelayKeySet = null
profileRelayKeySet = null
} }

11
src/lib/read-only-relay-personal.test.ts

@ -73,7 +73,7 @@ describe('read-only-relay-personal', () => {
expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual(urls) 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) setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://relay.example.com/']), { viewerActive: true }) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://relay.example.com/']), { viewerActive: true })
const urls = [ const urls = [
@ -84,6 +84,8 @@ describe('read-only-relay-personal', () => {
] ]
expect(sanitizeRelayUrlsForFetch(urls)).toEqual(urls) expect(sanitizeRelayUrlsForFetch(urls)).toEqual(urls)
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true) 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://theforest.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).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) 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', () => { it('metadata-only bypass allows relays outside personal lists', () => {
setRestrictConnectionsToMetadataRelaysOnly(true) setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true }) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true })

36
src/lib/read-only-relay-personal.ts

@ -1,10 +1,7 @@
import { import {
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS,
FAST_WRITE_RELAY_URLS,
READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS
} from '@/constants' } from '@/constants'
import { isMetadataPolicyCuratedRelay } from '@/lib/metadata-policy-curated-relays' import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays'
import { import {
filterAggrNostrLandUnlessViewerEligible, filterAggrNostrLandUnlessViewerEligible,
getViewerRelayStackNostrLandAggrEligible, getViewerRelayStackNostrLandAggrEligible,
@ -47,29 +44,7 @@ export function isMetadataRelaysOnlyBypassActive(): boolean {
return metadataRelaysOnlyBypassDepth > 0 return metadataRelaysOnlyBypassDepth > 0
} }
let metadataPolicyBootstrapBlockedKeys: ReadonlySet<string> | null = null /** Logged-in viewer with metadata-only mode: only connect reads to the viewer's relay lists. */
function getMetadataPolicyBootstrapBlockedKeys(): ReadonlySet<string> {
if (!metadataPolicyBootstrapBlockedKeys) {
const out = new Set<string>()
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. */
export function isMetadataRelaysOnlyPolicyActive(): boolean { export function isMetadataRelaysOnlyPolicyActive(): boolean {
return ( return (
restrictConnectionsToMetadataRelaysOnly && restrictConnectionsToMetadataRelaysOnly &&
@ -84,14 +59,13 @@ export function isRelayUrlInViewerMetadataLists(url: string): boolean {
} }
/** /**
* Under metadata-only policy: viewer lists, Nostr Land aggr, and {@link isMetadataPolicyCuratedRelay} * Under metadata-only policy: viewer NIP-65 / favorites / cache / HTTP lists, plus aggr.nostr.land when
* (profile / read-only / searchable / document stacks). Blocks ad-hoc relays and FAST_READ bootstrap only. * wss://nostr.land is listed, plus {@link PROFILE_RELAY_URLS} for kind-0 / profile hydration.
*/ */
export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean { export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean {
if (isRelayUrlInViewerMetadataLists(url)) return true if (isRelayUrlInViewerMetadataLists(url)) return true
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true
if (isMetadataPolicyCuratedRelay(url)) return true if (isMetadataPolicyProfileRelay(url)) return true
if (isMetadataPolicyBootstrapRelay(url)) return false
return false return false
} }

29
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()
}
}

9
src/main.tsx

@ -14,9 +14,11 @@ import { initI18n } from './i18n'
import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service' import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service'
import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery' import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery'
import { initPwaUpdate } from './lib/pwa-update' import { initPwaUpdate } from './lib/pwa-update'
import { installViewportHeightListeners } from './lib/viewport-height'
installStaleBuildChunkRecovery() installStaleBuildChunkRecovery()
initPwaUpdate() initPwaUpdate()
installViewportHeightListeners()
declare global { declare global {
interface Window { 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' const SESSION_STORAGE_KEY = 'jumble:session'
async function bootstrap() { async function bootstrap() {

2
src/pages/primary/NoteListPage/index.tsx

@ -67,7 +67,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
const subHeader = ( const subHeader = (
<> <>
{isSmallScreen ? <FavoriteRelaysActiveStripMobileBar /> : null} {isSmallScreen ? <FavoriteRelaysActiveStripMobileBar /> : null}
<div className="w-full min-w-0 border-b border-border/80 bg-background px-3 py-2 sm:px-4"> <div className="w-full min-w-0 border-b border-border/80 bg-background px-3 py-2.5 sm:px-4 sm:py-3">
<h1 className="app-chrome-title leading-tight tracking-tight">{feedPageTitle}</h1> <h1 className="app-chrome-title leading-tight tracking-tight">{feedPageTitle}</h1>
</div> </div>
{homeSubHeader} {homeSubHeader}

2
src/providers/FontSizeProvider.tsx

@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { syncViewportHeightCssVar } from '@/lib/viewport-height'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { TFontSize } from '@/types' import { TFontSize } from '@/types'
@ -38,6 +39,7 @@ export function FontSizeProvider({ children }: { children: React.ReactNode }) {
} }
root.style.setProperty('--content-font-size', sizes[fontSize]) root.style.setProperty('--content-font-size', sizes[fontSize])
requestAnimationFrame(() => syncViewportHeightCssVar())
}, [fontSize]) }, [fontSize])
const setFontSize = (newFontSize: TFontSize) => { const setFontSize = (newFontSize: TFontSize) => {

21
src/services/client.service.ts

@ -42,6 +42,8 @@ import {
isReadOnlyIndexerRelay, isReadOnlyIndexerRelay,
isReadOnlyRelayAllowedForViewer, isReadOnlyRelayAllowedForViewer,
isRelayConnectionAllowedForViewer, isRelayConnectionAllowedForViewer,
isMetadataRelaysOnlyPolicyActive,
isRestrictConnectionsToMetadataRelaysOnly,
setViewerPersonalRelayKeys setViewerPersonalRelayKeys
} from '@/lib/read-only-relay-personal' } from '@/lib/read-only-relay-personal'
import { import {
@ -631,6 +633,10 @@ class ClientService extends EventTarget {
setViewerBlockedRelayUrls([]) setViewerBlockedRelayUrls([])
return return
} }
/** Engage policy before any await so session hydrate cannot open PROFILE/FAST_WRITE stacks first. */
if (isRestrictConnectionsToMetadataRelaysOnly()) {
setViewerPersonalRelayKeys(new Set(), { viewerActive: true })
}
try { try {
const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS) const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS)
setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null)) setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null))
@ -665,10 +671,25 @@ class ClientService extends EventTarget {
} }
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true }) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true })
syncViewerRelayStackNostrLandAggrEligible(urls) 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. */ /** NIP-66: fetch relay discovery events (30166) in background to supplement search/NIP support. */
private async fetchNip66RelayDiscovery(): Promise<void> { private async fetchNip66RelayDiscovery(): Promise<void> {
if (isMetadataRelaysOnlyPolicyActive()) return
try { try {
const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS])) const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS]))
const events = await this.queryService.query( const events = await this.queryService.query(

Loading…
Cancel
Save