From 35df916a19ae337468dcc73872449e4393ab25c6 Mon Sep 17 00:00:00 2001 From: codytseng Date: Thu, 28 Aug 2025 22:58:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=92=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/PageManager.tsx | 45 +++++++---- .../TemporaryRelaySet.tsx | 28 ------- .../FavoriteRelaysSetting/index.tsx | 2 - src/components/FeedSwitcher/index.tsx | 21 +----- src/components/NotFound/index.tsx | 12 +++ src/components/Profile/index.tsx | 3 +- src/components/Relay/index.tsx | 50 +++++++++++++ src/components/RelayList/index.tsx | 7 +- src/components/RelayPageControls/index.tsx | 35 +++++++++ src/pages/primary/NoteListPage/FeedButton.tsx | 5 -- src/pages/primary/NoteListPage/RelaysFeed.tsx | 6 +- src/pages/primary/NoteListPage/index.tsx | 12 +-- src/pages/primary/RelayPage/index.tsx | 37 ++++++++++ src/pages/secondary/NotFoundPage/index.tsx | 9 +-- src/pages/secondary/RelayPage/index.tsx | 74 +------------------ src/providers/FeedProvider.tsx | 43 ----------- src/types/index.d.ts | 2 +- 17 files changed, 183 insertions(+), 208 deletions(-) delete mode 100644 src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx create mode 100644 src/components/NotFound/index.tsx create mode 100644 src/components/Relay/index.tsx create mode 100644 src/components/RelayPageControls/index.tsx create mode 100644 src/pages/primary/RelayPage/index.tsx diff --git a/src/PageManager.tsx b/src/PageManager.tsx index abb96c4c..da47b807 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -16,10 +16,12 @@ import { } from 'react' import BottomNavigationBar from './components/BottomNavigationBar' import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog' +import { normalizeUrl } from './lib/url' import ExplorePage from './pages/primary/ExplorePage' import MePage from './pages/primary/MePage' import NotificationListPage from './pages/primary/NotificationListPage' import ProfilePage from './pages/primary/ProfilePage' +import RelayPage from './pages/primary/RelayPage' import { NotificationProvider } from './providers/NotificationProvider' import { useScreenSize } from './providers/ScreenSizeProvider' import { routes } from './routes' @@ -28,7 +30,7 @@ import modalManager from './services/modal-manager.service' export type TPrimaryPageName = keyof typeof PRIMARY_PAGE_MAP type TPrimaryPageContext = { - navigate: (page: TPrimaryPageName) => void + navigate: (page: TPrimaryPageName, props?: object) => void current: TPrimaryPageName | null display: boolean } @@ -51,7 +53,8 @@ const PRIMARY_PAGE_REF_MAP = { explore: createRef(), notifications: createRef(), me: createRef(), - profile: createRef() + profile: createRef(), + relay: createRef() } const PRIMARY_PAGE_MAP = { @@ -59,7 +62,8 @@ const PRIMARY_PAGE_MAP = { explore: , notifications: , me: , - profile: + profile: , + relay: } const PrimaryPageContext = createContext(undefined) @@ -85,7 +89,7 @@ export function useSecondaryPage() { export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const [currentPrimaryPage, setCurrentPrimaryPage] = useState('home') const [primaryPages, setPrimaryPages] = useState< - { name: TPrimaryPageName; element: ReactNode }[] + { name: TPrimaryPageName; element: ReactNode; props?: any }[] >([ { name: 'home', @@ -131,6 +135,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } return newStack }) + } else { + const searchParams = new URLSearchParams(window.location.search) + const r = searchParams.get('r') + if (r) { + const url = normalizeUrl(r) + if (url) { + navigatePrimaryPage('relay', { url }) + } + } } const onPopState = (e: PopStateEvent) => { @@ -206,12 +219,18 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } }, []) - const navigatePrimaryPage = (page: TPrimaryPageName) => { + const navigatePrimaryPage = (page: TPrimaryPageName, props?: any) => { const needScrollToTop = page === currentPrimaryPage - const exists = primaryPages.find((p) => p.name === page) - if (!exists) { - setPrimaryPages((prev) => [...prev, { name: page, element: PRIMARY_PAGE_MAP[page] }]) - } + setPrimaryPages((prev) => { + const exists = prev.find((p) => p.name === page) + if (exists && props) { + exists.props = props + return [...prev] + } else if (!exists) { + return [...prev, { name: page, element: PRIMARY_PAGE_MAP[page], props }] + } + return prev + }) setCurrentPrimaryPage(page) if (needScrollToTop) { PRIMARY_PAGE_REF_MAP[page].current?.scrollToTop('smooth') @@ -284,7 +303,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { {item.component} ))} - {primaryPages.map(({ name, element }) => ( + {primaryPages.map(({ name, element, props }) => (
- {element} + {props ? cloneElement(element as React.ReactElement, props) : element}
))} @@ -323,7 +342,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
- {primaryPages.map(({ name, element }) => ( + {primaryPages.map(({ name, element, props }) => (
- {element} + {props ? cloneElement(element as React.ReactElement, props) : element}
))}
diff --git a/src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx b/src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx deleted file mode 100644 index 48161c99..00000000 --- a/src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useFeed } from '@/providers/FeedProvider' -import RelayIcon from '../RelayIcon' -import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' - -export default function TemporaryRelaySet() { - const { temporaryRelayUrls } = useFeed() - - if (!temporaryRelayUrls.length) { - return null - } - - return ( -
-
-
-
Temporary
-
- {temporaryRelayUrls.map((url) => ( -
- -
{url}
-
- ))} -
- -
- ) -} diff --git a/src/components/FavoriteRelaysSetting/index.tsx b/src/components/FavoriteRelaysSetting/index.tsx index 0a9c2022..6944088e 100644 --- a/src/components/FavoriteRelaysSetting/index.tsx +++ b/src/components/FavoriteRelaysSetting/index.tsx @@ -3,13 +3,11 @@ import AddNewRelaySet from './AddNewRelaySet' import FavoriteRelayList from './FavoriteRelayList' import { RelaySetsSettingComponentProvider } from './provider' import RelaySetList from './RelaySetList' -import TemporaryRelaySet from './TemporaryRelaySet' export default function FavoriteRelaysSetting() { return (
- diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx index 8426aa16..bb034242 100644 --- a/src/components/FeedSwitcher/index.tsx +++ b/src/components/FeedSwitcher/index.tsx @@ -8,13 +8,12 @@ import { BookmarkIcon, UsersRound } from 'lucide-react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' import RelaySetCard from '../RelaySetCard' -import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' export default function FeedSwitcher({ close }: { close?: () => void }) { const { t } = useTranslation() const { pubkey } = useNostr() const { relaySets, favoriteRelays } = useFavoriteRelays() - const { feedInfo, switchFeed, temporaryRelayUrls } = useFeed() + const { feedInfo, switchFeed } = useFeed() return (
@@ -54,20 +53,6 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { )} - {temporaryRelayUrls.length > 0 && ( - { - switchFeed('temporary') - close?.() - }} - controls={} - > - {temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : t('Temporary')} - - )}
void }) { function FeedSwitcherItem({ children, isActive, - temporary = false, onClick, controls }: { children: React.ReactNode isActive: boolean - temporary?: boolean onClick: () => void controls?: React.ReactNode }) { return (
diff --git a/src/components/NotFound/index.tsx b/src/components/NotFound/index.tsx new file mode 100644 index 00000000..b51e8ef0 --- /dev/null +++ b/src/components/NotFound/index.tsx @@ -0,0 +1,12 @@ +import { useTranslation } from 'react-i18next' + +export default function NotFound() { + const { t } = useTranslation() + + return ( +
+
{t('Lost in the void')} 🌌
+
(404)
+
+ ) +} diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index e2b2020f..66ae035f 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -20,6 +20,7 @@ import client from '@/services/client.service' import { Link, Zap } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import NotFound from '../NotFound' import FollowedBy from './FollowedBy' import Followings from './Followings' import ProfileFeed from './ProfileFeed' @@ -98,7 +99,7 @@ export default function Profile({ id }: { id?: string }) { ) } - if (!profile) return null + if (!profile) return const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile return ( diff --git a/src/components/Relay/index.tsx b/src/components/Relay/index.tsx new file mode 100644 index 00000000..a0b0c7d6 --- /dev/null +++ b/src/components/Relay/index.tsx @@ -0,0 +1,50 @@ +import NormalFeed from '@/components/NormalFeed' +import RelayInfo from '@/components/RelayInfo' +import SearchInput from '@/components/SearchInput' +import { useFetchRelayInfo } from '@/hooks' +import { normalizeUrl } from '@/lib/url' +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import NotFound from '../NotFound' + +export default function Relay({ url, className }: { url?: string; className?: string }) { + const { t } = useTranslation() + const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) + const { relayInfo } = useFetchRelayInfo(normalizedUrl) + const [searchInput, setSearchInput] = useState('') + const [debouncedInput, setDebouncedInput] = useState(searchInput) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedInput(searchInput) + }, 1000) + + return () => { + clearTimeout(handler) + } + }, [searchInput]) + + if (!normalizedUrl) { + return + } + + return ( +
+ + {relayInfo?.supported_nips?.includes(50) && ( +
+ setSearchInput(e.target.value)} + placeholder={t('Search')} + /> +
+ )} + +
+ ) +} diff --git a/src/components/RelayList/index.tsx b/src/components/RelayList/index.tsx index 6bf9b219..d4d52c14 100644 --- a/src/components/RelayList/index.tsx +++ b/src/components/RelayList/index.tsx @@ -1,5 +1,4 @@ -import { toRelay } from '@/lib/link' -import { useSecondaryPage } from '@/PageManager' +import { usePrimaryPage } from '@/PageManager' import relayInfoService from '@/services/relay-info.service' import { TNip66RelayInfo } from '@/types' import { useEffect, useRef, useState } from 'react' @@ -9,7 +8,7 @@ import SearchInput from '../SearchInput' export default function RelayList() { const { t } = useTranslation() - const { push } = useSecondaryPage() + const { navigate } = usePrimaryPage() const [loading, setLoading] = useState(true) const [relays, setRelays] = useState([]) const [showCount, setShowCount] = useState(20) @@ -78,7 +77,7 @@ export default function RelayList() { className="clickable p-4 border-b" onClick={(e) => { e.stopPropagation() - push(toRelay(relay.url)) + navigate('relay', { url: relay.url }) }} /> ))} diff --git a/src/components/RelayPageControls/index.tsx b/src/components/RelayPageControls/index.tsx new file mode 100644 index 00000000..cb412879 --- /dev/null +++ b/src/components/RelayPageControls/index.tsx @@ -0,0 +1,35 @@ +import { Button } from '@/components/ui/button' +import { Check, Copy, Link } from 'lucide-react' +import { useState } from 'react' +import { toast } from 'sonner' +import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' + +export default function RelayPageControls({ url }: { url: string }) { + const [copiedUrl, setCopiedUrl] = useState(false) + const [copiedShareableUrl, setCopiedShareableUrl] = useState(false) + + const handleCopyUrl = () => { + navigator.clipboard.writeText(url) + setCopiedUrl(true) + setTimeout(() => setCopiedUrl(false), 2000) + } + + const handleCopyShareableUrl = () => { + navigator.clipboard.writeText(`https://jumble.social/?r=${url}`) + setCopiedShareableUrl(true) + toast.success('Shareable URL copied to clipboard') + setTimeout(() => setCopiedShareableUrl(false), 2000) + } + + return ( + <> + + + + + ) +} diff --git a/src/pages/primary/NoteListPage/FeedButton.tsx b/src/pages/primary/NoteListPage/FeedButton.tsx index 0374baf1..b9581d78 100644 --- a/src/pages/primary/NoteListPage/FeedButton.tsx +++ b/src/pages/primary/NoteListPage/FeedButton.tsx @@ -71,11 +71,6 @@ const FeedSwitcherTrigger = forwardRef { - } + titlebar={} displayScrollToTopButton > {content} @@ -75,16 +70,13 @@ const NoteListPage = forwardRef((_, ref) => { NoteListPage.displayName = 'NoteListPage' export default NoteListPage -function NoteListPageTitlebar({ temporaryRelayUrls = [] }: { temporaryRelayUrls?: string[] }) { +function NoteListPageTitlebar() { const { isSmallScreen } = useScreenSize() return (
- {temporaryRelayUrls.length > 0 && ( - - )} {isSmallScreen && }
diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx new file mode 100644 index 00000000..63f798dc --- /dev/null +++ b/src/pages/primary/RelayPage/index.tsx @@ -0,0 +1,37 @@ +import Relay from '@/components/Relay' +import RelayPageControls from '@/components/RelayPageControls' +import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' +import { normalizeUrl, simplifyUrl } from '@/lib/url' +import { Server } from 'lucide-react' +import { forwardRef, useMemo } from 'react' + +const RelayPage = forwardRef(({ url }: { url?: string }, ref) => { + const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) + + return ( + } + displayScrollToTopButton + ref={ref} + > + + + ) +}) +RelayPage.displayName = 'RelayPage' +export default RelayPage + +function RelayPageTitlebar({ url }: { url?: string }) { + return ( +
+
+ +
{simplifyUrl(url ?? '')}
+
+
+ +
+
+ ) +} diff --git a/src/pages/secondary/NotFoundPage/index.tsx b/src/pages/secondary/NotFoundPage/index.tsx index 61416e13..e79c8dcd 100644 --- a/src/pages/secondary/NotFoundPage/index.tsx +++ b/src/pages/secondary/NotFoundPage/index.tsx @@ -1,16 +1,11 @@ +import NotFound from '@/components/NotFound' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { forwardRef } from 'react' -import { useTranslation } from 'react-i18next' const NotFoundPage = forwardRef(({ index }: { index?: number }, ref) => { - const { t } = useTranslation() - return ( -
-
{t('Lost in the void')} 🌌
-
(404)
-
+
) }) diff --git a/src/pages/secondary/RelayPage/index.tsx b/src/pages/secondary/RelayPage/index.tsx index 2bc84484..264f748a 100644 --- a/src/pages/secondary/RelayPage/index.tsx +++ b/src/pages/secondary/RelayPage/index.tsx @@ -1,34 +1,13 @@ -import NormalFeed from '@/components/NormalFeed' -import RelayInfo from '@/components/RelayInfo' -import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' -import SearchInput from '@/components/SearchInput' -import { Button } from '@/components/ui/button' -import { useFetchRelayInfo } from '@/hooks' +import Relay from '@/components/Relay' +import RelayPageControls from '@/components/RelayPageControls' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { normalizeUrl, simplifyUrl } from '@/lib/url' -import { Check, Copy, Link } from 'lucide-react' -import { forwardRef, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' +import { forwardRef, useMemo } from 'react' import NotFoundPage from '../NotFoundPage' const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number }, ref) => { - const { t } = useTranslation() const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) - const { relayInfo } = useFetchRelayInfo(normalizedUrl) const title = useMemo(() => (url ? simplifyUrl(url) : undefined), [url]) - const [searchInput, setSearchInput] = useState('') - const [debouncedInput, setDebouncedInput] = useState(searchInput) - - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedInput(searchInput) - }, 1000) - - return () => { - clearTimeout(handler) - } - }, [searchInput]) if (!normalizedUrl) { return @@ -42,54 +21,9 @@ const RelayPage = forwardRef(({ url, index }: { url?: string; index?: number }, controls={} displayScrollToTopButton > -
- - {relayInfo?.supported_nips?.includes(50) && ( -
- setSearchInput(e.target.value)} - placeholder={t('Search')} - /> -
- )} - + ) }) RelayPage.displayName = 'RelayPage' export default RelayPage - -function RelayPageControls({ url }: { url: string }) { - const [copiedUrl, setCopiedUrl] = useState(false) - const [copiedShareableUrl, setCopiedShareableUrl] = useState(false) - - const handleCopyUrl = () => { - navigator.clipboard.writeText(url) - setCopiedUrl(true) - setTimeout(() => setCopiedUrl(false), 2000) - } - - const handleCopyShareableUrl = () => { - navigator.clipboard.writeText(`https://jumble.social/?r=${url}`) - setCopiedShareableUrl(true) - toast.success('Shareable URL copied to clipboard') - setTimeout(() => setCopiedShareableUrl(false), 2000) - } - - return ( - <> - - - - - ) -} diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index b34e9d77..246a937b 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -15,7 +15,6 @@ import { useNostr } from './NostrProvider' type TFeedContext = { feedInfo: TFeedInfo relayUrls: string[] - temporaryRelayUrls: string[] isReady: boolean switchFeed: ( feedType: TFeedType, @@ -34,11 +33,9 @@ export const useFeed = () => { } export function FeedProvider({ children }: { children: React.ReactNode }) { - const isFirstRenderRef = useRef(true) const { pubkey, isInitialized } = useNostr() const { relaySets, favoriteRelays } = useFavoriteRelays() const [relayUrls, setRelayUrls] = useState([]) - const [temporaryRelayUrls, setTemporaryRelayUrls] = useState([]) const [isReady, setIsReady] = useState(false) const [feedInfo, setFeedInfo] = useState({ feedType: 'relay', @@ -48,24 +45,6 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { - const isFirstRender = isFirstRenderRef.current - isFirstRenderRef.current = false - if (isFirstRender) { - // temporary relay urls from query params - const searchParams = new URLSearchParams(window.location.search) - const temporaryRelayUrls = searchParams - .getAll('r') - .map((url) => normalizeUrl(url)) - .filter((url) => url && isWebsocketUrl(url)) - if (temporaryRelayUrls.length) { - return await switchFeed('temporary', { temporaryRelayUrls }) - } - } - - if (feedInfoRef.current.feedType === 'temporary') { - return - } - if (!isInitialized) { return } @@ -106,7 +85,6 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { feedType: TFeedType, options: { activeRelaySetId?: string | null - temporaryRelayUrls?: string[] | null pubkey?: string | null relay?: string | null } = {} @@ -195,26 +173,6 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { setIsReady(true) return } - if (feedType === 'temporary') { - const urls = options.temporaryRelayUrls ?? temporaryRelayUrls - if (!urls.length) { - setIsReady(true) - return - } - - const newFeedInfo = { feedType } - setFeedInfo(newFeedInfo) - feedInfoRef.current = newFeedInfo - setTemporaryRelayUrls(urls) - setRelayUrls(urls) - setIsReady(true) - - const relayInfos = await relayInfoService.getRelayInfos(urls) - client.setCurrentRelayUrls( - urls.filter((_, i) => !relayInfos[i] || !checkAlgoRelay(relayInfos[i])) - ) - return - } setIsReady(true) } @@ -223,7 +181,6 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { value={{ feedInfo, relayUrls, - temporaryRelayUrls, isReady, switchFeed }} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c65cc0b5..2128297c 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -104,7 +104,7 @@ export type TAccount = { export type TAccountPointer = Pick -export type TFeedType = 'following' | 'relays' | 'relay' | 'temporary' | 'bookmarks' +export type TFeedType = 'following' | 'relays' | 'relay' | 'bookmarks' export type TFeedInfo = { feedType: TFeedType; id?: string } export type TLanguage = 'en' | 'zh' | 'pl'