diff --git a/src/components/Embedded/EmbeddedWebsocketUrl.tsx b/src/components/Embedded/EmbeddedWebsocketUrl.tsx index 0800be11..8a2865a1 100644 --- a/src/components/Embedded/EmbeddedWebsocketUrl.tsx +++ b/src/components/Embedded/EmbeddedWebsocketUrl.tsx @@ -1,5 +1,5 @@ import { useSecondaryPage } from '@/PageManager' -import { toNoteList } from '@/lib/link' +import { toRelay } from '@/lib/link' import { TEmbeddedRenderer } from './types' export function EmbeddedWebsocketUrl({ url }: { url: string }) { @@ -9,7 +9,7 @@ export function EmbeddedWebsocketUrl({ url }: { url: string }) { className="cursor-pointer px-1 text-highlight hover:bg-highlight/20" onClick={(e) => { e.stopPropagation() - push(toNoteList({ relay: url })) + push(toRelay(url)) }} > [ {url} ] diff --git a/src/components/MailboxSetting/MailboxRelay.tsx b/src/components/MailboxSetting/MailboxRelay.tsx index db8e77d5..56d7a87c 100644 --- a/src/components/MailboxSetting/MailboxRelay.tsx +++ b/src/components/MailboxSetting/MailboxRelay.tsx @@ -1,3 +1,4 @@ +import { useSecondaryPage } from '@/PageManager' import { Select, SelectContent, @@ -5,6 +6,7 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' +import { toRelay } from '@/lib/link' import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { CircleX } from 'lucide-react' import { useTranslation } from 'react-i18next' @@ -20,10 +22,14 @@ export default function MailboxRelay({ removeMailboxRelay: (url: string) => void }) { const { t } = useTranslation() + const { push } = useSecondaryPage() return (
-
+
push(toRelay(mailboxRelay.url))} + >
{mailboxRelay.url}
diff --git a/src/components/OthersRelayList/index.tsx b/src/components/OthersRelayList/index.tsx index a1329dd9..5ff25892 100644 --- a/src/components/OthersRelayList/index.tsx +++ b/src/components/OthersRelayList/index.tsx @@ -1,7 +1,8 @@ import { useSecondaryPage } from '@/PageManager' +import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { useFetchRelayList } from '@/hooks/useFetchRelayList' -import { toNoteList } from '@/lib/link' +import { toRelay } from '@/lib/link' import { userIdToPubkey } from '@/lib/pubkey' import { relayListToMailboxRelay } from '@/lib/relay' import { simplifyUrl } from '@/lib/url' @@ -11,7 +12,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' -import { Badge } from '../ui/badge' export default function OthersRelayList({ userId }: { userId: string }) { const { t } = useTranslation() @@ -41,7 +41,7 @@ function RelayItem({ relay }: { relay: TMailboxRelay }) {
push(toNoteList({ relay: url }))} + onClick={() => push(toRelay(url))} >
{simplifyUrl(url)}
@@ -52,7 +52,7 @@ function RelayItem({ relay }: { relay: TMailboxRelay }) { ) : scope === 'write' ? ( {t('Write')} ) : null} - diff --git a/src/components/RelayInfo/index.tsx b/src/components/RelayInfo/index.tsx new file mode 100644 index 00000000..43f4921a --- /dev/null +++ b/src/components/RelayInfo/index.tsx @@ -0,0 +1,95 @@ +import { Badge } from '@/components/ui/badge' +import { useFetchRelayInfos } from '@/hooks' +import { TRelayInfo } from '@/types' +import { GitBranch, Mail, SquareCode } from 'lucide-react' +import RelayIcon from '../RelayIcon' +import UserAvatar from '../UserAvatar' +import Username from '../Username' + +export default function RelayInfo({ url }: { url: string }) { + const { + relayInfos: [relayInfo], + isFetching + } = useFetchRelayInfos([url]) + if (isFetching || !relayInfo) { + return null + } + + return ( +
+
+
+ + {relayInfo.name &&
{relayInfo.name}
} +
+ + {!!relayInfo.tags?.length && + relayInfo.tags.map((tag) => {tag})} + {relayInfo.description && ( +
+ {relayInfo.description} +
+ )} +
+
+ {relayInfo.pubkey && ( +
+
Operator
+
+ + +
+
+ )} + {relayInfo.contact && ( +
+
Contact
+
+ + {relayInfo.contact} +
+
+ )} + {relayInfo.software && ( +
+
Software
+
+ + {formatSoftware(relayInfo.software)} +
+
+ )} + {relayInfo.version && ( +
+
Version
+
+ + {relayInfo.version} +
+
+ )} +
+
+ ) +} + +function formatSoftware(software: string) { + const parts = software.split('/') + return parts[parts.length - 1] +} + +function RelayBadges({ relayInfo }: { relayInfo: TRelayInfo }) { + return ( +
+ {relayInfo.supported_nips?.includes(42) && ( + Auth + )} + {relayInfo.supported_nips?.includes(50) && ( + Search + )} + {relayInfo.limitation?.payment_required && ( + Payment + )} +
+ ) +} diff --git a/src/lib/link.ts b/src/lib/link.ts index d1194cfb..59fcd0e9 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -6,20 +6,11 @@ export const toNote = (eventOrId: Pick | string) => { const nevent = nip19.neventEncode({ id: eventOrId.id, author: eventOrId.pubkey }) return `/notes/${nevent}` } -export const toNoteList = ({ - hashtag, - search, - relay -}: { - hashtag?: string - search?: string - relay?: string -}) => { +export const toNoteList = ({ hashtag, search }: { hashtag?: string; search?: string }) => { const path = '/notes' const query = new URLSearchParams() if (hashtag) query.set('t', hashtag.toLowerCase()) if (search) query.set('s', search) - if (relay) query.set('relay', relay) return `${path}?${query.toString()}` } export const toProfile = (pubkeyOrNpub: string) => { @@ -44,6 +35,7 @@ export const toOthersRelaySettings = (pubkey: string) => { export const toRelaySettings = () => '/relay-settings' export const toSettings = () => '/settings' export const toProfileEditor = () => '/profile-editor' +export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}` export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}` export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}` diff --git a/src/pages/secondary/NoteListPage/index.tsx b/src/pages/secondary/NoteListPage/index.tsx index 3d8a7a9e..8987eeae 100644 --- a/src/pages/secondary/NoteListPage/index.tsx +++ b/src/pages/secondary/NoteListPage/index.tsx @@ -1,12 +1,8 @@ import NoteList from '@/components/NoteList' -import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' -import { Button } from '@/components/ui/button' import { SEARCHABLE_RELAY_URLS } from '@/constants' import { useFetchRelayInfos, useSearchParams } from '@/hooks' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { isWebsocketUrl, simplifyUrl } from '@/lib/url' import { useFeed } from '@/providers/FeedProvider' -import { ListPlus } from 'lucide-react' import { Filter } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -16,17 +12,14 @@ export default function NoteListPage({ index }: { index?: number }) { const { relayUrls } = useFeed() const { searchableRelayUrls } = useFetchRelayInfos(relayUrls) const { searchParams } = useSearchParams() - const relayUrlsString = JSON.stringify(relayUrls) const { title = '', filter, - urls, - type + urls } = useMemo<{ title?: string filter?: Filter urls: string[] - type?: 'search' | 'hashtag' | 'relay' }>(() => { const hashtag = searchParams.get('t') if (hashtag) { @@ -46,28 +39,11 @@ export default function NoteListPage({ index }: { index?: number }) { type: 'search' } } - const relayUrl = searchParams.get('relay') - if (relayUrl && isWebsocketUrl(relayUrl)) { - return { title: simplifyUrl(relayUrl), urls: [relayUrl], type: 'relay' } - } return { urls: relayUrls } - }, [searchParams, relayUrlsString]) + }, [searchParams, JSON.stringify(relayUrls)]) return ( - - -
- ) - } - displayScrollToTopButton - > + ) diff --git a/src/pages/secondary/RelayPage/index.tsx b/src/pages/secondary/RelayPage/index.tsx new file mode 100644 index 00000000..5f80b9e5 --- /dev/null +++ b/src/pages/secondary/RelayPage/index.tsx @@ -0,0 +1,36 @@ +import NoteList from '@/components/NoteList' +import RelayInfo from '@/components/RelayInfo' +import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' +import { Button } from '@/components/ui/button' +import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' +import { normalizeUrl, simplifyUrl } from '@/lib/url' +import { ListPlus } from 'lucide-react' +import { useMemo } from 'react' +import NotFoundPage from '../NotFoundPage' + +export default function RelayPage({ url, index }: { url?: string; index?: number }) { + const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) + const title = useMemo(() => (url ? simplifyUrl(url) : undefined), [url]) + + if (!normalizedUrl) { + return + } + + return ( + + + + } + displayScrollToTopButton + > + + + + ) +} diff --git a/src/routes.tsx b/src/routes.tsx index a7f0b024..208ad105 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -8,6 +8,7 @@ import OthersRelaySettingsPage from './pages/secondary/OthersRelaySettingsPage' import ProfileEditorPage from './pages/secondary/ProfileEditorPage' import ProfileListPage from './pages/secondary/ProfileListPage' import ProfilePage from './pages/secondary/ProfilePage' +import RelayPage from './pages/secondary/RelayPage' import RelaySettingsPage from './pages/secondary/RelaySettingsPage' import SettingsPage from './pages/secondary/SettingsPage' @@ -21,7 +22,8 @@ const ROUTES = [ { path: '/users/:id/relays', element: }, { path: '/relay-settings', element: }, { path: '/settings', element: }, - { path: '/profile-editor', element: } + { path: '/profile-editor', element: }, + { path: '/relays/:url', element: } ] export const routes = ROUTES.map(({ path, element }) => ({ diff --git a/src/types.ts b/src/types.ts index ae51a35b..a4b8c00c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,8 +17,19 @@ export type TRelayList = { } export type TRelayInfo = { + name?: string + description?: string + icon?: string + pubkey?: string + contact?: string supported_nips?: number[] software?: string + version?: string + tags?: string[] + limitation?: { + auth_required?: boolean + payment_required?: boolean + } } export type TWebMetadata = {