Browse Source

keep header fixed on scroll down

imwald
Silberengel 2 weeks ago
parent
commit
6caff4767d
  1. 2
      src/App.tsx
  2. 20
      src/PageManager.tsx
  3. 3
      src/components/Embedded/EmbeddedNoteProviders.tsx
  4. 7
      src/components/NoteCard/RepostNoteCard.tsx
  5. 7
      src/components/NoteCard/index.tsx
  6. 7
      src/components/ReplyNote/index.tsx
  7. 7
      src/components/ReplyNoteList/index.tsx
  8. 2
      src/constants.ts
  9. 42
      src/index.css
  10. 64
      src/layouts/PrimaryPageLayout/index.tsx
  11. 32
      src/layouts/SecondaryPageLayout/index.tsx
  12. 22
      src/lib/error-suppression.ts
  13. 17
      src/lib/mobile-primary-feed-scroll.ts
  14. 72
      src/pages/primary/SpellsPage/index.tsx
  15. 5
      src/services/relay-info.service.ts

2
src/App.tsx

@ -38,7 +38,7 @@ export default function App(): JSX.Element { @@ -38,7 +38,7 @@ export default function App(): JSX.Element {
<DeletedEventProvider>
<NostrProvider>
<CacheBrowserProvider>
<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">
<div className="flex h-dvh max-h-dvh min-h-0 flex-col overflow-hidden max-md:h-[var(--vh)] max-md:max-h-[var(--vh)] max-md:min-h-0">
<VersionUpdateBanner />
<StartupSessionBanner />
<SlowConnectionHint />

20
src/PageManager.tsx

@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button' @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import logger from '@/lib/logger'
import {
captureMobilePrimaryFeedScrollFromWindow,
captureMobilePrimaryFeedScroll,
peekMobilePrimaryFeedScroll
} from '@/lib/mobile-primary-feed-scroll'
import { useMobileSwipeBackOnElement } from '@/lib/mobile-swipe-back'
@ -2054,7 +2054,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2054,7 +2054,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}
if (isSmallScreen && currentPrimaryPage) {
captureMobilePrimaryFeedScrollFromWindow(currentPrimaryPage)
captureMobilePrimaryFeedScroll(currentPrimaryPage)
}
// Small screens overlay the frozen feed; clear full-screen primary overlays so the secondary page shows.
@ -2393,13 +2393,13 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2393,13 +2393,13 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}}
>
<NoteDrawerContext.Provider value={{ openDrawer, closeDrawer, isDrawerOpen: drawerOpen, drawerNoteId, drawerInitialEvent }}>
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-content-canvas min-h-[var(--vh)]">
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-content-canvas">
<LiveActivitiesStrip placement="mobile" />
{primaryNoteView ? (
// Show primary note view with back button on mobile
<div
ref={setMobilePrimarySwipeRoot}
className="flex min-h-0 flex-1 flex-col h-full w-full touch-pan-y"
className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden touch-pan-y"
>
<ImwaldBrandBar />
<div className="flex gap-1 border-b border-border p-1 items-center justify-between font-semibold">
@ -2423,15 +2423,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2423,15 +2423,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
</div>
<RefreshButton onClick={triggerPrimaryPanelRefresh} />
</div>
<div className="flex-1 overflow-auto">
<div className="page-scroll-y min-h-0 flex-1 basis-0 overflow-y-scroll overscroll-y-contain touch-pan-y">
{primaryNoteView}
</div>
</div>
) : (
<>
<div className="flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden">
<div
className={cn(
'block h-full min-h-0 min-w-0',
'flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden',
secondaryStack.length > 0 && 'hidden'
)}
aria-hidden={secondaryStack.length > 0}
@ -2441,12 +2441,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2441,12 +2441,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
{secondaryStack.length > 0 ? (
<div
ref={setMobileSecondarySwipeRoot}
className="flex min-h-0 min-w-0 flex-1 flex-col touch-pan-y bg-background"
className="flex min-h-0 min-w-0 flex-1 basis-0 flex-col overflow-hidden touch-pan-y bg-background"
>
<TopSecondaryStackPane item={secondaryStack[secondaryStack.length - 1]!} />
</div>
) : null}
</>
</div>
)}
</div>
<Suspense fallback={null}>
@ -2675,7 +2675,7 @@ function secondaryPanelUrlsMatch(stackUrl: string, locationUrl: string): boolean @@ -2675,7 +2675,7 @@ function secondaryPanelUrlsMatch(stackUrl: string, locationUrl: string): boolean
/** Mount only the top secondary frame so Back unmounts feeds/relays under the previous page. */
function TopSecondaryStackPane({
item,
className = 'block h-full min-h-0 min-w-0'
className = 'flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden'
}: {
item: TStackItem
className?: string

3
src/components/Embedded/EmbeddedNoteProviders.tsx

@ -1,11 +1,14 @@ @@ -1,11 +1,14 @@
import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider'
import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { ReplyProvider } from '@/providers/ReplyProvider'
/** Minimal providers for {@link EmbeddedNote} in isolated `createRoot` trees (e.g. Asciidoc). */
export default function EmbeddedNoteProviders({ children }: { children: React.ReactNode }) {
return (
<ContentPolicyProvider>
<DeletedEventProvider>
<ReplyProvider>{children}</ReplyProvider>
</DeletedEventProvider>
</ContentPolicyProvider>
)
}

7
src/components/NoteCard/RepostNoteCard.tsx

@ -1,7 +1,8 @@ @@ -1,7 +1,8 @@
import { ExtendedKind } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import { generateBech32IdFromATag, getFirstHexEventIdFromETags, tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import storage from '@/services/local-storage.service'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import client from '@/services/client.service'
@ -28,7 +29,9 @@ export default function RepostNoteCard({ @@ -28,7 +29,9 @@ export default function RepostNoteCard({
seenOnAllowlist?: readonly string[]
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const hideContentMentioningMutedUsers =
useContentPolicyOptional()?.hideContentMentioningMutedUsers ??
storage.getHideContentMentioningMutedUsers()
const [targetEvent, setTargetEvent] = useState<Event | null>(null)
const shouldHide = useMemo(() => {
if (!targetEvent) return true

7
src/components/NoteCard/index.tsx

@ -1,7 +1,8 @@ @@ -1,7 +1,8 @@
import { Skeleton } from '@/components/ui/skeleton'
import { isMentioningMutedUsers, isNip18RepostKind, isNip56ReportEvent } from '@/lib/event'
import ReportCard from '@/components/ReportCard'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import storage from '@/services/local-storage.service'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { Event } from 'nostr-tools'
@ -37,7 +38,9 @@ const NoteCard = memo(function NoteCard({ @@ -37,7 +38,9 @@ const NoteCard = memo(function NoteCard({
showPaymentAttestationAction?: boolean
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const hideContentMentioningMutedUsers =
useContentPolicyOptional()?.hideContentMentioningMutedUsers ??
storage.getHideContentMentioningMutedUsers()
const shouldHide = useMemo(() => {
if (filterMutedNotes && muteSetHas(mutePubkeySet, event.pubkey)) {
return true

7
src/components/ReplyNote/index.tsx

@ -18,7 +18,8 @@ import { getWebExternalReactionTargetUrl } from '@/lib/rss-article' @@ -18,7 +18,8 @@ import { getWebExternalReactionTargetUrl } from '@/lib/rss-article'
import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import storage from '@/services/local-storage.service'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -59,7 +60,9 @@ export default function ReplyNote({ @@ -59,7 +60,9 @@ export default function ReplyNote({
const { isSmallScreen } = useScreenSize()
const { navigateToNote } = useSmartNoteNavigation()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const hideContentMentioningMutedUsers =
useContentPolicyOptional()?.hideContentMentioningMutedUsers ??
storage.getHideContentMentioningMutedUsers()
const [showMuted, setShowMuted] = useState(false)
const reactionDisplay = useNotificationReactionDisplay(event)
const webReactionParentUrl = useMemo(

7
src/components/ReplyNoteList/index.tsx

@ -20,7 +20,8 @@ import { getCachedThreadContextEvents } from '@/lib/navigation-related-events' @@ -20,7 +20,8 @@ import { getCachedThreadContextEvents } from '@/lib/navigation-related-events'
import { toNote } from '@/lib/link'
import { generateBech32IdFromETag } from '@/lib/tag'
import { useSmartNoteNavigation } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import storage from '@/services/local-storage.service'
import { useMuteList } from '@/contexts/mute-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
@ -109,7 +110,9 @@ function ReplyNoteList({ @@ -109,7 +110,9 @@ function ReplyNoteList({
const { navigateToNote } = useSmartNoteNavigation()
const noteStats = useNoteStatsById(event.id)
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const hideContentMentioningMutedUsers =
useContentPolicyOptional()?.hideContentMentioningMutedUsers ??
storage.getHideContentMentioningMutedUsers()
const { pubkey: userPubkey } = useNostr()
const { blockedRelays, favoriteRelays } = useFavoriteRelays()
const { relayUrls: browsingRelayUrls } = useCurrentRelays()

2
src/constants.ts

@ -557,7 +557,7 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550 @@ -557,7 +557,7 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550
export const PROFILE_RELAY_URLS = [
'wss://profiles.nostr1.com',
'wss://profiles.nostrver.se/',
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com',
'wss://indexer.coracle.social/'
]

42
src/index.css

@ -25,6 +25,20 @@ @@ -25,6 +25,20 @@
}
}
/* Mobile: lock document scroll; feeds and note panels scroll inside .page-scroll-y regions. */
@media (max-width: 768px) {
html,
body {
height: 100%;
overflow: hidden;
}
#root {
height: 100%;
min-height: 0;
overflow: hidden;
}
}
input,
textarea,
button {
@ -96,27 +110,51 @@ @@ -96,27 +110,51 @@
display: none; /* Safari and Chrome */
}
/* Popover / select lists: keep a visible vertical scrollbar (not overlay-only). */
/*
* Primary/secondary pages, popovers, selects: visible vertical scrollbar (not overlay-only).
* Pair with Tailwind overflow-y-scroll on the same element.
*/
.page-scroll-y,
.popover-scroll-y {
overflow-y: scroll;
scrollbar-gutter: stable;
scrollbar-width: thin;
}
.page-scroll-y::-webkit-scrollbar,
.popover-scroll-y::-webkit-scrollbar {
width: 10px;
}
.page-scroll-y::-webkit-scrollbar-thumb,
.popover-scroll-y::-webkit-scrollbar-thumb {
border-radius: 9999px;
background-color: hsl(var(--muted-foreground) / 0.35);
}
.page-scroll-y::-webkit-scrollbar-thumb:hover,
.popover-scroll-y::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
.page-scroll-y::-webkit-scrollbar-track,
.popover-scroll-y::-webkit-scrollbar-track {
border-radius: 9999px;
background-color: hsl(var(--muted) / 0.45);
}
/* Narrow viewports: stronger scroll affordance (feed + note panels use inner scroll). */
@media (max-width: 768px) {
.page-scroll-y {
-webkit-overflow-scrolling: touch;
scrollbar-width: auto;
}
.page-scroll-y::-webkit-scrollbar {
width: 12px;
}
.page-scroll-y::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground) / 0.55);
}
.page-scroll-y::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.7);
}
}
/*
* Radix Select injects a sibling <style> that hides scrollbars on the viewport.
* That sheet loads after app CSS, so undo it with !important (see VIEWPORT_NAME in @radix-ui/react-select).

64
src/layouts/PrimaryPageLayout/index.tsx

@ -13,10 +13,11 @@ import { @@ -13,10 +13,11 @@ import {
} from '@/lib/keyboard-shortcuts'
import {
peekMobilePrimaryFeedScroll,
registerMobilePrimaryFeedScrollElement,
saveMobilePrimaryFeedScroll
} from '@/lib/mobile-primary-feed-scroll'
import { cn } from '@/lib/utils'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react'
const PrimaryPageLayout = forwardRef(
(
@ -39,7 +40,6 @@ const PrimaryPageLayout = forwardRef( @@ -39,7 +40,6 @@ const PrimaryPageLayout = forwardRef(
ref
) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const smallScreenScrollAreaRef = useRef<HTMLDivElement>(null)
const { isSmallScreen } = useScreenSize()
const { current, display, frozen } = usePrimaryPage()
const savedScrollTopRef = useRef(0)
@ -50,34 +50,45 @@ const PrimaryPageLayout = forwardRef( @@ -50,34 +50,45 @@ const PrimaryPageLayout = forwardRef(
() => ({
scrollToTop: (behavior: ScrollBehavior = 'smooth') => {
setTimeout(() => {
if (scrollAreaRef.current) {
return scrollAreaRef.current.scrollTo({ top: 0, behavior })
}
window.scrollTo({ top: 0, behavior })
scrollAreaRef.current?.scrollTo({ top: 0, behavior })
}, 10)
}
}),
[]
)
useLayoutEffect(() => {
if (!isSmallScreen) {
registerMobilePrimaryFeedScrollElement(null)
return
}
registerMobilePrimaryFeedScrollElement(scrollAreaRef.current)
return () => registerMobilePrimaryFeedScrollElement(null)
}, [isSmallScreen, display, current, pageName])
useEffect(() => {
if (!isSmallScreen || current !== pageName || frozen) return
if (!isSmallScreen || current !== pageName) return
const el = scrollAreaRef.current
if (!el) return
const handleScroll = () => {
saveMobilePrimaryFeedScroll(pageName, window.scrollY)
saveMobilePrimaryFeedScroll(pageName, el.scrollTop)
}
window.addEventListener('scroll', handleScroll, { passive: true })
el.addEventListener('scroll', handleScroll, { passive: true })
return () => {
handleScroll()
window.removeEventListener('scroll', handleScroll)
el.removeEventListener('scroll', handleScroll)
}
}, [current, frozen, isSmallScreen, pageName])
}, [current, isSmallScreen, pageName])
useEffect(() => {
if (!isSmallScreen || current !== pageName || !display) return
const top = peekMobilePrimaryFeedScroll(pageName)
requestAnimationFrame(() => {
window.scrollTo({ top, behavior: 'instant' })
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop = top
}
})
}, [current, display, isSmallScreen, pageName])
@ -125,25 +136,32 @@ const PrimaryPageLayout = forwardRef( @@ -125,25 +136,32 @@ const PrimaryPageLayout = forwardRef(
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={current === pageName && display && !frozen}>
<div
ref={smallScreenScrollAreaRef}
className="min-w-0 w-full overflow-x-hidden"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
<DeepBrowsingProvider
active={current === pageName && display && !frozen}
scrollAreaRef={scrollAreaRef}
>
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{hasTitlebarRow ? (
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
) : null}
{subHeader && <div className="shrink-0 w-full min-w-0 bg-background">{subHeader}</div>}
<div className="min-w-0 w-full">
{subHeader ? (
<div className="min-w-0 shrink-0 border-b border-border/80 bg-background">
{subHeader}
</div>
) : null}
<div
ref={scrollAreaRef}
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-hidden overscroll-y-contain touch-pan-y"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
{children}
</div>
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
@ -165,7 +183,7 @@ const PrimaryPageLayout = forwardRef( @@ -165,7 +183,7 @@ const PrimaryPageLayout = forwardRef(
<div
ref={scrollAreaRef}
tabIndex={-1}
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto"
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-auto overscroll-y-contain"
>
{children}
<div className="h-4" />

32
src/layouts/SecondaryPageLayout/index.tsx

@ -62,11 +62,9 @@ const SecondaryPageLayout = forwardRef( @@ -62,11 +62,9 @@ const SecondaryPageLayout = forwardRef(
)
useEffect(() => {
if (isSmallScreen) {
setTimeout(() => window.scrollTo({ top: 0 }), 10)
return
}
}, [])
if (!isSmallScreen) return
setTimeout(() => scrollAreaRef.current?.scrollTo({ top: 0 }), 10)
}, [isSmallScreen])
useEffect(() => {
if (isSmallScreen) return
@ -88,13 +86,8 @@ const SecondaryPageLayout = forwardRef( @@ -88,13 +86,8 @@ const SecondaryPageLayout = forwardRef(
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={currentIndex === index}>
<div
className="flex min-h-0 min-w-0 flex-1 flex-col touch-pan-y"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{shouldRenderTitlebar ? (
<SecondaryPageTitlebar
title={title}
@ -102,19 +95,26 @@ const SecondaryPageLayout = forwardRef( @@ -102,19 +95,26 @@ const SecondaryPageLayout = forwardRef(
hideBackButton={hideBackButton}
hideBottomBorder={hideTitlebarBottomBorder}
titlebar={titlebar}
sticky={isSmallScreen}
/>
) : null}
<div
ref={scrollAreaRef}
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-hidden overscroll-y-contain touch-pan-y"
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
{children}
</div>
{displayScrollToTopButton && <ScrollToTopButton />}
</div>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
return (
<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-1 flex-col overflow-hidden">
{shouldRenderTitlebar ? (
<SecondaryPageTitlebar
title={title}
@ -127,7 +127,7 @@ const SecondaryPageLayout = forwardRef( @@ -127,7 +127,7 @@ const SecondaryPageLayout = forwardRef(
<div
ref={scrollAreaRef}
tabIndex={-1}
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto"
className="page-scroll-y min-h-0 min-w-0 flex-1 basis-0 overflow-y-scroll overflow-x-auto overscroll-y-contain"
>
{children}
<div className="h-12" />

22
src/lib/error-suppression.ts

@ -115,6 +115,15 @@ function isExpectedDevAppNoise(message: string): boolean { @@ -115,6 +115,15 @@ function isExpectedDevAppNoise(message: string): boolean {
) {
return true
}
if (
message.includes('feeds.nostrarchives.com') &&
(message.includes('CORS') ||
message.includes('Gleiche-Quelle') ||
message.includes('Cross-Origin') ||
message.includes('Access-Control-Allow-Origin'))
) {
return true
}
if (
message.includes('localhost:4869') ||
message.includes('127.0.0.1:4869') ||
@ -126,11 +135,22 @@ function isExpectedDevAppNoise(message: string): boolean { @@ -126,11 +135,22 @@ function isExpectedDevAppNoise(message: string): boolean {
message.includes('[RelayOp]') ||
message.includes('connection failed') ||
message.includes('connection timed out') ||
message.includes('Local relay connection timeout')
message.includes('Local relay connection timeout') ||
message.includes('kann keine Verbindung') ||
message.includes('can\'t establish a connection') ||
message.includes("can't establish a connection")
) {
return true
}
}
if (
message.includes('profiles.nostrver.se') &&
(message.includes('kann keine Verbindung') ||
message.includes('can\'t establish a connection') ||
message.includes("can't establish a connection"))
) {
return true
}
if (message.includes('[FetchRelayLists] Network relay-list fetch exceeded budget')) {
return true
}

17
src/lib/mobile-primary-feed-scroll.ts

@ -1,8 +1,15 @@ @@ -1,8 +1,15 @@
import type { TPrimaryPageName } from '@/PageManager'
/** Persist primary feed window scroll across mobile secondary unmount (PageManager hides the feed while a panel is open). */
/** Persist primary feed scroll across mobile secondary navigation. */
const scrollByPage = new Map<TPrimaryPageName, number>()
/** Primary feed scroll container when using inner scroll (not window). */
let registeredScrollElement: HTMLElement | null = null
export function registerMobilePrimaryFeedScrollElement(el: HTMLElement | null): void {
registeredScrollElement = el
}
export function saveMobilePrimaryFeedScroll(page: TPrimaryPageName, top: number): void {
if (!Number.isFinite(top) || top < 0) return
scrollByPage.set(page, top)
@ -12,6 +19,12 @@ export function peekMobilePrimaryFeedScroll(page: TPrimaryPageName): number { @@ -12,6 +19,12 @@ export function peekMobilePrimaryFeedScroll(page: TPrimaryPageName): number {
return scrollByPage.get(page) ?? 0
}
export function captureMobilePrimaryFeedScroll(page: TPrimaryPageName): void {
const top = registeredScrollElement?.scrollTop ?? window.scrollY
saveMobilePrimaryFeedScroll(page, top)
}
/** @deprecated Use captureMobilePrimaryFeedScroll */
export function captureMobilePrimaryFeedScrollFromWindow(page: TPrimaryPageName): void {
saveMobilePrimaryFeedScroll(page, window.scrollY)
captureMobilePrimaryFeedScroll(page)
}

72
src/pages/primary/SpellsPage/index.tsx

@ -813,38 +813,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -813,38 +813,8 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
</Button>
)
return (
<PrimaryPageLayout
ref={layoutRef}
pageName="spells"
titlebar={
<div className="flex h-full w-full items-center justify-between gap-2 pr-1">
<div
className="app-chrome-title min-w-0 flex-1 truncate pl-3"
title={spellsTitlebarTitle}
>
{spellsTitlebarTitle}
</div>
<div className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={refreshSpellsFeedAndCatalog} />
<Button
variant="ghost"
size="titlebar-icon"
onClick={() => {
setSpellToEdit(null)
setSpellToClone(null)
setCreateOpen(true)
}}
title={t('Create a Spell')}
>
<Plus className="size-5" />
</Button>
</div>
</div>
}
displayScrollToTopButton
>
<div className="flex min-h-0 flex-1 flex-col gap-4 p-4">
const spellsSubHeader = (
<div className="flex flex-col gap-2 px-4 py-2.5 sm:px-4">
{selectedFauxSpell ? (
<div className="flex shrink-0 items-center">
<Button
@ -860,9 +830,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -860,9 +830,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
</div>
) : (
<>
{/* Spell picker + actions above the feed */}
<div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<>
{isSmallScreen ? (
<>
<Button
@ -914,7 +882,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -914,7 +882,6 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
</DropdownMenuContent>
</DropdownMenu>
)}
</>
<div className="flex shrink-0 flex-wrap items-center gap-2">
<Button
@ -1010,7 +977,42 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1010,7 +977,42 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
)}
</>
)}
</div>
)
return (
<PrimaryPageLayout
ref={layoutRef}
pageName="spells"
titlebar={
<div className="flex h-full w-full items-center justify-between gap-2 pr-1">
<div
className="app-chrome-title min-w-0 flex-1 truncate pl-3"
title={spellsTitlebarTitle}
>
{spellsTitlebarTitle}
</div>
<div className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={refreshSpellsFeedAndCatalog} />
<Button
variant="ghost"
size="titlebar-icon"
onClick={() => {
setSpellToEdit(null)
setSpellToClone(null)
setCreateOpen(true)
}}
title={t('Create a Spell')}
>
<Plus className="size-5" />
</Button>
</div>
</div>
}
subHeader={spellsSubHeader}
displayScrollToTopButton
>
<div className="flex min-h-0 flex-1 flex-col gap-4 px-4 pb-4 pt-2">
{/* Feed — faux spells and kind-777 spells all use NoteList */}
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
{selectedFauxSpell === 'notifications' && !pubkey ? (

5
src/services/relay-info.service.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { isViewerRelayBlocked } from '@/lib/viewer-blocked-relays'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import {
devProxyCorsProblematicHttpsIndexRelayBase,
devProxyLoopbackHttpRelayBase,
@ -169,6 +170,10 @@ class RelayInfoService { @@ -169,6 +170,10 @@ class RelayInfoService {
}
private async fetchRelayNip11(url: string) {
// Path-based WS trending feed — no NIP-11 document at the derived https URL (avoids CORS noise).
if (isWispTrendingNotesRelayUrl(url)) {
return undefined
}
try {
const httpCandidate = url.trim().replace(/^ws:\/\//i, 'http://').replace(/^wss:\/\//i, 'https://')
const httpBase = normalizeHttpRelayUrl(httpCandidate) || httpCandidate

Loading…
Cancel
Save