Browse Source

bug-fix

imwald
Silberengel 3 weeks ago
parent
commit
8a441c30c2
  1. 39
      src/PageManager.tsx
  2. 8
      src/contexts/primary-page-context.tsx
  3. 29
      src/layouts/SecondaryPageLayout/index.tsx
  4. 20
      src/lib/sheet-dismiss-guard.ts
  5. 10
      src/pages/secondary/NotFoundPage/index.tsx
  6. 1
      src/pages/secondary/RelayPage/index.tsx
  7. 1
      src/pages/secondary/RelayReviewsPage/index.tsx
  8. 2
      src/services/navigation.service.ts

39
src/PageManager.tsx

@ -605,18 +605,8 @@ export function useSmartRelayNavigation() {
// Build contextual URL based on current page // Build contextual URL based on current page
const contextualUrl = buildRelayUrl(relayUrl, currentPrimaryPage) const contextualUrl = buildRelayUrl(relayUrl, currentPrimaryPage)
if (isSmallScreen) {
// Use primary note view on mobile
window.history.pushState(null, '', contextualUrl)
setPrimaryNoteView(
suspensePrimaryPage(<SecondaryRelayPageLazy url={relayUrl} index={0} hideTitlebar={true} />),
'relay'
)
} else {
// Desktop: always use secondary routing (will be rendered in drawer in single-pane, side panel in double-pane)
pushSecondaryPage(contextualUrl) pushSecondaryPage(contextualUrl)
} }
}
return { navigateToRelay } return { navigateToRelay }
} }
@ -642,16 +632,8 @@ export function useSmartRelayNavigationOptional() {
url.match(/\/relays\/(.+)$/) url.match(/\/relays\/(.+)$/)
const relayUrl = relayUrlMatch ? decodeURIComponent(relayUrlMatch[relayUrlMatch.length - 1]) : decodeURIComponent(url.replace(/.*\/relays\//, '')) const relayUrl = relayUrlMatch ? decodeURIComponent(relayUrlMatch[relayUrlMatch.length - 1]) : decodeURIComponent(url.replace(/.*\/relays\//, ''))
const contextualUrl = buildRelayUrl(relayUrl, currentPrimaryPage) const contextualUrl = buildRelayUrl(relayUrl, currentPrimaryPage)
if (isSmallScreen) {
window.history.pushState(null, '', contextualUrl)
setPrimaryNoteView(
suspensePrimaryPage(<SecondaryRelayPageLazy url={relayUrl} index={0} hideTitlebar={true} />),
'relay'
)
} else {
pushSecondaryPage(contextualUrl) pushSecondaryPage(contextualUrl)
} }
}
return { navigateToRelay } return { navigateToRelay }
} }
@ -1242,6 +1224,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
// Drawer handlers // Drawer handlers
const [drawerInitialEvent, setDrawerInitialEvent] = useState<Event | null>(null) const [drawerInitialEvent, setDrawerInitialEvent] = useState<Event | null>(null)
const openDrawer = useCallback((noteId: string, initialEvent?: Event) => { const openDrawer = useCallback((noteId: string, initialEvent?: Event) => {
noteStatsService.setBackgroundStatsPaused(true)
client.interruptBackgroundQueries()
setDrawerNoteId(noteId) setDrawerNoteId(noteId)
setDrawerInitialEvent(initialEvent ?? null) setDrawerInitialEvent(initialEvent ?? null)
setDrawerOpen(true) setDrawerOpen(true)
@ -1990,6 +1974,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const navigatePrimaryPageStable = useEventCallback(navigatePrimaryPage) const navigatePrimaryPageStable = useEventCallback(navigatePrimaryPage)
const goBack = () => { const goBack = () => {
if (primaryViewType === 'relay') {
setPrimaryNoteView(null)
return
}
if (primaryViewType === 'settings-sub') { if (primaryViewType === 'settings-sub') {
navigatePrimaryPage('settings') navigatePrimaryPage('settings')
return return
@ -2050,6 +2038,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
} }
recentSecondaryPushRef.current = { url, at: now } recentSecondaryPushRef.current = { url, at: now }
noteStatsService.setBackgroundStatsPaused(true)
client.interruptBackgroundQueries()
// Small screens render either the primary overlay OR the secondary stack — not both. // Small screens render either the primary overlay OR the secondary stack — not both.
// Clear overlays (e.g. full-screen note) so pushes from Seen-on, settings deep links, etc. show the target page. // Clear overlays (e.g. full-screen note) so pushes from Seen-on, settings deep links, etc. show the target page.
if (isSmallScreen && primaryNoteView) { if (isSmallScreen && primaryNoteView) {
@ -2282,8 +2273,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
setSinglePaneSheetOpen(shouldBeOpen) setSinglePaneSheetOpen(shouldBeOpen)
}, [panelMode, isSmallScreen, secondaryStack.length, drawerOpen]) }, [panelMode, isSmallScreen, secondaryStack.length, drawerOpen])
const primaryFrozen = const primaryObscured =
secondaryStack.length > 0 && (isSmallScreen || panelMode === 'double') secondaryStack.length > 0 || drawerOpen || primaryNoteView != null
const primaryFrozen = primaryObscured
useLayoutEffect(() => { useLayoutEffect(() => {
noteStatsService.setBackgroundStatsPaused(primaryFrozen) noteStatsService.setBackgroundStatsPaused(primaryFrozen)
@ -2298,7 +2291,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
navigate: navigatePrimaryPageStable, navigate: navigatePrimaryPageStable,
current: currentPrimaryPage, current: currentPrimaryPage,
currentPageProps, currentPageProps,
display: isSmallScreen ? secondaryStack.length === 0 : true, /** Double-pane keeps the feed visible (frozen); single-pane / mobile unmount primary while a panel is open. */
display: panelMode === 'double' || !primaryObscured,
frozen: primaryFrozen frozen: primaryFrozen
}), }),
[ [
@ -2306,7 +2300,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
currentPrimaryPage, currentPrimaryPage,
currentPageProps, currentPageProps,
isSmallScreen, isSmallScreen,
secondaryStack.length, panelMode,
primaryObscured,
primaryFrozen primaryFrozen
] ]
) )

8
src/contexts/primary-page-context.tsx

@ -13,13 +13,13 @@ export type PrimaryPageContextValue = {
/** Props passed to the current primary page (e.g. `{ spell: 'discussions' }` for spells). */ /** Props passed to the current primary page (e.g. `{ spell: 'discussions' }` for spells). */
currentPageProps: object | undefined currentPageProps: object | undefined
/** /**
* False on small screens while the secondary stack is open (primary feed unmounted). * False while a note drawer, secondary page, or mobile overlay covers the feed (primary unmounted).
* True on desktop double-pane so the left column stays visible. * True on desktop double-pane so the left column stays visible (but {@link frozen} pauses it).
*/ */
display: boolean display: boolean
/** /**
* True while a secondary panel is open: pause primary feed timelines / background stats * True while any secondary panel, note drawer, or mobile overlay is open: pause primary feed
* and preserve scroll position until the panel closes. * timelines / background stats and abort non-foreground relay queries.
*/ */
frozen: boolean frozen: boolean
} }

29
src/layouts/SecondaryPageLayout/index.tsx

@ -50,6 +50,9 @@ const SecondaryPageLayout = forwardRef(
enabled: mobileSwipeActive enabled: mobileSwipeActive
}) })
const shouldRenderTitlebar =
titlebar != null || (title != null && title !== '') || !hideBackButton
useImperativeHandle( useImperativeHandle(
ref, ref,
() => ({ () => ({
@ -99,15 +102,16 @@ const SecondaryPageLayout = forwardRef(
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)' paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}} }}
> >
{title && ( {shouldRenderTitlebar ? (
<SecondaryPageTitlebar <SecondaryPageTitlebar
title={title} title={title}
controls={controls} controls={controls}
hideBackButton={hideBackButton} hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder} hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar} titlebar={titlebar}
sticky={isSmallScreen}
/> />
)} ) : null}
{children} {children}
</div> </div>
{displayScrollToTopButton && <ScrollToTopButton />} {displayScrollToTopButton && <ScrollToTopButton />}
@ -118,7 +122,7 @@ const SecondaryPageLayout = forwardRef(
return ( return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}> <DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<div className="flex h-full min-h-0 min-w-0 flex-col"> <div className="flex h-full min-h-0 min-w-0 flex-col">
{title && ( {shouldRenderTitlebar ? (
<SecondaryPageTitlebar <SecondaryPageTitlebar
title={title} title={title}
controls={controls} controls={controls}
@ -126,7 +130,7 @@ const SecondaryPageLayout = forwardRef(
hideBottomBorder={hideTitlebarBottomBorder} hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar} titlebar={titlebar}
/> />
)} ) : null}
<div <div
ref={scrollAreaRef} ref={scrollAreaRef}
tabIndex={-1} tabIndex={-1}
@ -149,23 +153,30 @@ function SecondaryPageTitlebar({
controls, controls,
hideBackButton = false, hideBackButton = false,
hideBottomBorder = false, hideBottomBorder = false,
titlebar titlebar,
sticky = false
}: { }: {
title?: React.ReactNode title?: React.ReactNode
controls?: React.ReactNode controls?: React.ReactNode
hideBackButton?: boolean hideBackButton?: boolean
hideBottomBorder?: boolean hideBottomBorder?: boolean
titlebar?: React.ReactNode titlebar?: React.ReactNode
/** Keep back visible while the page scrolls (mobile secondary stack). */
sticky?: boolean
}): JSX.Element { }): JSX.Element {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { t } = useTranslation()
const titlebarInset = isSmallScreen const titlebarInset = isSmallScreen
? 'py-1 pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]' ? 'py-1 pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]'
: 'p-1' : 'p-1'
const stickyClass = sticky
? 'sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
: ''
if (titlebar) { if (titlebar) {
return ( return (
<Titlebar <Titlebar
className={cn('flex min-w-0 items-center gap-2', titlebarInset)} className={cn('flex min-w-0 items-center gap-2', titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
@ -176,18 +187,20 @@ function SecondaryPageTitlebar({
} }
return ( return (
<Titlebar <Titlebar
className={cn('flex min-w-0 gap-1 items-center font-semibold', titlebarInset)} className={cn('flex min-w-0 gap-1 items-center font-semibold', titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="flex min-w-0 flex-1 items-center justify-between gap-1"> <div className="flex min-w-0 flex-1 items-center justify-between gap-1">
{hideBackButton ? ( {hideBackButton ? (
title ? (
<div className="app-chrome-title flex w-fit items-center gap-2 truncate pl-2"> <div className="app-chrome-title flex w-fit items-center gap-2 truncate pl-2">
{title} {title}
</div> </div>
) : null
) : ( ) : (
<div className="flex min-w-0 flex-1 items-center"> <div className="flex min-w-0 flex-1 items-center">
<BackButton>{title}</BackButton> <BackButton>{title ?? t('back')}</BackButton>
</div> </div>
)} )}
<div className="flex shrink-0 flex-wrap items-center justify-end gap-0.5 min-w-0 max-w-[min(100%,14rem)] sm:max-w-none"> <div className="flex shrink-0 flex-wrap items-center justify-end gap-0.5 min-w-0 max-w-[min(100%,14rem)] sm:max-w-none">

20
src/lib/sheet-dismiss-guard.ts

@ -17,6 +17,17 @@ function eventComposedPath(e: RadixOutsideEvent): EventTarget[] {
return [] return []
} }
function pathIncludesPortaledOverlay(path: EventTarget[]): boolean {
return path.some((node) => {
if (!(node instanceof HTMLElement)) return false
if (node.tagName.toLowerCase() === 'bc-modal') return true
if (node.hasAttribute('data-radix-dialog-content')) return true
if (node.hasAttribute('data-radix-dialog-overlay')) return true
if (node.getAttribute('role') === 'dialog' && node.closest('[data-radix-portal]')) return true
return false
})
}
export function preventRadixSheetCloseForPortaledOverlay(e: RadixOutsideEvent): void { export function preventRadixSheetCloseForPortaledOverlay(e: RadixOutsideEvent): void {
if (typeof document === 'undefined') return if (typeof document === 'undefined') return
if (document.body.classList.contains('yarl__no_scroll')) { if (document.body.classList.contains('yarl__no_scroll')) {
@ -24,10 +35,11 @@ export function preventRadixSheetCloseForPortaledOverlay(e: RadixOutsideEvent):
return return
} }
const path = eventComposedPath(e) const path = eventComposedPath(e)
const inBitcoinConnectModal = path.some( if (pathIncludesPortaledOverlay(path)) {
(node) => node instanceof HTMLElement && node.tagName.toLowerCase() === 'bc-modal' e.preventDefault()
) return
if (inBitcoinConnectModal) { }
if (document.querySelector('[data-radix-dialog-content][data-state="open"]')) {
e.preventDefault() e.preventDefault()
} }
} }

10
src/pages/secondary/NotFoundPage/index.tsx

@ -2,12 +2,20 @@ import NotFound from '@/components/NotFound'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef, useCallback, useState } from 'react' import { forwardRef, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
const NotFoundPage = forwardRef(({ index }: { index?: number }, ref) => { const NotFoundPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const [contentKey, setContentKey] = useState(0) const [contentKey, setContentKey] = useState(0)
const bump = useCallback(() => setContentKey((k) => k + 1), []) const bump = useCallback(() => setContentKey((k) => k + 1), [])
return ( return (
<SecondaryPageLayout ref={ref} index={index} controls={<RefreshButton onClick={bump} />}> <SecondaryPageLayout
ref={ref}
index={index}
title={t('Lost in the void')}
hideBackButton={false}
controls={<RefreshButton onClick={bump} />}
>
<div key={contentKey}> <div key={contentKey}>
<NotFound /> <NotFound />
</div> </div>

1
src/pages/secondary/RelayPage/index.tsx

@ -35,6 +35,7 @@ const RelayPage = forwardRef(({ url, index, hideTitlebar = false }: { url?: stri
ref={ref} ref={ref}
index={index} index={index}
title={hideTitlebar ? undefined : title} title={hideTitlebar ? undefined : title}
hideBackButton={false}
controls={hideTitlebar ? undefined : <RefreshButton onClick={bumpFeed} />} controls={hideTitlebar ? undefined : <RefreshButton onClick={bumpFeed} />}
displayScrollToTopButton displayScrollToTopButton
> >

1
src/pages/secondary/RelayReviewsPage/index.tsx

@ -81,6 +81,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
ref={ref} ref={ref}
index={index} index={index}
title={hideTitlebar ? undefined : title} title={hideTitlebar ? undefined : title}
hideBackButton={false}
controls={hideTitlebar ? undefined : <RefreshButton onClick={bumpFeed} />} controls={hideTitlebar ? undefined : <RefreshButton onClick={bumpFeed} />}
displayScrollToTopButton displayScrollToTopButton
> >

2
src/services/navigation.service.ts

@ -119,7 +119,7 @@ export class ComponentFactory {
} }
static createRelayPage(relayUrl: string): ReactNode { static createRelayPage(relayUrl: string): ReactNode {
return React.createElement(SecondaryRelayPage, { url: relayUrl, index: 0, hideTitlebar: true }) return React.createElement(SecondaryRelayPage, { url: relayUrl, index: 0 })
} }
static createProfilePage(profileId: string): ReactNode { static createProfilePage(profileId: string): ReactNode {

Loading…
Cancel
Save