From a8d3a9aa55aab4b55d5fda3a2fdbcbb651bdf78c Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 21 Mar 2026 09:57:27 +0100 Subject: [PATCH] more refactor --- .../Explore/ExploreRelayReviews.tsx | 29 +++++++ src/components/Note/RelayReview.tsx | 25 +++++- src/components/NoteInteractions/Tabs.tsx | 82 ------------------- src/components/NoteInteractions/index.tsx | 35 ++++---- src/components/NoteList/index.tsx | 29 ++++++- src/components/QuoteList/index.tsx | 22 ++++- src/components/RelayInfo/RelayReviewCard.tsx | 27 +++++- src/components/ReplyNoteList/index.tsx | 15 +++- src/i18n/locales/ar.ts | 1 + src/i18n/locales/de.ts | 1 + src/i18n/locales/en.ts | 1 + src/i18n/locales/es.ts | 1 + src/i18n/locales/fa.ts | 1 + src/i18n/locales/fr.ts | 1 + src/i18n/locales/hi.ts | 1 + src/i18n/locales/it.ts | 1 + src/i18n/locales/ja.ts | 1 + src/i18n/locales/ko.ts | 1 + src/i18n/locales/pl.ts | 1 + src/i18n/locales/pt-BR.ts | 1 + src/i18n/locales/pt-PT.ts | 1 + src/i18n/locales/ru.ts | 1 + src/i18n/locales/th.ts | 1 + src/i18n/locales/zh.ts | 1 + src/lib/event-metadata.ts | 7 ++ src/pages/primary/ExplorePage/index.tsx | 6 +- 26 files changed, 175 insertions(+), 118 deletions(-) create mode 100644 src/components/Explore/ExploreRelayReviews.tsx delete mode 100644 src/components/NoteInteractions/Tabs.tsx diff --git a/src/components/Explore/ExploreRelayReviews.tsx b/src/components/Explore/ExploreRelayReviews.tsx new file mode 100644 index 00000000..be792bcb --- /dev/null +++ b/src/components/Explore/ExploreRelayReviews.tsx @@ -0,0 +1,29 @@ +import NoteList from '@/components/NoteList' +import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' +import { + getRelayUrlFromRelayReviewEvent, + getStarsFromRelayReviewEvent +} from '@/lib/event-metadata' +import { Event } from 'nostr-tools' +import { useCallback } from 'react' + +export default function ExploreRelayReviews() { + const extraShouldHideEvent = useCallback((evt: Event) => { + if (evt.kind !== ExtendedKind.RELAY_REVIEW) return false + if (!getRelayUrlFromRelayReviewEvent(evt)) return true + return !getStarsFromRelayReviewEvent(evt) + }, []) + + return ( +
+ +
+ ) +} diff --git a/src/components/Note/RelayReview.tsx b/src/components/Note/RelayReview.tsx index 4919311d..fc9e2279 100644 --- a/src/components/Note/RelayReview.tsx +++ b/src/components/Note/RelayReview.tsx @@ -1,15 +1,36 @@ -import { getStarsFromRelayReviewEvent } from '@/lib/event-metadata' +import { getRelayUrlFromRelayReviewEvent, getStarsFromRelayReviewEvent } from '@/lib/event-metadata' +import { toRelay } from '@/lib/link' +import { simplifyUrl } from '@/lib/url' +import { useSmartRelayNavigation } from '@/PageManager' +import { Link2 } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo } from 'react' import Content from '../Content' import Stars from '../Stars' export default function RelayReview({ event, className }: { event: Event; className?: string }) { + const { navigateToRelay } = useSmartRelayNavigation() const stars = useMemo(() => getStarsFromRelayReviewEvent(event), [event]) + const relayUrl = useMemo(() => getRelayUrlFromRelayReviewEvent(event), [event]) return (
- +
+ + {relayUrl ? ( + + ) : null} +
) diff --git a/src/components/NoteInteractions/Tabs.tsx b/src/components/NoteInteractions/Tabs.tsx deleted file mode 100644 index 64543b54..00000000 --- a/src/components/NoteInteractions/Tabs.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { cn } from '@/lib/utils' -import { useTranslation } from 'react-i18next' -import { useRef, useEffect, useState } from 'react' - -export type TTabValue = 'replies' | 'quotes' -const TABS = [ - { value: 'replies', label: 'Replies' }, - { value: 'quotes', label: 'Quotes' } -] as { value: TTabValue; label: string }[] - -export function Tabs({ - selectedTab, - onTabChange, - hideQuotesForDiscussion = false -}: { - selectedTab: TTabValue - onTabChange: (tab: TTabValue) => void - /** Hide the quotes tab on discussion threads */ - hideQuotesForDiscussion?: boolean -}) { - const { t } = useTranslation() - const tabRefs = useRef<(HTMLDivElement | null)[]>([]) - const containerRef = useRef(null) - const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0, top: 0 }) - - const visibleTabs = hideQuotesForDiscussion ? TABS.filter((tab) => tab.value !== 'quotes') : TABS - - useEffect(() => { - setTimeout(() => { - const activeIndex = visibleTabs.findIndex((tab) => tab.value === selectedTab) - if (activeIndex >= 0 && tabRefs.current[activeIndex] && containerRef.current) { - const activeTab = tabRefs.current[activeIndex] - const container = containerRef.current - const { offsetWidth, offsetLeft, offsetHeight } = activeTab - - // Get the container's top position relative to the viewport - const containerTop = container.getBoundingClientRect().top - const tabTop = activeTab.getBoundingClientRect().top - - // Calculate the indicator's top position relative to the container - // Position it at the bottom of the active tab's row - const relativeTop = tabTop - containerTop + offsetHeight - // Responsive padding: smaller on mobile, larger on desktop - const padding = window.innerWidth < 640 ? 16 : window.innerWidth < 768 ? 32 : 48 - - setIndicatorStyle({ - width: offsetWidth - padding, - left: offsetLeft + padding / 2, - top: relativeTop - 4 // 4px for the indicator height (1px) + spacing - }) - } - }, 20) // ensure tabs are rendered before calculating - }, [selectedTab, visibleTabs]) - - return ( -
-
- {visibleTabs.map((tab, index) => ( -
(tabRefs.current[index] = el)} - className={cn( - `text-center py-2 px-2 sm:px-4 md:px-6 font-semibold whitespace-nowrap clickable cursor-pointer rounded-lg text-xs sm:text-sm md:text-base shrink-0`, - selectedTab === tab.value ? '' : 'text-muted-foreground' - )} - onClick={() => onTabChange(tab.value)} - > - {t(tab.label)} -
- ))} -
-
-
- ) -} diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index e751a5f9..ffe28910 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -3,10 +3,9 @@ import { ExtendedKind } from '@/constants' import { shouldHideInteractions } from '@/lib/event-filtering' import { Event } from 'nostr-tools' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import HideUntrustedContentButton from '../HideUntrustedContentButton' -import QuoteList from '../QuoteList' import ReplyNoteList from '../ReplyNoteList' -import { Tabs, TTabValue } from './Tabs' import ReplySort, { ReplySortOption } from './ReplySort' export default function NoteInteractions({ @@ -16,36 +15,25 @@ export default function NoteInteractions({ pageIndex?: number event: Event }) { - const [type, setType] = useState('replies') + const { t } = useTranslation() const [replySort, setReplySort] = useState('oldest') const isDiscussion = event.kind === ExtendedKind.DISCUSSION - + // Hide interactions if event is in quiet mode if (shouldHideInteractions(event)) { return null } - - let list - switch (type) { - case 'replies': - list = - break - case 'quotes': - if (isDiscussion) return null // Hide quotes for discussions - list = - break - default: - break - } return ( <>
-
- +
+
+ {t('Replies')} +
- {type === 'replies' && isDiscussion && ( + {isDiscussion && ( <> @@ -56,7 +44,12 @@ export default function NoteInteractions({
- {list} + ) } diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 2a81ffd7..0a2358a7 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -55,7 +55,8 @@ const NoteList = forwardRef( areAlgoRelays = false, showRelayCloseReason = false, pinnedEventIds = [], - useFilterAsIs = false + useFilterAsIs = false, + extraShouldHideEvent }: { subRequests: TFeedSubRequest[] showKinds: number[] @@ -70,6 +71,8 @@ const NoteList = forwardRef( pinnedEventIds?: string[] /** When true, use filter from subRequests as-is (kinds, limit) instead of showKinds. For spell feeds. */ useFilterAsIs?: boolean + /** When provided and returns true, the event is omitted from the feed (in addition to built-in rules). */ + extraShouldHideEvent?: (evt: Event) => boolean }, ref ) => { @@ -147,9 +150,19 @@ const NoteList = forwardRef( } } + if (extraShouldHideEvent?.(evt)) return true + return false }, - [hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted, zapReplyThreshold] + [ + hideReplies, + hideUntrustedNotes, + mutePubkeySet, + pinnedEventIds, + isEventDeleted, + zapReplyThreshold, + extraShouldHideEvent + ] ) const filteredEvents = useMemo(() => { @@ -409,6 +422,7 @@ const NoteList = forwardRef( if (!isReply && !showKind1OPs) return } if (event.kind === ExtendedKind.COMMENT && !showKind1111) return + if (shouldHideEvent(event)) return if (pubkey && event.pubkey === pubkey) { // If the new event is from the current user, insert it directly into the feed setEvents((oldEvents) => @@ -485,7 +499,16 @@ const NoteList = forwardRef( return () => { promise.then((closer) => closer?.()) } - }, [subRequestsKey, refreshCount, showKindsKey, showKind1OPs, showKind1Replies, showKind1111, useFilterAsIs]) + }, [ + subRequestsKey, + refreshCount, + showKindsKey, + showKind1OPs, + showKind1Replies, + showKind1111, + useFilterAsIs, + shouldHideEvent + ]) // Use refs to avoid dependency issues and ensure latest values in async callbacks const eventsRef = useRef(events) diff --git a/src/components/QuoteList/index.tsx b/src/components/QuoteList/index.tsx index 13b180c9..9ce973af 100644 --- a/src/components/QuoteList/index.tsx +++ b/src/components/QuoteList/index.tsx @@ -8,12 +8,22 @@ import dayjs from 'dayjs' import { Event, kinds } from 'nostr-tools' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' const LIMIT = 100 const SHOW_COUNT = 10 -export default function QuoteList({ event, className }: { event: Event; className?: string }) { +export default function QuoteList({ + event, + className, + embedded = false +}: { + event: Event + className?: string + /** When true, compact layout for use below the replies feed (no full-tab min-height). */ + embedded?: boolean +}) { const { t } = useTranslation() const { relayList: userRelayList } = useNostr() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() @@ -183,8 +193,11 @@ export default function QuoteList({ event, className }: { event: Event; classNam }, [timelineKey, loading, hasMore, events, showCount]) return ( -
-
+
+ {embedded && ( +

{t('Quotes')}

+ )} +
{events.slice(0, showCount).map((event) => { if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) { @@ -201,7 +214,8 @@ export default function QuoteList({ event, className }: { event: Event; classNam
{t('no more notes')}
)}
-
+ {!embedded &&
} + {embedded &&
}
) } diff --git a/src/components/RelayInfo/RelayReviewCard.tsx b/src/components/RelayInfo/RelayReviewCard.tsx index ab2a762a..40ec16a8 100644 --- a/src/components/RelayInfo/RelayReviewCard.tsx +++ b/src/components/RelayInfo/RelayReviewCard.tsx @@ -1,8 +1,10 @@ -import { useSmartNoteNavigation } from '@/PageManager' -import { getStarsFromRelayReviewEvent } from '@/lib/event-metadata' -import { toNote } from '@/lib/link' +import { useSmartNoteNavigation, useSmartRelayNavigation } from '@/PageManager' +import { getRelayUrlFromRelayReviewEvent, getStarsFromRelayReviewEvent } from '@/lib/event-metadata' +import { toNote, toRelay } from '@/lib/link' +import { simplifyUrl } from '@/lib/url' import { cn } from '@/lib/utils' import client from '@/services/client.service' +import { Link2 } from 'lucide-react' import { NostrEvent } from 'nostr-tools' import { useMemo } from 'react' import ClientTag from '../ClientTag' @@ -21,7 +23,9 @@ export default function RelayReviewCard({ className?: string }) { const { navigateToNote } = useSmartNoteNavigation() + const { navigateToRelay } = useSmartRelayNavigation() const stars = useMemo(() => getStarsFromRelayReviewEvent(event), [event]) + const relayUrl = useMemo(() => getRelayUrlFromRelayReviewEvent(event), [event]) return (
- +
+ + {relayUrl ? ( + + ) : null} +
) diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index d256c6a0..de0b3ce1 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -30,6 +30,7 @@ import { useNoteStatsById } from '@/hooks/useNoteStatsById' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LoadingBar } from '../LoadingBar' +import QuoteList from '../QuoteList' import ReplyNote, { ReplyNoteSkeleton } from '../ReplyNote' import ZapReplyFeedRow from './ZapReplyFeedRow' @@ -41,7 +42,18 @@ type TRootInfo = const LIMIT = 100 const SHOW_COUNT = 10 -function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) { +function ReplyNoteList({ + index, + event, + sort = 'oldest', + showQuotes = true +}: { + index?: number + event: NEvent + sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' + /** When false, omit the quotes section (e.g. discussion threads). */ + showQuotes?: boolean +}) { const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigation() const { currentIndex } = useSecondaryPage() @@ -564,6 +576,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even )}
{loading && } + {showQuotes && }
) } diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 0f7528b8..f6a1914d 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -162,6 +162,7 @@ export default { 'Send only to r': 'إرسال فقط إلى {{r}}', 'Send only to these relays': 'إرسال فقط إلى هذه الريلايات', Explore: 'استكشاف', + 'Relay reviews': 'مراجعات الترحيل', 'Search relays': 'البحث في الريلايات', relayInfoBadgeAuth: 'مصادقة', relayInfoBadgeSearch: 'بحث', diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 25c8cb35..0913298f 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -251,6 +251,7 @@ export default { 'Send only to r': 'Nur an {{r}} senden', 'Send only to these relays': 'Nur an diese Relays senden', Explore: 'Entdecken', + 'Relay reviews': 'Bewertungen', 'Search relays': 'Relays suchen', relayInfoBadgeAuth: 'Auth', relayInfoBadgeSearch: 'Suche', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 9779a724..b062cd08 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -310,6 +310,7 @@ export default { 'Send only to r': 'Send only to {{r}}', 'Send only to these relays': 'Send only to these relays', Explore: 'Explore', + 'Relay reviews': 'Relay reviews', 'Search relays': 'Search relays', relayInfoBadgeAuth: 'Auth', relayInfoBadgeSearch: 'Search', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index de3534ff..d817ef73 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -166,6 +166,7 @@ export default { 'Send only to r': 'Enviar únicamente a {{r}}', 'Send only to these relays': 'Enviar únicamente a estos relés', Explore: 'Explorar', + 'Relay reviews': 'Reseñas de relays', 'Search relays': 'Buscar relés', relayInfoBadgeAuth: 'Autenticación', relayInfoBadgeSearch: 'Búsqueda', diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 435ae2f2..b06bf24a 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -164,6 +164,7 @@ export default { 'Send only to r': 'فقط به {{r}} ارسال شود', 'Send only to these relays': 'فقط به این رله‌ها ارسال شود', Explore: 'کاوش', + 'Relay reviews': 'نقد رله‌ها', 'Search relays': 'جستجو رله‌ها', relayInfoBadgeAuth: 'احراز هویت', relayInfoBadgeSearch: 'جستجو', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 312598b7..5fcca787 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -165,6 +165,7 @@ export default { 'Send only to r': 'Envoyer uniquement à {{r}}', 'Send only to these relays': 'Envoyer uniquement à ces relais', Explore: 'Explorer', + 'Relay reviews': 'Avis sur les relais', 'Search relays': 'Rechercher des relais', relayInfoBadgeAuth: 'Auth', relayInfoBadgeSearch: 'Recherche', diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 1939ebbb..ca11af0e 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -165,6 +165,7 @@ export default { 'Send only to r': 'केवल {{r}} को भेजें', 'Send only to these relays': 'केवल इन रिले को भेजें', Explore: 'एक्सप्लोर करें', + 'Relay reviews': 'रिले समीक्षाएँ', 'Search relays': 'रिले खोजें', relayInfoBadgeAuth: 'प्रमाणीकरण', relayInfoBadgeSearch: 'खोज', diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 32138855..cf402b1b 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -165,6 +165,7 @@ export default { 'Send only to r': 'Invia solo a {{r}}', 'Send only to these relays': 'Invia solo a questi relay', Explore: 'Esplora', + 'Relay reviews': 'Recensioni relay', 'Search relays': 'Ricerca relay', relayInfoBadgeAuth: 'Autorizzazione', relayInfoBadgeSearch: 'Ricerca', diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 170f5fd5..8d01f290 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -164,6 +164,7 @@ export default { 'Send only to r': '{{r}} にのみ送信', 'Send only to these relays': 'これらのリレイにのみ送信', Explore: '探索', + 'Relay reviews': 'リレーレビュー', 'Search relays': 'リレイを検索', relayInfoBadgeAuth: '認証', relayInfoBadgeSearch: '検索', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index cfa08e95..52244112 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -165,6 +165,7 @@ export default { 'Send only to r': '{{r}}에만 전송', 'Send only to these relays': '이 릴레이에만 전송', Explore: '탐색', + 'Relay reviews': '릴레이 리뷰', 'Search relays': '릴레이 검색', relayInfoBadgeAuth: '로그인 필요', relayInfoBadgeSearch: '검색 지원', diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 12729108..d09402e5 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -162,6 +162,7 @@ export default { 'Send only to r': 'Wyślij tylko do {{r}}', 'Send only to these relays': 'Wyślij tylko do tych transmiterów', Explore: 'Transmitery', + 'Relay reviews': 'Opinie o relayach', 'Search relays': 'Wyszukaj transmiter', relayInfoBadgeAuth: '✔️', relayInfoBadgeSearch: 'Wyszukiwarka', diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index f3c25462..cc02b165 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -164,6 +164,7 @@ export default { 'Send only to r': 'Enviar apenas para {{r}}', 'Send only to these relays': 'Enviar apenas para estes relays', Explore: 'Explorar', + 'Relay reviews': 'Avaliações de relays', 'Search relays': 'Pesquisar relays', relayInfoBadgeAuth: 'Auth', relayInfoBadgeSearch: 'Pesquisar', diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 266e8924..c9cc9c64 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -165,6 +165,7 @@ export default { 'Send only to r': 'Enviar apenas para {{r}}', 'Send only to these relays': 'Enviar apenas para estes relés', Explore: 'Explorar', + 'Relay reviews': 'Avaliações de relays', 'Search relays': 'Pesquisar relés', relayInfoBadgeAuth: 'Auth', relayInfoBadgeSearch: 'Pesquisar', diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index bca79a9d..bcc82f12 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -167,6 +167,7 @@ export default { 'Send only to r': 'Отправить только на {{r}}', 'Send only to these relays': 'Отправить только на эти ретрансляторы', Explore: 'Обзор', + 'Relay reviews': 'Отзывы о ретрансляторах', 'Search relays': 'Поиск ретрансляторов', relayInfoBadgeAuth: 'Авторизация', relayInfoBadgeSearch: 'Поиск', diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 81ec6db8..520b29e8 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -162,6 +162,7 @@ export default { 'Send only to r': 'ส่งเฉพาะไปยัง {{r}}', 'Send only to these relays': 'ส่งเฉพาะไปยังรีเลย์เหล่านี้', Explore: 'สำรวจ', + 'Relay reviews': 'รีวิวรีเลย์', 'Search relays': 'ค้นหารีเลย์', relayInfoBadgeAuth: 'ยืนยันตัวตน', relayInfoBadgeSearch: 'ค้นหา', diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index a2e7e416..476db1f5 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -164,6 +164,7 @@ export default { 'Send only to r': '只发送到 {{r}}', 'Send only to these relays': '只发送到这些服务器', Explore: '探索', + 'Relay reviews': '中继评价', 'Search relays': '搜索服务器', relayInfoBadgeAuth: '需登陆', relayInfoBadgeSearch: '支持搜索', diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts index d15dd27b..c5a2eec8 100644 --- a/src/lib/event-metadata.ts +++ b/src/lib/event-metadata.ts @@ -577,3 +577,10 @@ export function getStarsFromRelayReviewEvent(event: Event): number { } return 0 } + +/** Relay URL from the `d` tag (NIP for relay reviews). */ +export function getRelayUrlFromRelayReviewEvent(event: Event): string | undefined { + const d = event.tags.find((t) => t[0] === 'd')?.[1]?.trim() + if (!d) return undefined + return normalizeUrl(d) || d +} diff --git a/src/pages/primary/ExplorePage/index.tsx b/src/pages/primary/ExplorePage/index.tsx index ffe4184e..526f9d34 100644 --- a/src/pages/primary/ExplorePage/index.tsx +++ b/src/pages/primary/ExplorePage/index.tsx @@ -1,5 +1,6 @@ import Explore from '@/components/Explore' import ExploreFavoriteRelays from '@/components/Explore/ExploreFavoriteRelays' +import ExploreRelayReviews from '@/components/Explore/ExploreRelayReviews' import FollowingFavoriteRelayList from '@/components/FollowingFavoriteRelayList' import Tabs from '@/components/Tabs' import VersionUpdateBanner from '@/components/VersionUpdateBanner' @@ -55,10 +56,11 @@ function filterMonitoringRelaySuggestions(urls: string[], rawQuery: string): str return matches.slice(0, RELAY_SUGGESTION_LIMIT) } -type TExploreTabs = 'explore' | 'following' +type TExploreTabs = 'explore' | 'reviews' | 'following' function normalizeHomeTab(restored: string): TExploreTabs { if (restored === 'following') return 'following' + if (restored === 'reviews') return 'reviews' // Removed "favorites" tab — treat saved state as Explore return 'explore' } @@ -88,6 +90,7 @@ const ExplorePage = forwardRef((_, ref) => { value={tab} tabs={[ { value: 'explore', label: t('Explore') }, + { value: 'reviews', label: t('Relay reviews') }, { value: 'following', label: t("Following's Favorites") } ]} onTabChange={(next) => { @@ -113,6 +116,7 @@ const ExplorePage = forwardRef((_, ref) => { )} + {tab === 'reviews' && } {tab === 'following' && }