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 { @@ -40,7 +40,7 @@ export default function App(): JSX.Element {
<DeletedEventProvider>
<NostrProvider>
<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 />
<StartupSessionBanner />
<SlowConnectionHint />

13
src/PageManager.tsx

@ -1054,8 +1054,8 @@ function MainContentArea({ @@ -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 (
<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 overflow-hidden rounded-lg border border-border bg-card shadow-lg">
<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 rounded-lg border border-border bg-card shadow-lg">
{primaryNoteView ? (
// Show note view with back button
<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 }) { @@ -2432,12 +2432,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}}
>
<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
className="flex h-[var(--vh)] w-full bg-content-canvas"
style={{
maxWidth: '1920px'
}}
className="mx-auto flex h-full min-h-0 w-full max-w-[1920px] flex-1 bg-content-canvas"
>
<Suspense fallback={null}>
<SidebarLazy />
@ -2459,7 +2456,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2459,7 +2456,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
/>
</div>
{/* 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 ? (
<TopSecondaryStackPane
item={secondaryStack[secondaryStack.length - 1]!}

3
src/components/MetadataRelaysOnlySetting/index.tsx

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

2
src/components/Titlebar/index.tsx

@ -12,7 +12,7 @@ export function Titlebar({ @@ -12,7 +12,7 @@ export function Titlebar({
return (
<div
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',
className
)}

4
src/i18n/locales/de.ts

@ -108,8 +108,8 @@ export default { @@ -108,8 +108,8 @@ export default {
"Relay Settings": "Relay-Einstellungen",
"Relays and Storage Settings": "Relays und Speicher",
"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.":
"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.",
"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, 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",
"Add a new relay set": "Neues Relay-Set hinzufügen",
Add: "Hinzufügen",

4
src/i18n/locales/en.ts

@ -113,8 +113,8 @@ export default { @@ -113,8 +113,8 @@ export default {
"Relay Settings": "Relays and Storage Settings",
"Relays and Storage Settings": "Relays and Storage Settings",
"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 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, 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, 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",
"Add a new relay set": "Add a new relay set",
Add: "Add",

8
src/index.css

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

15
src/layouts/PrimaryPageLayout/index.tsx

@ -161,7 +161,7 @@ const PrimaryPageLayout = forwardRef( @@ -161,7 +161,7 @@ const PrimaryPageLayout = forwardRef(
active={current === pageName && display && !frozen}
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 ? (
<PrimaryPageTitlebar
hideBottomBorder={hideTitlebarBottomBorder}
@ -176,13 +176,7 @@ const PrimaryPageLayout = forwardRef( @@ -176,13 +176,7 @@ const PrimaryPageLayout = forwardRef(
<div
ref={scrollAreaRef}
tabIndex={-1}
className={
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'
}
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto"
>
{children}
<div className="h-4" />
@ -216,14 +210,13 @@ function PrimaryPageTitlebar({ @@ -216,14 +210,13 @@ function PrimaryPageTitlebar({
return (
<Titlebar
className={cn(
'py-1',
isSmallScreen ? 'pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]' : 'px-1'
)}
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" />
<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}
</div>
</Titlebar>

2
src/layouts/SecondaryPageLayout/index.tsx

@ -180,7 +180,7 @@ function SecondaryPageTitlebar({ @@ -180,7 +180,7 @@ function SecondaryPageTitlebar({
hideBottomBorder={hideBottomBorder}
>
<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}
</Titlebar>
)

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

@ -50,7 +50,28 @@ export function isMetadataPolicyCuratedRelay(url: string): boolean { @@ -50,7 +50,28 @@ export function isMetadataPolicyCuratedRelay(url: string): boolean {
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. */
export function resetMetadataPolicyCuratedRelayKeysForTests(): void {
curatedRelayKeySet = null
profileRelayKeySet = null
}

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

@ -73,7 +73,7 @@ describe('read-only-relay-personal', () => { @@ -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', () => { @@ -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', () => { @@ -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 })

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

@ -1,10 +1,7 @@ @@ -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 { @@ -47,29 +44,7 @@ export function isMetadataRelaysOnlyBypassActive(): boolean {
return metadataRelaysOnlyBypassDepth > 0
}
let metadataPolicyBootstrapBlockedKeys: ReadonlySet<string> | null = null
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. */
/** 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 { @@ -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
}

29
src/lib/viewport-height.ts

@ -0,0 +1,29 @@ @@ -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' @@ -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 { @@ -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() {

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

@ -67,7 +67,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => { @@ -67,7 +67,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
const subHeader = (
<>
{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>
</div>
{homeSubHeader}

2
src/providers/FontSizeProvider.tsx

@ -1,4 +1,5 @@ @@ -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 }) { @@ -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) => {

21
src/services/client.service.ts

@ -42,6 +42,8 @@ import { @@ -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 { @@ -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 { @@ -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<void> {
if (isMetadataRelaysOnlyPolicyActive()) return
try {
const discoveryRelays = Array.from(new Set([...FAST_READ_RELAY_URLS, ...NIP66_DISCOVERY_RELAY_URLS]))
const events = await this.queryService.query(

Loading…
Cancel
Save