Browse Source

more refactor

imwald
Silberengel 1 month ago
parent
commit
a8d3a9aa55
  1. 29
      src/components/Explore/ExploreRelayReviews.tsx
  2. 25
      src/components/Note/RelayReview.tsx
  3. 82
      src/components/NoteInteractions/Tabs.tsx
  4. 35
      src/components/NoteInteractions/index.tsx
  5. 29
      src/components/NoteList/index.tsx
  6. 22
      src/components/QuoteList/index.tsx
  7. 27
      src/components/RelayInfo/RelayReviewCard.tsx
  8. 15
      src/components/ReplyNoteList/index.tsx
  9. 1
      src/i18n/locales/ar.ts
  10. 1
      src/i18n/locales/de.ts
  11. 1
      src/i18n/locales/en.ts
  12. 1
      src/i18n/locales/es.ts
  13. 1
      src/i18n/locales/fa.ts
  14. 1
      src/i18n/locales/fr.ts
  15. 1
      src/i18n/locales/hi.ts
  16. 1
      src/i18n/locales/it.ts
  17. 1
      src/i18n/locales/ja.ts
  18. 1
      src/i18n/locales/ko.ts
  19. 1
      src/i18n/locales/pl.ts
  20. 1
      src/i18n/locales/pt-BR.ts
  21. 1
      src/i18n/locales/pt-PT.ts
  22. 1
      src/i18n/locales/ru.ts
  23. 1
      src/i18n/locales/th.ts
  24. 1
      src/i18n/locales/zh.ts
  25. 7
      src/lib/event-metadata.ts
  26. 6
      src/pages/primary/ExplorePage/index.tsx

29
src/components/Explore/ExploreRelayReviews.tsx

@ -0,0 +1,29 @@ @@ -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 (
<div className="min-w-0 pt-1">
<NoteList
showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={[{ urls: [...FAST_READ_RELAY_URLS], filter: {} }]}
showKind1OPs={false}
showKind1Replies={false}
showKind1111={false}
extraShouldHideEvent={extraShouldHideEvent}
/>
</div>
)
}

25
src/components/Note/RelayReview.tsx

@ -1,15 +1,36 @@ @@ -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 (
<div className={className}>
<Stars stars={stars} className="mt-2" />
<div className="mt-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
<Stars stars={stars} className="gap-0.5 [&_svg]:size-4 shrink-0" />
{relayUrl ? (
<button
type="button"
className="flex min-w-0 max-w-full items-center gap-1 text-left text-sm text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation()
navigateToRelay(toRelay(relayUrl))
}}
>
<Link2 className="size-3.5 shrink-0" aria-hidden />
<span className="truncate font-mono">{simplifyUrl(relayUrl)}</span>
</button>
) : null}
</div>
<Content event={event} className="mt-2" />
</div>
)

82
src/components/NoteInteractions/Tabs.tsx

@ -1,82 +0,0 @@ @@ -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<HTMLDivElement | null>(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 (
<div className="w-full min-w-0">
<div ref={containerRef} className="flex relative gap-1 overflow-x-auto scrollbar-hide">
{visibleTabs.map((tab, index) => (
<div
key={tab.value}
ref={(el) => (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)}
</div>
))}
<div
className="absolute h-1 bg-primary rounded-full transition-all duration-500"
style={{
width: `${indicatorStyle.width}px`,
left: `${indicatorStyle.left}px`,
top: `${indicatorStyle.top}px`
}}
/>
</div>
</div>
)
}

35
src/components/NoteInteractions/index.tsx

