feat: improve mobile experience
This commit is contained in:
107
src/pages/primary/MePage/index.tsx
Normal file
107
src/pages/primary/MePage/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import AccountManager from '@/components/AccountManager'
|
||||
import LoginDialog from '@/components/LoginDialog'
|
||||
import LogoutDialog from '@/components/LogoutDialog'
|
||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||
import QrCodePopover from '@/components/QrCodePopover'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
||||
import { SimpleUsername } from '@/components/Username'
|
||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { toProfile, toSettings } from '@/lib/link'
|
||||
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 { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function MePage() {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { pubkey } = useNostr()
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
|
||||
|
||||
if (!pubkey) {
|
||||
return (
|
||||
<PrimaryPageLayout pageName="home">
|
||||
<div className="flex flex-col p-4 gap-4 overflow-auto">
|
||||
<AccountManager />
|
||||
</div>
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PrimaryPageLayout pageName="home">
|
||||
<div className="flex gap-4 items-center p-4">
|
||||
<SimpleUserAvatar userId={pubkey} size="big" />
|
||||
<div className="space-y-1">
|
||||
<SimpleUsername
|
||||
className="text-xl font-semibold truncate"
|
||||
userId={pubkey}
|
||||
skeletonClassName="h-6 w-32"
|
||||
/>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<PubkeyCopy pubkey={pubkey} />
|
||||
<QrCodePopover pubkey={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<ItemGroup>
|
||||
<Item onClick={() => push(toProfile(pubkey))}>
|
||||
<UserRound />
|
||||
{t('Profile')}
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Item onClick={() => push(toSettings())}>
|
||||
<Settings />
|
||||
{t('Settings')}
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Item onClick={() => setLoginDialogOpen(true)}>
|
||||
<ArrowDownUp /> {t('Switch account')}
|
||||
</Item>
|
||||
<Separator className="bg-background" />
|
||||
<Item
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => setLogoutDialogOpen(true)}
|
||||
hideChevron
|
||||
>
|
||||
<LogOut />
|
||||
{t('Logout')}
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</div>
|
||||
<LoginDialog open={loginDialogOpen} setOpen={setLoginDialogOpen} />
|
||||
<LogoutDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen} />
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function Item({
|
||||
children,
|
||||
className,
|
||||
hideChevron = false,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement> & { hideChevron?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between px-4 py-2 w-full clickable rounded-lg [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-4">{children}</div>
|
||||
{!hideChevron && <ChevronRight />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ItemGroup({ children }: { children: React.ReactNode }) {
|
||||
return <div className="rounded-lg m-4 bg-muted/40">{children}</div>
|
||||
}
|
||||
74
src/pages/primary/NoteListPage/FeedButton.tsx
Normal file
74
src/pages/primary/NoteListPage/FeedButton.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import FeedSwitcher from '@/components/FeedSwitcher'
|
||||
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { simplifyUrl } from '@/lib/url'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { ChevronDown, Server, UsersRound } from 'lucide-react'
|
||||
import { forwardRef, HTMLAttributes, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function FeedButton() {
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<>
|
||||
<FeedSwitcherTrigger onClick={() => setOpen(true)} />
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="max-h-[80vh]">
|
||||
<div className="p-4 overflow-auto">
|
||||
<FeedSwitcher close={() => setOpen(false)} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FeedSwitcherTrigger />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" className="w-96 p-4 max-h-[80vh] overflow-auto">
|
||||
<FeedSwitcher close={() => setOpen(false)} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { feedType } = useFeed()
|
||||
const { relayGroups, temporaryRelayUrls } = useRelaySettings()
|
||||
const activeGroup = relayGroups.find((group) => group.isActive)
|
||||
const title =
|
||||
feedType === 'following'
|
||||
? t('Following')
|
||||
: temporaryRelayUrls.length > 0
|
||||
? temporaryRelayUrls.length === 1
|
||||
? simplifyUrl(temporaryRelayUrls[0])
|
||||
: t('Temporary')
|
||||
: activeGroup
|
||||
? activeGroup.relayUrls.length === 1
|
||||
? simplifyUrl(activeGroup.relayUrls[0])
|
||||
: activeGroup.groupName
|
||||
: t('Choose a relay collection')
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 clickable px-3 h-full rounded-lg"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{feedType === 'following' ? <UsersRound /> : <Server />}
|
||||
<div className="text-lg font-semibold">{title}</div>
|
||||
<ChevronDown />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
17
src/pages/primary/NoteListPage/SearchButton.tsx
Normal file
17
src/pages/primary/NoteListPage/SearchButton.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SearchDialog } from '@/components/SearchDialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function SearchButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="ghost" size="titlebar-icon" onClick={() => setOpen(true)}>
|
||||
<Search />
|
||||
</Button>
|
||||
<SearchDialog open={open} setOpen={setOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +1,69 @@
|
||||
import NoteList from '@/components/NoteList'
|
||||
import RelaySettings from '@/components/RelaySettings'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FeedButton from './FeedButton'
|
||||
import SearchButton from './SearchButton'
|
||||
|
||||
export default function NoteListPage() {
|
||||
const { t } = useTranslation()
|
||||
const layoutRef = useRef<{ scrollToTop: () => void }>(null)
|
||||
const { relayUrls } = useRelaySettings()
|
||||
const relayUrlsString = JSON.stringify(relayUrls)
|
||||
const { feedType } = useFeed()
|
||||
const { relayUrls, temporaryRelayUrls } = useRelaySettings()
|
||||
const { pubkey, relayList, followings } = useNostr()
|
||||
const urls = useMemo(() => {
|
||||
return feedType === 'following'
|
||||
? relayList?.read.length
|
||||
? relayList.read.slice(0, 4)
|
||||
: BIG_RELAY_URLS
|
||||
: temporaryRelayUrls.length > 0
|
||||
? temporaryRelayUrls
|
||||
: relayUrls
|
||||
}, [feedType, relayUrls, relayList, temporaryRelayUrls])
|
||||
|
||||
useEffect(() => {
|
||||
if (layoutRef.current) {
|
||||
layoutRef.current.scrollToTop()
|
||||
}
|
||||
}, [relayUrlsString])
|
||||
|
||||
if (!relayUrls.length) {
|
||||
return (
|
||||
<PrimaryPageLayout>
|
||||
<div className="w-full text-center">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button title="relay settings" size="lg">
|
||||
Choose a relay group
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-96 h-[450px] p-0">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4">
|
||||
<RelaySettings />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
}, [JSON.stringify(relayUrls), feedType])
|
||||
|
||||
return (
|
||||
<PrimaryPageLayout ref={layoutRef}>
|
||||
<NoteList relayUrls={relayUrls} />
|
||||
<PrimaryPageLayout
|
||||
pageName="home"
|
||||
ref={layoutRef}
|
||||
titlebar={<NoteListPageTitlebar />}
|
||||
displayScrollToTopButton
|
||||
>
|
||||
{!!urls.length && (feedType === 'relays' || (relayList && followings)) ? (
|
||||
<NoteList
|
||||
relayUrls={urls}
|
||||
filter={
|
||||
feedType === 'following'
|
||||
? {
|
||||
authors:
|
||||
pubkey && !followings?.includes(pubkey)
|
||||
? [...(followings ?? []), pubkey]
|
||||
: (followings ?? [])
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
|
||||
)}
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteListPageTitlebar() {
|
||||
return (
|
||||
<div className="flex gap-1 items-center h-full justify-between">
|
||||
<FeedButton />
|
||||
<SearchButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
29
src/pages/primary/NotificationListPage/index.tsx
Normal file
29
src/pages/primary/NotificationListPage/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import NotificationList from '@/components/NotificationList'
|
||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { Bell } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NotificationListPage() {
|
||||
return (
|
||||
<PrimaryPageLayout
|
||||
pageName="notifications"
|
||||
titlebar={<NotificationListPageTitlebar />}
|
||||
displayScrollToTopButton
|
||||
>
|
||||
<div className="px-4">
|
||||
<NotificationList />
|
||||
</div>
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationListPageTitlebar() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center h-full pl-3">
|
||||
<Bell />
|
||||
<div className="text-lg font-semibold">{t('Notifications')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function FollowingListPage({ id }: { id?: string }) {
|
||||
export default function FollowingListPage({ id, index }: { id?: string; index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { profile } = useFetchProfile(id)
|
||||
const { followings } = useFetchFollowings(profile?.pubkey)
|
||||
@@ -45,13 +45,15 @@ export default function FollowingListPage({ id }: { id?: string }) {
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout
|
||||
index={index}
|
||||
titlebarContent={
|
||||
profile?.username
|
||||
? t("username's following", { username: profile.username })
|
||||
: t('following')
|
||||
: t('Following')
|
||||
}
|
||||
displayScrollToTopButton
|
||||
>
|
||||
<div className="space-y-2 max-sm:px-4">
|
||||
<div className="space-y-2 px-4">
|
||||
{visibleFollowings.map((pubkey, index) => (
|
||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
||||
))}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function HomePage() {
|
||||
export default function HomePage({ index }: { index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<SecondaryPageLayout hideBackButton hideScrollToTopButton>
|
||||
<div className="text-muted-foreground w-full h-full flex items-center justify-center">
|
||||
<SecondaryPageLayout index={index} hideBackButton>
|
||||
<div className="text-muted-foreground w-full h-screen flex items-center justify-center">
|
||||
{t('Welcome! 🥳')}
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
|
||||
export default function LoadingPage({ title }: { title?: string }) {
|
||||
export default function LoadingPage({ title, index }: { title?: string; index?: number }) {
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={title}>
|
||||
<SecondaryPageLayout index={index} titlebarContent={title}>
|
||||
<div className="text-muted-foreground text-center">
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NotFoundPage() {
|
||||
export default function NotFoundPage({ index }: { index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout hideBackButton>
|
||||
<SecondaryPageLayout 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>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import NoteList from '@/components/NoteList'
|
||||
import { useSearchParams } from '@/hooks'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { isWebsocketUrl } from '@/lib/url'
|
||||
import { isWebsocketUrl, simplifyUrl } from '@/lib/url'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { Filter } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NoteListPage() {
|
||||
export default function NoteListPage({ index }: { index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { relayUrls, searchableRelayUrls } = useRelaySettings()
|
||||
const { searchParams } = useSearchParams()
|
||||
@@ -27,18 +27,18 @@ export default function NoteListPage() {
|
||||
}
|
||||
const search = searchParams.get('s')
|
||||
if (search) {
|
||||
return { title: `${t('search')}: ${search}`, filter: { search }, urls: searchableRelayUrls }
|
||||
return { title: `${t('Search')}: ${search}`, filter: { search }, urls: searchableRelayUrls }
|
||||
}
|
||||
const relayUrl = searchParams.get('relay')
|
||||
if (relayUrl && isWebsocketUrl(relayUrl)) {
|
||||
return { title: relayUrl, urls: [relayUrl] }
|
||||
return { title: simplifyUrl(relayUrl), urls: [relayUrl] }
|
||||
}
|
||||
return { urls: relayUrls }
|
||||
}, [searchParams, relayUrlsString])
|
||||
|
||||
if (filter?.search && searchableRelayUrls.length === 0) {
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={title}>
|
||||
<SecondaryPageLayout index={index} titlebarContent={title} displayScrollToTopButton>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
{t('The relays you are connected to do not support search')}
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export default function NoteListPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={title}>
|
||||
<SecondaryPageLayout index={index} titlebarContent={title}>
|
||||
<NoteList key={title} filter={filter} relayUrls={urls} />
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NotFoundPage from '../NotFoundPage'
|
||||
|
||||
export default function NotePage({ id }: { id?: string }) {
|
||||
export default function NotePage({ id, index }: { id?: string; index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { event, isFetching } = useFetchEvent(id)
|
||||
const parentEventId = useMemo(() => getParentEventId(event), [event])
|
||||
@@ -22,8 +22,8 @@ export default function NotePage({ id }: { id?: string }) {
|
||||
|
||||
if (!event && isFetching) {
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={t('note')}>
|
||||
<div className="max-sm:px-4">
|
||||
<SecondaryPageLayout index={index} titlebarContent={t('Note')} displayScrollToTopButton>
|
||||
<div className="px-4">
|
||||
<Skeleton className="w-10 h-10 rounded-full" />
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
@@ -32,14 +32,14 @@ export default function NotePage({ id }: { id?: string }) {
|
||||
if (!event) return <NotFoundPage />
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={t('note')}>
|
||||
<div className="max-sm:px-4">
|
||||
<SecondaryPageLayout index={index} titlebarContent={t('Note')}>
|
||||
<div className="px-4">
|
||||
<ParentNote key={`root-note-${event.id}`} eventId={rootEventId} />
|
||||
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
|
||||
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||
</div>
|
||||
<Separator className="mb-2 mt-4" />
|
||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="max-sm:px-2" />
|
||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="px-2" />
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ function ParentNote({ eventId }: { eventId?: string }) {
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
className="flex space-x-1 p-1 items-center hover:bg-muted/50 cursor-pointer text-sm text-muted-foreground hover:text-foreground"
|
||||
className="flex space-x-1 p-1 items-center clickable text-sm text-muted-foreground hover:text-foreground"
|
||||
onClick={() => push(toNote(event))}
|
||||
>
|
||||
<UserAvatar userId={event.pubkey} size="tiny" />
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import NotificationList from '@/components/NotificationList'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NotificationListPage() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={t('notifications')}>
|
||||
<div className="max-sm:px-4">
|
||||
<NotificationList />
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
const LIMIT = 50
|
||||
|
||||
export default function ProfileListPage() {
|
||||
export default function ProfileListPage({ index }: { index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { searchParams } = useSearchParams()
|
||||
const { relayUrls, searchableRelayUrls } = useRelaySettings()
|
||||
@@ -30,7 +30,7 @@ export default function ProfileListPage() {
|
||||
return filter.search ? searchableRelayUrls : relayUrls
|
||||
}, [relayUrls, searchableRelayUrls, filter])
|
||||
const title = useMemo(() => {
|
||||
return filter.search ? `${t('search')}: ${filter.search}` : t('all users')
|
||||
return filter.search ? `${t('Search')}: ${filter.search}` : t('All users')
|
||||
}, [filter])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -78,8 +78,8 @@ export default function ProfileListPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={title}>
|
||||
<div className="space-y-2 max-sm:px-4">
|
||||
<SecondaryPageLayout index={index} titlebarContent={title} displayScrollToTopButton>
|
||||
<div className="space-y-2 px-4">
|
||||
{Array.from(pubkeySet).map((pubkey, index) => (
|
||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
||||
))}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { formatNpub } from '@/lib/pubkey'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
export default function PubkeyCopy({ pubkey }: { pubkey: string }) {
|
||||
const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey])
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const copyNpub = () => {
|
||||
if (!npub) return
|
||||
|
||||
navigator.clipboard.writeText(npub)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 text-sm text-muted-foreground items-center bg-muted w-fit px-2 rounded-full hover:text-foreground cursor-pointer"
|
||||
onClick={() => copyNpub()}
|
||||
>
|
||||
<div>{formatNpub(npub, 24)}</div>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { QrCode } from 'lucide-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
|
||||
export default function QrCodePopover({ pubkey }: { pubkey: string }) {
|
||||
const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey])
|
||||
if (!npub) return null
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<div className="bg-muted rounded-full h-5 w-5 flex flex-col items-center justify-center text-muted-foreground hover:text-foreground">
|
||||
<QrCode size={14} />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit h-fit">
|
||||
<QRCodeSVG value={`nostr:${npub}`} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import Nip05 from '@/components/Nip05'
|
||||
import NoteList from '@/components/NoteList'
|
||||
import ProfileAbout from '@/components/ProfileAbout'
|
||||
import ProfileBanner from '@/components/ProfileBanner'
|
||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||
import QrCodePopover from '@/components/QrCodePopover'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { useFetchFollowings, useFetchProfile } from '@/hooks'
|
||||
import { useFetchRelayList } from '@/hooks/useFetchRelayList'
|
||||
@@ -18,10 +19,8 @@ import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NotFoundPage from '../NotFoundPage'
|
||||
import PubkeyCopy from './PubkeyCopy'
|
||||
import QrCodePopover from './QrCodePopover'
|
||||
|
||||
export default function ProfilePage({ id }: { id?: string }) {
|
||||
export default function ProfilePage({ id, index }: { id?: string; index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
const { profile, isFetching } = useFetchProfile(id)
|
||||
const { relayList, isFetching: isFetchingRelayInfo } = useFetchRelayList(profile?.pubkey)
|
||||
@@ -46,8 +45,8 @@ export default function ProfilePage({ id }: { id?: string }) {
|
||||
|
||||
if (!profile && isFetching) {
|
||||
return (
|
||||
<SecondaryPageLayout>
|
||||
<div className="max-sm:px-4">
|
||||
<SecondaryPageLayout index={index}>
|
||||
<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" />
|
||||
<Skeleton className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background rounded-full" />
|
||||
@@ -62,8 +61,8 @@ export default function ProfilePage({ id }: { id?: string }) {
|
||||
|
||||
const { banner, username, nip05, about, avatar, pubkey } = profile
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={username}>
|
||||
<div className="max-sm:px-4">
|
||||
<SecondaryPageLayout index={index} titlebarContent={username} displayScrollToTopButton>
|
||||
<div className="px-4">
|
||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||
<ProfileBanner
|
||||
banner={banner}
|
||||
@@ -102,9 +101,8 @@ export default function ProfilePage({ id }: { id?: string }) {
|
||||
</SecondaryPageLink>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="hidden sm:block mt-4 sm:my-4" />
|
||||
{!isFetchingRelayInfo && (
|
||||
<NoteList filter={{ authors: [pubkey] }} relayUrls={relayUrls} className="max-sm:mt-2" />
|
||||
<NoteList filter={{ authors: [pubkey] }} relayUrls={relayUrls} className="mt-2" />
|
||||
)}
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
|
||||
@@ -2,12 +2,12 @@ import RelaySettings from '@/components/RelaySettings'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelaySettingsPage() {
|
||||
export default function RelaySettingsPage({ index }: { index?: number }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout titlebarContent={t('Relay settings')}>
|
||||
<div className="max-sm:px-4">
|
||||
<SecondaryPageLayout index={index} titlebarContent={t('Relay settings')}>
|
||||
<div className="px-4">
|
||||
<RelaySettings hideTitle />
|
||||
</div>
|
||||
</SecondaryPageLayout>
|
||||
|
||||
70
src/pages/secondary/SettingsPage/index.tsx
Normal file
70
src/pages/secondary/SettingsPage/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import AboutInfoDialog from '@/components/AboutInfoDialog'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useTheme } from '@/providers/ThemeProvider'
|
||||
import { TLanguage } from '@/types'
|
||||
import { SelectValue } from '@radix-ui/react-select'
|
||||
import { ChevronRight, Info, Languages, SunMoon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function SettingsPage({ index }: { index?: number }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
|
||||
const { themeSetting, setThemeSetting } = useTheme()
|
||||
|
||||
const handleLanguageChange = (value: TLanguage) => {
|
||||
i18n.changeLanguage(value)
|
||||
setLanguage(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout index={index} titlebarContent={t('Settings')}>
|
||||
<div className="flex justify-between items-center px-4 py-2 [&_svg]:size-4 [&_svg]:shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<Languages />
|
||||
<div>{t('Languages')}</div>
|
||||
</div>
|
||||
<Select defaultValue="en" value={language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">{t('English')}</SelectItem>
|
||||
<SelectItem value="zh">{t('Chinese')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-between items-center px-4 py-2 [&_svg]:size-4 [&_svg]:shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<SunMoon />
|
||||
<div>{t('Theme')}</div>
|
||||
</div>
|
||||
<Select defaultValue="system" value={themeSetting} onValueChange={setThemeSetting}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="system">{t('System')}</SelectItem>
|
||||
<SelectItem value="light">{t('Light')}</SelectItem>
|
||||
<SelectItem value="dark">{t('Dark')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<AboutInfoDialog>
|
||||
<div className="flex clickable justify-between items-center px-4 py-2 h-[52px] rounded-lg [&_svg]:size-4 [&_svg]:shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<Info />
|
||||
<div>{t('About')}</div>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="text-muted-foreground">
|
||||
v{__APP_VERSION__} ({__GIT_COMMIT__})
|
||||
</div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</AboutInfoDialog>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user