feat: scroll to top when jumping to the current page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
}),
|
||||
[]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user