diff --git a/src/App.tsx b/src/App.tsx index 66281d99..adf113fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,7 +38,7 @@ export default function App(): JSX.Element { -
+
diff --git a/src/PageManager.tsx b/src/PageManager.tsx index bbfdabfe..748eb271 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -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 }) { } 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 }) { }} > -
+
{primaryNoteView ? ( // Show primary note view with back button on mobile
@@ -2423,15 +2423,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
-
+
{primaryNoteView}
) : ( - <> +
0 && 'hidden' )} aria-hidden={secondaryStack.length > 0} @@ -2441,12 +2441,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { {secondaryStack.length > 0 ? (
) : null} - +
)}
@@ -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 diff --git a/src/components/Embedded/EmbeddedNoteProviders.tsx b/src/components/Embedded/EmbeddedNoteProviders.tsx index 85c1ae7a..3e3bb75d 100644 --- a/src/components/Embedded/EmbeddedNoteProviders.tsx +++ b/src/components/Embedded/EmbeddedNoteProviders.tsx @@ -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 ( - - {children} - + + + {children} + + ) } diff --git a/src/components/NoteCard/RepostNoteCard.tsx b/src/components/NoteCard/RepostNoteCard.tsx index 3ae72254..089d7046 100644 --- a/src/components/NoteCard/RepostNoteCard.tsx +++ b/src/components/NoteCard/RepostNoteCard.tsx @@ -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({ seenOnAllowlist?: readonly string[] }) { const { mutePubkeySet } = useMuteList() - const { hideContentMentioningMutedUsers } = useContentPolicy() + const hideContentMentioningMutedUsers = + useContentPolicyOptional()?.hideContentMentioningMutedUsers ?? + storage.getHideContentMentioningMutedUsers() const [targetEvent, setTargetEvent] = useState(null) const shouldHide = useMemo(() => { if (!targetEvent) return true diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index b2bf6025..2e90d711 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -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({ 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 diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index a764be3c..8b60676d 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -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({ 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( diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 268820a4..195b23cd 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -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({ 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() diff --git a/src/constants.ts b/src/constants.ts index aca8b1e3..bba5515b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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/' ] diff --git a/src/index.css b/src/index.css index 66c3406d..c3b4a968 100644 --- a/src/index.css +++ b/src/index.css @@ -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 @@ 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