feat: scroll to top when jumping to the current page

This commit is contained in:
codytseng
2025-01-26 16:28:47 +08:00
parent 82537f192b
commit 23bf7fd005
23 changed files with 235 additions and 132 deletions

View File

@@ -4,7 +4,17 @@ import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage'
import { cloneElement, createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { TPageRef } from '@/types'
import {
cloneElement,
createContext,
createRef,
ReactNode,
RefObject,
useContext,
useEffect,
useState
} from 'react'
import MePage from './pages/primary/MePage'
import NotificationListPage from './pages/primary/NotificationListPage'
import { useScreenSize } from './providers/ScreenSizeProvider'
@@ -26,13 +36,20 @@ type TSecondaryPageContext = {
type TStackItem = {
index: number
url: string
component: React.ReactNode | null
component: React.ReactElement | null
ref: RefObject<TPageRef> | null
}
const PRIMARY_PAGE_REF_MAP = {
home: createRef<TPageRef>(),
notifications: createRef<TPageRef>(),
me: createRef<TPageRef>()
}
const PRIMARY_PAGE_MAP = {
home: <NoteListPage />,
notifications: <NotificationListPage />,
me: <MePage />
home: <NoteListPage ref={PRIMARY_PAGE_REF_MAP.home} />,
notifications: <NotificationListPage ref={PRIMARY_PAGE_REF_MAP.notifications} />,
me: <MePage ref={PRIMARY_PAGE_REF_MAP.me} />
}
const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefined)
@@ -111,13 +128,22 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const topItem = newStack[newStack.length - 1] as TStackItem | undefined
if (!topItem) {
// Create a new stack item if it's not exist (e.g. when the user refreshes the page, the stack will be empty)
const newComponent = findAndCreateComponent(state.url, state.index)
if (newComponent) {
newStack.push({ index: state.index, url: state.url, component: newComponent })
const { component, ref } = findAndCreateComponent(state.url, state.index)
if (component) {
newStack.push({
index: state.index,
url: state.url,
component,
ref
})
}
} else if (!topItem.component) {
// Load the component if it's not cached
topItem.component = findAndCreateComponent(topItem.url, state.index)
const { component, ref } = findAndCreateComponent(topItem.url, state.index)
if (component) {
topItem.component = component
topItem.ref = ref
}
}
if (newStack.length === 0) {
window.history.replaceState(null, '', '/')
@@ -139,6 +165,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
setPrimaryPages((prev) => [...prev, { name: page, element: PRIMARY_PAGE_MAP[page] }])
}
setCurrentPrimaryPage(page)
PRIMARY_PAGE_REF_MAP[page].current?.scrollToTop()
if (isSmallScreen) {
clearSecondaryPages()
}
@@ -146,7 +173,13 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const pushSecondaryPage = (url: string, index?: number) => {
setSecondaryStack((prevStack) => {
if (isCurrentPage(prevStack, url)) return prevStack
if (isCurrentPage(prevStack, url)) {
const currentItem = prevStack[prevStack.length - 1]
if (currentItem?.ref?.current) {
currentItem.ref.current.scrollToTop()
}
return prevStack
}
const { newStack, newItem } = pushNewPageToStack(prevStack, url, maxStackSize, index)
if (newItem) {
@@ -302,10 +335,11 @@ function findAndCreateComponent(url: string, index: number) {
const match = matcher(path)
if (!match) continue
if (!element) return null
return cloneElement(element, { ...match.params, index } as any)
if (!element) return {}
const ref = createRef<TPageRef>()
return { component: cloneElement(element, { ...match.params, index, ref } as any), ref }
}
return null
return {}
}
function pushNewPageToStack(
@@ -317,10 +351,10 @@ function pushNewPageToStack(
const currentItem = stack[stack.length - 1]
const currentIndex = specificIndex ?? (currentItem ? currentItem.index + 1 : 0)
const component = findAndCreateComponent(url, currentIndex)
const { component, ref } = findAndCreateComponent(url, currentIndex)
if (!component) return { newStack: stack, newItem: null }
const newItem = { component, url, index: currentIndex }
const newItem = { component, ref, url, index: currentIndex }
const newStack = [...stack, newItem]
const lastCachedIndex = newStack.findIndex((stack) => stack.component)
// Clear the oldest cached component if there are too many cached components

View File

@@ -15,9 +15,8 @@ export default function BottomNavigationBarItem({
<Button
className={cn(
'flex shadow-none items-center bg-transparent w-full h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-4 rounded-lg xl:justify-start text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4',
active && 'text-primary disabled:opacity-100'
active && 'text-primary hover:text-primary'
)}
disabled={active}
variant="ghost"
onClick={onClick}
>

View File

@@ -13,10 +13,9 @@ const SidebarItem = forwardRef<
<Button
className={cn(
'flex shadow-none items-center bg-transparent w-12 h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-4 rounded-lg xl:justify-start gap-4 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4',
active && 'text-primary disabled:opacity-100',
active && 'text-primary hover:text-primary',
className
)}
disabled={active}
variant="ghost"
title={t(title)}
ref={ref}

View File

@@ -30,11 +30,10 @@ const PrimaryPageLayout = forwardRef(
ref,
() => ({
scrollToTop: () => {
if (isSmallScreen) {
window.scrollTo({ top: 0 })
return
if (scrollAreaRef.current) {
return scrollAreaRef.current.scrollTo({ top: 0, behavior: 'smooth' })
}
scrollAreaRef.current?.scrollTo({ top: 0 })
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}),
[]

View File

@@ -6,41 +6,78 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { useSecondaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useEffect, useRef } from 'react'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
export default function SecondaryPageLayout({
children,
index,
title,
controls,
hideBackButton = false,
displayScrollToTopButton = false
}: {
children?: React.ReactNode
index?: number
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
displayScrollToTopButton?: boolean
}): JSX.Element {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const { isSmallScreen } = useScreenSize()
const { currentIndex } = useSecondaryPage()
const SecondaryPageLayout = forwardRef(
(
{
children,
index,
title,
controls,
hideBackButton = false,
displayScrollToTopButton = false
}: {
children?: React.ReactNode
index?: number
title?: React.ReactNode
controls?: React.ReactNode
hideBackButton?: boolean
displayScrollToTopButton?: boolean
},
ref
) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const { isSmallScreen } = useScreenSize()
const { currentIndex } = useSecondaryPage()
useImperativeHandle(
ref,
() => ({
scrollToTop: () => {
if (scrollAreaRef.current) {
return scrollAreaRef.current.scrollTo({ top: 0, behavior: 'smooth' })
}
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}),
[]
)
useEffect(() => {
if (isSmallScreen) {
window.scrollTo({ top: 0 })
return
}
}, [])
useEffect(() => {
if (isSmallScreen) {
window.scrollTo({ top: 0 })
return
return (
<DeepBrowsingProvider active={currentIndex === index}>
<div
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<SecondaryPageTitlebar
title={title}
controls={controls}
hideBackButton={hideBackButton}
/>
{children}
{displayScrollToTopButton && <ScrollToTopButton />}
<BottomNavigationBar />
</div>
</DeepBrowsingProvider>
)
}
}, [])
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={currentIndex === index}>
<div
style={{
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-screen overflow-auto"
scrollBarClassName="sm:z-50"
ref={scrollAreaRef}
>
<SecondaryPageTitlebar
title={title}
@@ -48,27 +85,14 @@ export default function SecondaryPageLayout({
hideBackButton={hideBackButton}
/>
{children}
{displayScrollToTopButton && <ScrollToTopButton />}
<BottomNavigationBar />
</div>
</ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
return (
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea
className="h-screen overflow-auto"
scrollBarClassName="sm:z-50"
ref={scrollAreaRef}
>
<SecondaryPageTitlebar title={title} controls={controls} hideBackButton={hideBackButton} />
{children}
</ScrollArea>
{displayScrollToTopButton && <ScrollToTopButton scrollAreaRef={scrollAreaRef} />}
</DeepBrowsingProvider>
)
}
)
SecondaryPageLayout.displayName = 'SecondaryPageLayout'
export default SecondaryPageLayout
export function SecondaryPageTitlebar({
title,

View File

@@ -13,10 +13,10 @@ import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { ArrowDownUp, ChevronRight, LogOut, Settings, UserRound } from 'lucide-react'
import { HTMLProps, useState } from 'react'
import { forwardRef, HTMLProps, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function MePage() {
const MePage = forwardRef((_, ref) => {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey } = useNostr()
@@ -25,7 +25,7 @@ export default function MePage() {
if (!pubkey) {
return (
<PrimaryPageLayout pageName="home" titlebar={<MePageTitlebar />}>
<PrimaryPageLayout ref={ref} pageName="home" titlebar={<MePageTitlebar />}>
<div className="flex flex-col p-4 gap-4 overflow-auto">
<AccountManager />
</div>
@@ -34,7 +34,7 @@ export default function MePage() {
}
return (
<PrimaryPageLayout pageName="home" titlebar={<MePageTitlebar />}>
<PrimaryPageLayout ref={ref} pageName="home" titlebar={<MePageTitlebar />}>
<div className="flex gap-4 items-center p-4">
<SimpleUserAvatar userId={pubkey} size="big" />
<div className="space-y-1">
@@ -71,7 +71,9 @@ export default function MePage() {
<LogoutDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen} />
</PrimaryPageLayout>
)
}
})
MePage.displayName = 'MePage'
export default MePage
function MePageTitlebar() {
const { push } = useSecondaryPage()

View File

@@ -4,16 +4,18 @@ import { Button } from '@/components/ui/button'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { useFeed } from '@/providers/FeedProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useEffect, useRef } from 'react'
import { TPageRef } from '@/types'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import FeedButton from './FeedButton'
import SearchButton from './SearchButton'
export default function NoteListPage() {
const NoteListPage = forwardRef((_, ref) => {
const { t } = useTranslation()
const layoutRef = useRef<{ scrollToTop: () => void }>(null)
const layoutRef = useRef<TPageRef>(null)
const { pubkey, checkLogin } = useNostr()
const { feedType, relayUrls, isReady, filter } = useFeed()
useImperativeHandle(ref, () => layoutRef.current)
useEffect(() => {
if (layoutRef.current) {
@@ -46,7 +48,9 @@ export default function NoteListPage() {
{content}
</PrimaryPageLayout>
)
}
})
NoteListPage.displayName = 'NoteListPage'
export default NoteListPage
function NoteListPageTitlebar({ temporaryRelayUrls = [] }: { temporaryRelayUrls?: string[] }) {
return (

View File

@@ -1,11 +1,13 @@
import NotificationList from '@/components/NotificationList'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { Bell } from 'lucide-react'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
export default function NotificationListPage() {
const NotificationListPage = forwardRef((_, ref) => {
return (
<PrimaryPageLayout
ref={ref}
pageName="notifications"
titlebar={<NotificationListPageTitlebar />}
displayScrollToTopButton
@@ -15,7 +17,9 @@ export default function NotificationListPage() {
</div>
</PrimaryPageLayout>
)
}
})
NotificationListPage.displayName = 'NotificationListPage'
export default NotificationListPage
function NotificationListPageTitlebar() {
const { t } = useTranslation()

View File

@@ -1,10 +1,10 @@
import UserItem from '@/components/UserItem'
import { useFetchFollowings, useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useEffect, useRef, useState } from 'react'
import { forwardRef, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function FollowingListPage({ id, index }: { id?: string; index?: number }) {
const FollowingListPage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
const { t } = useTranslation()
const { profile } = useFetchProfile(id)
const { followings } = useFetchFollowings(profile?.pubkey)
@@ -45,6 +45,7 @@ export default function FollowingListPage({ id, index }: { id?: string; index?:
return (
<SecondaryPageLayout
ref={ref}
index={index}
title={
profile?.username
@@ -61,4 +62,6 @@ export default function FollowingListPage({ id, index }: { id?: string; index?:
</div>
</SecondaryPageLayout>
)
}
})
FollowingListPage.displayName = 'FollowingListPage'
export default FollowingListPage

View File

@@ -1,13 +1,16 @@
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
export default function HomePage({ index }: { index?: number }) {
const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
return (
<SecondaryPageLayout index={index} hideBackButton>
<SecondaryPageLayout ref={ref} index={index} hideBackButton>
<div className="text-muted-foreground w-full h-screen flex items-center justify-center">
{t('Welcome! 🥳')}
</div>
</SecondaryPageLayout>
)
}
})
HomePage.displayName = 'HomePage'
export default HomePage

View File

@@ -1,11 +1,14 @@
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react'
export default function LoadingPage({ title, index }: { title?: string; index?: number }) {
const LoadingPage = forwardRef(({ title, index }: { title?: string; index?: number }, ref) => {
return (
<SecondaryPageLayout index={index} title={title}>
<SecondaryPageLayout ref={ref} index={index} title={title}>
<div className="text-muted-foreground text-center">
<div>Loading...</div>
</div>
</SecondaryPageLayout>
)
}
})
LoadingPage.displayName = 'LoadingPage'
export default LoadingPage

View File

@@ -6,11 +6,11 @@ import { useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useEffect, useRef, useState } from 'react'
import { forwardRef, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage'
export default function MuteListPage({ index }: { index?: number }) {
const MuteListPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { profile } = useNostr()
const { mutePubkeys } = useMuteList()
@@ -55,6 +55,7 @@ export default function MuteListPage({ index }: { index?: number }) {
return (
<SecondaryPageLayout
ref={ref}
index={index}
title={t("username's muted", { username: profile.username })}
displayScrollToTopButton
@@ -67,7 +68,9 @@ export default function MuteListPage({ index }: { index?: number }) {
</div>
</SecondaryPageLayout>
)
}
})
MuteListPage.displayName = 'MuteListPage'
export default MuteListPage
function UserItem({ pubkey }: { pubkey: string }) {
const { profile } = useFetchProfile(pubkey)

View File

@@ -1,15 +1,18 @@
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
export default function NotFoundPage({ index }: { index?: number }) {
const NotFoundPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
return (
<SecondaryPageLayout index={index} hideBackButton>
<SecondaryPageLayout ref={ref} index={index} hideBackButton>
<div className="text-muted-foreground w-full h-full flex flex-col items-center justify-center gap-2">
<div>{t('Lost in the void')} 🌌</div>
<div>(404)</div>
</div>
</SecondaryPageLayout>
)
}
})
NotFoundPage.displayName = 'NotFoundPage'
export default NotFoundPage

View File

@@ -4,10 +4,10 @@ import { useFetchRelayInfos, useSearchParams } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useFeed } from '@/providers/FeedProvider'
import { Filter } from 'nostr-tools'
import { useMemo } from 'react'
import { forwardRef, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export default function NoteListPage({ index }: { index?: number }) {
const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { relayUrls } = useFeed()
const { searchableRelayUrls } = useFetchRelayInfos(relayUrls)
@@ -43,8 +43,10 @@ export default function NoteListPage({ index }: { index?: number }) {
}, [searchParams, JSON.stringify(relayUrls)])
return (
<SecondaryPageLayout index={index} title={title} displayScrollToTopButton>
<SecondaryPageLayout ref={ref} index={index} title={title} displayScrollToTopButton>
<NoteList key={title} filter={filter} relayUrls={urls} />
</SecondaryPageLayout>
)
}
})
NoteListPage.displayName = 'NoteListPage'
export default NoteListPage

View File

@@ -12,11 +12,11 @@ import { useFetchEvent } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event'
import { toNote } from '@/lib/link'
import { useMemo } from 'react'
import { forwardRef, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage'
export default function NotePage({ id, index }: { id?: string; index?: number }) {
const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(id)
const parentEventId = useMemo(() => getParentEventId(event), [event])
@@ -24,7 +24,7 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
if (!event && isFetching) {
return (
<SecondaryPageLayout index={index} title={t('Note')}>
<SecondaryPageLayout ref={ref} index={index} title={t('Note')}>
<div className="px-4">
<Skeleton className="w-10 h-10 rounded-full" />
</div>
@@ -35,7 +35,7 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
if (isPictureEvent(event)) {
return (
<SecondaryPageLayout index={index} title={t('Note')} displayScrollToTopButton>
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton>
<PictureNote key={`note-${event.id}`} event={event} fetchNoteStats />
<Separator className="mb-2 mt-4" />
<Nip22ReplyNoteList
@@ -48,7 +48,7 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
}
return (
<SecondaryPageLayout index={index} title={t('Note')} displayScrollToTopButton>
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton>
<div className="px-4">
{rootEventId !== parentEventId && (
<ParentNote key={`root-note-${event.id}`} eventId={rootEventId} />
@@ -68,7 +68,9 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
)}
</SecondaryPageLayout>
)
}
})
NotePage.displayName = 'NotePage'
export default NotePage
function ParentNote({ eventId }: { eventId?: string }) {
const { push } = useSecondaryPage()

View File

@@ -1,9 +1,10 @@
import OthersRelayList from '@/components/OthersRelayList'
import { useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
export default function RelaySettingsPage({ id, index }: { id?: string; index?: number }) {
const RelaySettingsPage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
const { t } = useTranslation()
const { profile } = useFetchProfile(id)
@@ -13,6 +14,7 @@ export default function RelaySettingsPage({ id, index }: { id?: string; index?:
return (
<SecondaryPageLayout
ref={ref}
index={index}
title={t("username's used relays", { username: profile.username })}
>
@@ -21,4 +23,6 @@ export default function RelaySettingsPage({ id, index }: { id?: string; index?:
</div>
</SecondaryPageLayout>
)
}
})
RelaySettingsPage.displayName = 'RelaySettingsPage'
export default RelaySettingsPage

View File

@@ -11,10 +11,10 @@ import { generateImageByPubkey } from '@/lib/pubkey'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { Loader, Upload } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { forwardRef, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function ProfileEditorPage({ index }: { index?: number }) {
const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { pop } = useSecondaryPage()
const { account, profile, profileEvent, publish, updateProfileEvent } = useNostr()
@@ -98,7 +98,7 @@ export default function ProfileEditorPage({ index }: { index?: number }) {
)
return (
<SecondaryPageLayout index={index} title={profile.username} controls={controls}>
<SecondaryPageLayout ref={ref} index={index} title={profile.username} controls={controls}>
<div className="px-4">
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
<Uploader
@@ -174,7 +174,9 @@ export default function ProfileEditorPage({ index }: { index?: number }) {
</div>
</SecondaryPageLayout>
)
}
})
ProfileEditorPage.displayName = 'ProfileEditorPage'
export default ProfileEditorPage
function ItemTitle({ children }: { children: React.ReactNode }) {
return <div className="text-sm font-semibold text-muted-foreground pl-3">{children}</div>

View File

@@ -6,12 +6,12 @@ import { useFeed } from '@/providers/FeedProvider'
import client from '@/services/client.service'
import dayjs from 'dayjs'
import { Filter } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
const LIMIT = 50
export default function ProfileListPage({ index }: { index?: number }) {
const ProfileListPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { searchParams } = useSearchParams()
const { relayUrls } = useFeed()
@@ -80,7 +80,7 @@ export default function ProfileListPage({ index }: { index?: number }) {
}
return (
<SecondaryPageLayout index={index} title={title} displayScrollToTopButton>
<SecondaryPageLayout ref={ref} index={index} title={title} displayScrollToTopButton>
<div className="space-y-2 px-4">
{Array.from(pubkeySet).map((pubkey, index) => (
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
@@ -89,4 +89,6 @@ export default function ProfileListPage({ index }: { index?: number }) {
</div>
</SecondaryPageLayout>
)
}
})
ProfileListPage.displayName = 'ProfileListPage'
export default ProfileListPage

View File

@@ -25,11 +25,11 @@ import { useFeed } from '@/providers/FeedProvider'
import { useFollowList } from '@/providers/FollowListProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useMemo } from 'react'
import { forwardRef, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage'
export default function ProfilePage({ id, index }: { id?: string; index?: number }) {
const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { profile, isFetching } = useFetchProfile(id)
@@ -59,7 +59,7 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
if (!profile && isFetching) {
return (
<SecondaryPageLayout index={index}>
<SecondaryPageLayout index={index} ref={ref}>
<div className="px-4">
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
<Skeleton className="w-full h-full object-cover rounded-lg" />
@@ -75,7 +75,7 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
const { banner, username, about, avatar, pubkey } = profile
return (
<SecondaryPageLayout index={index} title={username} displayScrollToTopButton>
<SecondaryPageLayout index={index} title={username} displayScrollToTopButton ref={ref}>
<div className="px-4">
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
<ProfileBanner
@@ -151,4 +151,6 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
)}
</SecondaryPageLayout>
)
}
})
ProfilePage.displayName = 'ProfilePage'
export default ProfilePage

View File

@@ -5,19 +5,20 @@ import { Button } from '@/components/ui/button'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { Check, Copy } from 'lucide-react'
import { useMemo, useState } from 'react'
import { forwardRef, useMemo, useState } from 'react'
import NotFoundPage from '../NotFoundPage'
export default function RelayPage({ url, index }: { url?: string; index?: number }) {
const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number }, ref) => {
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const title = useMemo(() => (url ? simplifyUrl(url) : undefined), [url])
if (!normalizedUrl) {
return <NotFoundPage />
return <NotFoundPage ref={ref} />
}
return (
<SecondaryPageLayout
ref={ref}
index={index}
title={title}
controls={<RelayPageControls url={normalizedUrl} />}
@@ -27,7 +28,9 @@ export default function RelayPage({ url, index }: { url?: string; index?: number
<NoteList relayUrls={[normalizedUrl]} />
</SecondaryPageLayout>
)
}
})
RelayPage.displayName = 'RelayPage'
export default RelayPage
function RelayPageControls({ url }: { url: string }) {
const [copied, setCopied] = useState(false)

View File

@@ -2,10 +2,10 @@ import MailboxSetting from '@/components/MailboxSetting'
import RelaySetsSetting from '@/components/RelaySetsSetting'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useEffect, useState } from 'react'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function RelaySettingsPage({ index }: { index?: number }) {
const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const [tabValue, setTabValue] = useState('relay-sets')
@@ -21,7 +21,7 @@ export default function RelaySettingsPage({ index }: { index?: number }) {
}, [])
return (
<SecondaryPageLayout index={index} title={t('Relay settings')}>
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 space-y-4">
<TabsList>
<TabsTrigger value="relay-sets">{t('Relay Sets')}</TabsTrigger>
@@ -36,4 +36,6 @@ export default function RelaySettingsPage({ index }: { index?: number }) {
</Tabs>
</SecondaryPageLayout>
)
}
})
RelaySettingsPage.displayName = 'RelaySettingsPage'
export default RelaySettingsPage

View File

@@ -12,7 +12,7 @@ import { Check, ChevronRight, Copy, Info, KeyRound, Languages, Server, SunMoon }
import { forwardRef, HTMLProps, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function SettingsPage({ index }: { index?: number }) {
const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t, i18n } = useTranslation()
const { nsec, ncryptsec } = useNostr()
const { push } = useSecondaryPage()
@@ -27,7 +27,7 @@ export default function SettingsPage({ index }: { index?: number }) {
}
return (
<SecondaryPageLayout index={index} title={t('Settings')}>
<SecondaryPageLayout ref={ref} index={index} title={t('Settings')}>
<SettingItem>
<div className="flex items-center gap-4">
<Languages />
@@ -112,7 +112,9 @@ export default function SettingsPage({ index }: { index?: number }) {
</AboutInfoDialog>
</SecondaryPageLayout>
)
}
})
SettingsPage.displayName = 'SettingsPage'
export default SettingsPage
const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
({ children, className, ...props }, ref) => {

View File

@@ -95,3 +95,5 @@ export type TLanguage = 'en' | 'zh'
export type TImageInfo = { url: string; blurHash?: string; dim?: { width: number; height: number } }
export type TNoteListMode = 'posts' | 'postsAndReplies' | 'pictures'
export type TPageRef = { scrollToTop: () => void }