@ -3,10 +3,9 @@ import { ExtendedKind } from '@/constants' @@ -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({ @@ -16,36 +15,25 @@ export default function NoteInteractions({
pageIndex?: number
event: Event
}) {
const [type, setType] = useState<TTabValue>('replies')
const { t } = useTranslation()
const [replySort, setReplySort] = useState<ReplySortOption>('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 = <ReplyNoteList index={pageIndex} event={event} sort={replySort} />
break
case 'quotes':
if (isDiscussion) return null // Hide quotes for discussions
list = <QuoteList event={event} />
break
default:
break
}
return (
<>
<div className="flex items-center justify-between">
<div className="flex-1 w-0">
<Tabs selectedTab={type} onTabChange={setType} hideQuotesForDiscussion={isDiscussion} />
<div className="flex-1 w-0 min-w-0">
<div className="py-2 px-2 sm:px-4 md:px-6 font-semibold text-xs sm:text-sm md:text-base text-foreground whitespace-nowrap">
{t('Replies')}
</div>
</div>
<Separator orientation="vertical" className="h-6" />
{type === 'replies' && isDiscussion && (
{isDiscussion && (
<>
<ReplySort selectedSort={replySort} onSortChange={setReplySort} />
<Separator orientation="vertical" className="h-6" />
@ -56,7 +44,12 @@ export default function NoteInteractions({ @@ -56,7 +44,12 @@ export default function NoteInteractions({
</div>
</div>
<Separator />
{list}
<ReplyNoteList
index={pageIndex}
event={event}
sort={replySort}
showQuotes={!isDiscussion}
/>
</>
)
}

29
src/components/NoteList/index.tsx

@ -55,7 +55,8 @@ const NoteList = forwardRef( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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)

22
src/components/QuoteList/index.tsx

@ -8,12 +8,22 @@ import dayjs from 'dayjs' @@ -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 @@ -183,8 +193,11 @@ export default function QuoteList({ event, className }: { event: Event; classNam
}, [timelineKey, loading, hasMore, events, showCount])
return (
<div className={className}>
<div className="min-h-[80vh]">
<div className={cn(className, embedded && 'mt-6 border-t border-border pt-4')}>
{embedded && (
<h3 className="text-sm font-semibold text-muted-foreground mb-3 px-4">{t('Quotes')}</h3>
)}
<div className={embedded ? undefined : 'min-h-[80vh]'}>
<div>
{events.slice(0, showCount).map((event) => {
if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) {
@ -201,7 +214,8 @@ export default function QuoteList({ event, className }: { event: Event; classNam @@ -201,7 +214,8 @@ export default function QuoteList({ event, className }: { event: Event; classNam
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
)}
</div>
<div className="h-40" />
{!embedded && <div className="h-40" />}
{embedded && <div className="pb-8" />}
</div>
)
}

27
src/components/RelayInfo/RelayReviewCard.tsx

@ -1,8 +1,10 @@ @@ -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({ @@ -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 (
<div
@ -55,7 +59,22 @@ export default function RelayReviewCard({ @@ -55,7 +59,22 @@ export default function RelayReviewCard({
</div>
</div>
</div>
<Stars stars={stars} className="mt-2 gap-0.5 [&_svg]:size-3" />
<div className="mt-2 flex flex-wrap items-center justify-between gap-x-2 gap-y-1">
<Stars stars={stars} className="gap-0.5 [&_svg]:size-3 shrink-0" />
{relayUrl ? (
<button
type="button"
className="flex min-w-0 max-w-full items-center gap-1 text-left text-xs text-muted-foreground hover:text-foreground"
onClick={(e) => {
e.stopPropagation()
navigateToRelay(toRelay(relayUrl))
}}
>
<Link2 className="size-3 shrink-0" aria-hidden />
<span className="truncate font-mono">{simplifyUrl(relayUrl)}</span>
</button>
) : null}
</div>
<ContentPreview className="mt-2 line-clamp-4" event={event} />
</div>
)

15
src/components/ReplyNoteList/index.tsx

@ -30,6 +30,7 @@ import { useNoteStatsById } from '@/hooks/useNoteStatsById' @@ -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 = @@ -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 @@ -564,6 +576,7 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
)}
<div ref={bottomRef} />
{loading && <ReplyNoteSkeleton />}
{showQuotes && <QuoteList event={event} embedded />}
</div>
)
}

1
src/i18n/locales/ar.ts

@ -162,6 +162,7 @@ export default { @@ -162,6 +162,7 @@ export default {
'Send only to r': 'إرسال فقط إلى {{r}}',
'Send only to these relays': 'إرسال فقط إلى هذه الريلايات',
Explore: 'استكشاف',
'Relay reviews': 'مراجعات الترحيل',
'Search relays': 'البحث في الريلايات',
relayInfoBadgeAuth: 'مصادقة',
relayInfoBadgeSearch: 'بحث',

1
src/i18n/locales/de.ts

@ -251,6 +251,7 @@ export default { @@ -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',

1
src/i18n/locales/en.ts

@ -310,6 +310,7 @@ export default { @@ -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',

1
src/i18n/locales/es.ts

@ -166,6 +166,7 @@ export default { @@ -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',

1
src/i18n/locales/fa.ts

@ -164,6 +164,7 @@ export default { @@ -164,6 +164,7 @@ export default {
'Send only to r': 'فقط به {{r}} ارسال شود',
'Send only to these relays': 'فقط به این رلهها ارسال شود',
Explore: 'کاوش',
'Relay reviews': 'نقد رلهها',
'Search relays': 'جستجو رلهها',
relayInfoBadgeAuth: 'احراز هویت',
relayInfoBadgeSearch: 'جستجو',

1
src/i18n/locales/fr.ts

@ -165,6 +165,7 @@ export default { @@ -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',

1
src/i18n/locales/hi.ts

@ -165,6 +165,7 @@ export default { @@ -165,6 +165,7 @@ export default {
'Send only to r': 'कवल {{r}} क',
'Send only to these relays': 'कवल इन रि',
Explore: 'एकसपर कर',
'Relay reviews': 'रि सम',
'Search relays': 'रि',
relayInfoBadgeAuth: 'परमकरण',
relayInfoBadgeSearch: 'खज',

1
src/i18n/locales/it.ts

@ -165,6 +165,7 @@ export default { @@ -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',

1
src/i18n/locales/ja.ts

@ -164,6 +164,7 @@ export default { @@ -164,6 +164,7 @@ export default {
'Send only to r': '{{r}} にのみ送信',
'Send only to these relays': 'これらのリレイにのみ送信',
Explore: '探索',
'Relay reviews': 'リレーレビュー',
'Search relays': 'リレイを検索',
relayInfoBadgeAuth: '認証',
relayInfoBadgeSearch: '検索',

1
src/i18n/locales/ko.ts

@ -165,6 +165,7 @@ export default { @@ -165,6 +165,7 @@ export default {
'Send only to r': '{{r}}에만 전송',
'Send only to these relays': '이 릴레이에만 전송',
Explore: '탐색',
'Relay reviews': '릴레이 리뷰',
'Search relays': '릴레이 검색',
relayInfoBadgeAuth: '로그인 필요',
relayInfoBadgeSearch: '검색 지원',

1
src/i18n/locales/pl.ts

@ -162,6 +162,7 @@ export default { @@ -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',

1
src/i18n/locales/pt-BR.ts

@ -164,6 +164,7 @@ export default { @@ -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',

1
src/i18n/locales/pt-PT.ts

@ -165,6 +165,7 @@ export default { @@ -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',

1
src/i18n/locales/ru.ts

@ -167,6 +167,7 @@ export default { @@ -167,6 +167,7 @@ export default {
'Send only to r': 'Отправить только на {{r}}',
'Send only to these relays': 'Отправить только на эти ретрансляторы',
Explore: 'Обзор',
'Relay reviews': 'Отзывы о ретрансляторах',
'Search relays': 'Поиск ретрансляторов',
relayInfoBadgeAuth: 'Авторизация',
relayInfoBadgeSearch: 'Поиск',

1
src/i18n/locales/th.ts

@ -162,6 +162,7 @@ export default { @@ -162,6 +162,7 @@ export default {
'Send only to r': 'สงเฉพาะไปยง {{r}}',
'Send only to these relays': 'สงเฉพาะไปยงรเลยเหลาน',
Explore: 'สำรวจ',
'Relay reviews': 'รวรเลย',
'Search relays': 'คนหารเลย',
relayInfoBadgeAuth: 'ยนยนตวตน',
relayInfoBadgeSearch: 'คนหา',

1
src/i18n/locales/zh.ts

@ -164,6 +164,7 @@ export default { @@ -164,6 +164,7 @@ export default {
'Send only to r': '只发送到 {{r}}',
'Send only to these relays': '只发送到这些服务器',
Explore: '探索',
'Relay reviews': '中继评价',
'Search relays': '搜索服务器',
relayInfoBadgeAuth: '需登陆',
relayInfoBadgeSearch: '支持搜索',

7
src/lib/event-metadata.ts

@ -577,3 +577,10 @@ export function getStarsFromRelayReviewEvent(event: Event): number { @@ -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
}

6
src/pages/primary/ExplorePage/index.tsx

@ -1,5 +1,6 @@ @@ -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 @@ -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) => { @@ -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) => { @@ -113,6 +116,7 @@ const ExplorePage = forwardRef((_, ref) => {
<Explore />
</>
)}
{tab === 'reviews' && <ExploreRelayReviews />}
{tab === 'following' && <FollowingFavoriteRelayList />}
</div>
</PrimaryPageLayout>

Loading…
Cancel
Save