Browse Source

add active relays component

imwald
Silberengel 2 weeks ago
parent
commit
aaa41e8df4
  1. 115
      src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx
  2. 97
      src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx
  3. 2
      src/components/Sidebar/index.tsx
  4. 77
      src/hooks/useRelayConnectionRows.ts
  5. 3
      src/i18n/locales/de.ts
  6. 3
      src/i18n/locales/en.ts
  7. 55
      src/layouts/PrimaryPageLayout/index.tsx
  8. 17
      src/layouts/SecondaryPageLayout/index.tsx
  9. 35
      src/pages/primary/NoteListPage/index.tsx
  10. 12
      src/services/client.service.ts

115
src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
import { useSecondaryPage } from '@/PageManager'
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Server } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
/**
* Same interaction pattern as {@link SeenOnButton}: Server + counts, menu lists relays with {@link RelayIcon}.
* Shows favorites + default/inbox relays; disconnected sockets are muted.
*/
export function ActiveRelaysTitlebarButton() {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage()
const { rows, connectedCount } = useRelayConnectionRows()
const [drawerOpen, setDrawerOpen] = useState(false)
const trigger = (
<Button
variant="ghost"
size="titlebar-icon"
className="shrink-0 gap-0.5 text-muted-foreground hover:text-primary disabled:opacity-40"
title={t('Active relays')}
aria-label={t('Active relays')}
disabled={rows.length === 0}
onClick={() => {
if (isSmallScreen) setDrawerOpen(true)
}}
>
<Server className="size-5 shrink-0" />
{rows.length > 0 ? (
<span className="text-xs tabular-nums leading-none">
<span className="text-foreground">{connectedCount}</span>
<span className="text-muted-foreground">/{rows.length}</span>
</span>
) : null}
</Button>
)
const rowClass = (connected: boolean) =>
cn(!connected && 'opacity-45 text-muted-foreground')
if (isSmallScreen) {
return (
<>
{trigger}
<Drawer handleOnly open={drawerOpen} onOpenChange={setDrawerOpen}>
<DrawerOverlay onClick={() => setDrawerOpen(false)} />
<DrawerContent
hideOverlay
dragHandle="vaul"
className="flex max-h-[min(85dvh,32rem)] flex-col gap-0"
>
<DrawerHeader className="sr-only">
<DrawerTitle>{t('Active relays')}</DrawerTitle>
</DrawerHeader>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-1 py-2 pb-4">
{rows.map(({ url, connected }) => (
<Button
className={cn('h-auto w-full justify-start gap-3 p-4 text-base', rowClass(connected))}
variant="ghost"
key={url}
title={connected ? simplifyUrl(url) : `${simplifyUrl(url)}${t('Not connected')}`}
onClick={() => {
setDrawerOpen(false)
setTimeout(() => push(toRelay(url)), 50)
}}
>
<RelayIcon url={url} />
{simplifyUrl(url)}
</Button>
))}
</div>
</DrawerContent>
</Drawer>
</>
)
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t('Active relays')}</DropdownMenuLabel>
<DropdownMenuSeparator />
{rows.map(({ url, connected }) => (
<DropdownMenuItem
key={url}
title={connected ? simplifyUrl(url) : `${simplifyUrl(url)}${t('Not connected')}`}
onClick={() => push(toRelay(url))}
className={cn('min-w-52 gap-2', rowClass(connected))}
>
<RelayIcon url={url} />
{simplifyUrl(url)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}

97
src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
import { useSecondaryPage } from '@/PageManager'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useRelayConnectionRows } from '@/hooks/useRelayConnectionRows'
import { toRelay } from '@/lib/link'
import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
const MAX_ICONS = 14
function rowMenuClass(connected: boolean) {
return cn(!connected && 'opacity-50 text-muted-foreground')
}
/**
* Desktop sidebar: relay avatars for favorites + defaults + inbox; muted when the pool socket is down.
*/
export function ConnectedRelaysSidebarStrip({ className }: { className?: string }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { rows } = useRelayConnectionRows()
const shown = rows.slice(0, MAX_ICONS)
const overflowRows = rows.slice(MAX_ICONS)
const overflow = overflowRows.length
if (rows.length === 0) {
return (
<div className={cn('px-1 py-1.5 xl:px-0', className)} title={t('Active relays')}>
<p className="text-center text-[0.6rem] font-medium text-muted-foreground xl:text-left">{t('Active relays')}</p>
<p className="mt-0.5 text-center text-[0.55rem] text-muted-foreground/80 xl:text-left"></p>
</div>
)
}
return (
<div className={cn('px-1 py-2 xl:px-0', className)} title={t('Active relays')}>
<p className="mb-1.5 text-center text-[0.65rem] font-medium leading-snug text-foreground xl:text-left">
{t('Active relays')}
</p>
<div className="flex flex-wrap justify-center gap-1 xl:justify-start">
{shown.map(({ url, connected }) => (
<span
key={url}
title={
connected ? simplifyUrl(url) : `${simplifyUrl(url)}${t('Not connected')}`
}
className={cn('inline-flex', !connected && 'opacity-40 grayscale')}
>
<RelayIcon url={url} className="h-5 w-5" iconSize={11} />
</span>
))}
{overflow > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-5 min-h-5 min-w-5 shrink-0 rounded-full bg-muted px-1 py-0 text-[0.6rem] font-medium tabular-nums text-muted-foreground hover:bg-muted/80 hover:text-foreground"
title={t('More relays', { count: overflow })}
aria-label={t('More relays', { count: overflow })}
>
+{overflow}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="max-h-[min(70vh,24rem)] w-72 overflow-y-auto">
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
{t('More relays', { count: overflow })}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{overflowRows.map(({ url, connected }) => (
<DropdownMenuItem
key={url}
className={cn('min-w-0 gap-2', rowMenuClass(connected))}
title={connected ? simplifyUrl(url) : `${simplifyUrl(url)}${t('Not connected')}`}
onClick={() => push(toRelay(url))}
>
<RelayIcon url={url} className="h-5 w-5 shrink-0" iconSize={11} />
<span className="truncate">{simplifyUrl(url)}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null}
</div>
</div>
)
}

2
src/components/Sidebar/index.tsx

@ -12,6 +12,7 @@ import SearchButton from './SearchButton' @@ -12,6 +12,7 @@ import SearchButton from './SearchButton'
import FollowsLatestButton from './FollowsLatestButton'
import FavoritesButton from './FavoritesButton'
import SpellsButton from './SpellsButton'
import { ConnectedRelaysSidebarStrip } from '@/components/ConnectedRelays/ConnectedRelaysSidebarStrip'
import { FavoriteRelaysActiveStripSidebar } from '@/components/FavoriteRelaysActiveStrip'
import PaneModeToggle from './PaneModeToggle'
import DownloadDesktopSidebarButton from './DownloadDesktopSidebarButton'
@ -48,6 +49,7 @@ export default function PrimaryPageSidebar() { @@ -48,6 +49,7 @@ export default function PrimaryPageSidebar() {
<SpellsButton />
<RssButton />
<FavoriteRelaysActiveStripSidebar />
<ConnectedRelaysSidebarStrip />
<PostButton />
</div>
<div className="space-y-2">

77
src/hooks/useRelayConnectionRows.ts

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
import { useEffect, useMemo, useState } from 'react'
const POLL_MS = 1500
function canon(url: string): string {
return (normalizeAnyRelayUrl(url) || url).trim().toLowerCase()
}
function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const list of lists) {
if (!list?.length) continue
for (const raw of list) {
const n = normalizeAnyRelayUrl(raw) || raw
const k = canon(n)
if (!k || seen.has(k)) continue
seen.add(k)
out.push(n)
}
}
return out
}
export type TRelayConnectionRow = { url: string; connected: boolean }
/**
* Relays to show in active relays UI: favorites + NIP-65 read/write + defaults + fast-read,
* then any pool-connected URL not already listed. {@link row.connected} reflects the live WebSocket.
*/
export function useRelayConnectionRows(): {
rows: TRelayConnectionRow[]
connectedCount: number
} {
const { relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const [connectedCanon, setConnectedCanon] = useState<Set<string>>(() =>
new Set(client.getConnectedRelayUrls().map(canon))
)
useEffect(() => {
const tick = () => setConnectedCanon(new Set(client.getConnectedRelayUrls().map(canon)))
tick()
const id = window.setInterval(tick, POLL_MS)
return () => clearInterval(id)
}, [])
return useMemo(() => {
const inbox = [...(relayList?.read ?? []), ...(relayList?.write ?? [])]
const base = mergeUniquePreserveOrder(
favoriteRelays,
inbox,
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS
)
const baseCanon = new Set(base.map(canon))
const rows: TRelayConnectionRow[] = base.map((url) => ({
url,
connected: connectedCanon.has(canon(url))
}))
for (const url of client.getConnectedRelayUrls()) {
const k = canon(url)
if (baseCanon.has(k)) continue
rows.push({ url, connected: true })
}
const connectedCount = rows.filter((r) => r.connected).length
return { rows, connectedCount }
}, [favoriteRelays, relayList?.read, relayList?.write, connectedCanon])
}

3
src/i18n/locales/de.ts

@ -531,6 +531,9 @@ export default { @@ -531,6 +531,9 @@ export default {
'Möchtest du diesem Benutzer wirklich nicht mehr folgen?',
'Recent Supporters': 'Neueste Unterstützer',
'Seen on': 'Gesehen auf',
'Active relays': 'Aktive Relays',
'Not connected': 'Nicht verbunden',
'More relays': '+{{count}} Relays',
'Temporarily display this reply': 'Antwort vorübergehend anzeigen',
'Note not found': 'Die Notiz wurde nicht gefunden',
'Invalid embedded note reference': 'Ungültige eingebettete Notiz-Referenz',

3
src/i18n/locales/en.ts

@ -527,6 +527,9 @@ export default { @@ -527,6 +527,9 @@ export default {
'Are you sure you want to unfollow this user?': 'Are you sure you want to unfollow this user?',
'Recent Supporters': 'Recent Supporters',
'Seen on': 'Seen on',
'Active relays': 'Active relays',
'Not connected': 'Not connected',
'More relays': '+{{count}} relays',
'Temporarily display this reply': 'Temporarily display this reply',
'Note not found': 'Note not found',
'Invalid embedded note reference': 'Invalid embedded note reference',

55
src/layouts/PrimaryPageLayout/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator'
import { Titlebar } from '@/components/Titlebar'
@ -10,6 +11,7 @@ import { @@ -10,6 +11,7 @@ import {
isRadixDialogOpen,
shouldIgnoreKeyboardShortcutEvent
} from '@/lib/keyboard-shortcuts'
import { cn } from '@/lib/utils'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
const PrimaryPageLayout = forwardRef(
@ -20,7 +22,8 @@ const PrimaryPageLayout = forwardRef( @@ -20,7 +22,8 @@ const PrimaryPageLayout = forwardRef(
pageName,
displayScrollToTopButton = false,
hideTitlebarBottomBorder = false,
subHeader
subHeader,
suppressMobileDefaultActiveRelaysButton = false
}: {
children?: React.ReactNode
titlebar: React.ReactNode
@ -29,6 +32,11 @@ const PrimaryPageLayout = forwardRef( @@ -29,6 +32,11 @@ const PrimaryPageLayout = forwardRef(
hideTitlebarBottomBorder?: boolean
/** Rendered between titlebar and scroll area; not in scroll flow so it never overlaps content */
subHeader?: React.ReactNode
/**
* When true on small screens, omit the trailing {@link ActiveRelaysTitlebarButton} so the page can
* place it next to the account control (e.g. feed titlebar).
*/
suppressMobileDefaultActiveRelaysButton?: boolean
},
ref
) => {
@ -94,6 +102,8 @@ const PrimaryPageLayout = forwardRef( @@ -94,6 +102,8 @@ const PrimaryPageLayout = forwardRef(
return () => document.removeEventListener('keydown', onKeyDown)
}, [isSmallScreen, current, pageName, display])
const hasTitlebarRow = titlebar != null
if (isSmallScreen) {
return (
<DeepBrowsingProvider active={current === pageName && display}>
@ -104,9 +114,14 @@ const PrimaryPageLayout = forwardRef( @@ -104,9 +114,14 @@ const PrimaryPageLayout = forwardRef(
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}}
>
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{hasTitlebarRow ? (
<PrimaryPageTitlebar
hideBottomBorder={hideTitlebarBottomBorder}
suppressMobileActiveRelays={suppressMobileDefaultActiveRelaysButton}
>
{titlebar}
</PrimaryPageTitlebar>
) : null}
{subHeader && <div className="shrink-0 w-full min-w-0 bg-background">{subHeader}</div>}
<div className="min-w-0 w-full">
{children}
@ -120,9 +135,14 @@ const PrimaryPageLayout = forwardRef( @@ -120,9 +135,14 @@ const PrimaryPageLayout = forwardRef(
return (
<DeepBrowsingProvider active={current === pageName && display} scrollAreaRef={scrollAreaRef}>
<div className="relative flex h-full min-h-0 min-w-0 flex-col">
<PrimaryPageTitlebar hideBottomBorder={hideTitlebarBottomBorder}>
{titlebar}
</PrimaryPageTitlebar>
{hasTitlebarRow ? (
<PrimaryPageTitlebar
hideBottomBorder={hideTitlebarBottomBorder}
suppressMobileActiveRelays={false}
>
{titlebar}
</PrimaryPageTitlebar>
) : null}
{subHeader && (
<div className="min-w-0 shrink-0 bg-background">{subHeader}</div>
)}
@ -132,7 +152,9 @@ const PrimaryPageLayout = forwardRef( @@ -132,7 +152,9 @@ const PrimaryPageLayout = forwardRef(
className={
subHeader
? 'min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-auto'
: 'absolute bottom-0 left-0 right-0 top-12 min-w-0 overflow-y-auto overflow-x-auto'
: hasTitlebarRow
? 'absolute bottom-0 left-0 right-0 top-12 min-w-0 overflow-y-auto overflow-x-auto'
: 'absolute bottom-0 left-0 right-0 top-0 min-w-0 overflow-y-auto overflow-x-auto'
}
>
{children}
@ -153,16 +175,29 @@ export type TPrimaryPageLayoutRef = { @@ -153,16 +175,29 @@ export type TPrimaryPageLayoutRef = {
function PrimaryPageTitlebar({
children,
hideBottomBorder = false
hideBottomBorder = false,
suppressMobileActiveRelays = false
}: {
children?: React.ReactNode
hideBottomBorder?: boolean
suppressMobileActiveRelays?: boolean
}) {
const { isSmallScreen } = useScreenSize()
/** Desktop: relay strip lives in the sidebar only. Narrow screens: titlebar control (or inline on feed). */
const showTrailingActiveRelays = isSmallScreen && !suppressMobileActiveRelays
return (
<Titlebar className="p-1" hideBottomBorder={hideBottomBorder}>
<Titlebar
className={cn(
'py-1',
isSmallScreen ? 'pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]' : 'px-1'
)}
hideBottomBorder={hideBottomBorder}
>
<div className="flex h-full w-full min-w-0 items-center gap-2">
<ReadOnlySessionIndicator variant="titlebar" />
<div className="relative min-h-0 min-w-0 flex-1 h-full">{children}</div>
{showTrailingActiveRelays ? <ActiveRelaysTitlebarButton /> : null}
</div>
</Titlebar>
)

17
src/layouts/SecondaryPageLayout/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { ImwaldBrandBar } from '@/assets/Logo'
import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import { ReadOnlySessionIndicator } from '@/components/ReadOnlySessionIndicator'
import { Titlebar } from '@/components/Titlebar'
@ -11,6 +12,7 @@ import { @@ -11,6 +12,7 @@ import {
import { useSecondaryPage } from '@/PageManager'
import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { cn } from '@/lib/utils'
import { ChevronLeft } from 'lucide-react'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -158,20 +160,26 @@ function SecondaryPageTitlebar({ @@ -158,20 +160,26 @@ function SecondaryPageTitlebar({
hideBottomBorder?: boolean
titlebar?: React.ReactNode
}): JSX.Element {
const { isSmallScreen } = useScreenSize()
const titlebarInset = isSmallScreen
? 'py-1 pl-2 pr-[max(0.75rem,env(safe-area-inset-right,0px))]'
: 'p-1'
if (titlebar) {
return (
<Titlebar
className="flex min-w-0 items-center gap-2 p-1"
className={cn('flex min-w-0 items-center gap-2', titlebarInset)}
hideBottomBorder={hideBottomBorder}
>
<ReadOnlySessionIndicator variant="titlebar" />
<div className="min-h-0 min-w-0 flex-1 h-full">{titlebar}</div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</Titlebar>
)
}
return (
<Titlebar
className="flex min-w-0 gap-1 p-1 items-center font-semibold"
className={cn('flex min-w-0 gap-1 items-center font-semibold', titlebarInset)}
hideBottomBorder={hideBottomBorder}
>
<ReadOnlySessionIndicator variant="titlebar" />
@ -185,7 +193,10 @@ function SecondaryPageTitlebar({ @@ -185,7 +193,10 @@ function SecondaryPageTitlebar({
<BackButton>{title}</BackButton>
</div>
)}
<div className="flex-shrink-0">{controls}</div>
<div className="flex shrink-0 items-center gap-0.5">
{controls}
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</div>
</Titlebar>
)

35
src/pages/primary/NoteListPage/index.tsx

@ -24,6 +24,7 @@ import React, { @@ -24,6 +24,7 @@ import React, {
import { useTranslation } from 'react-i18next'
import { FavoriteRelaysActiveStripMobileBar } from '@/components/FavoriteRelaysActiveStrip'
import FavoriteRelaysFeedPicker from '@/components/FavoriteRelaysFeedPicker'
import { ActiveRelaysTitlebarButton } from '@/components/ConnectedRelays/ActiveRelaysTitlebarButton'
import HelpAndAccountMenu from '@/components/HelpAndAccountMenu'
import Logo from '@/assets/Logo'
import RelaysFeed from './RelaysFeed'
@ -136,20 +137,29 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => { @@ -136,20 +137,29 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
</>
)
/** Desktop: nav/logo/account live in titlebar only on small screens; refresh moves to subheader when present. Omit empty h-12 strip. */
const showNoteListTitlebar =
isSmallScreen ||
!usesSubHeader ||
(feedInfo.feedType === 'relay' && !!feedInfo.id)
return (
<PrimaryPageLayout
pageName="feed"
ref={layoutRef}
suppressMobileDefaultActiveRelaysButton
titlebar={
<NoteListPageTitlebar
layoutRef={layoutRef}
onFeedRefresh={runFeedRefresh}
showTitlebarRefresh={!usesSubHeader}
showRelayDetails={showRelayDetails}
setShowRelayDetails={
feedInfo.feedType === 'relay' && !!feedInfo.id ? setShowRelayDetails : undefined
}
/>
showNoteListTitlebar ? (
<NoteListPageTitlebar
layoutRef={layoutRef}
onFeedRefresh={runFeedRefresh}
showTitlebarRefresh={!usesSubHeader}
showRelayDetails={showRelayDetails}
setShowRelayDetails={
feedInfo.feedType === 'relay' && !!feedInfo.id ? setShowRelayDetails : undefined
}
/>
) : null
}
subHeader={subHeader}
displayScrollToTopButton
@ -283,7 +293,12 @@ function NoteListPageTitlebar({ @@ -283,7 +293,12 @@ function NoteListPageTitlebar({
<Info />
</Button>
)}
{isSmallScreen && <HelpAndAccountMenu variant="titlebar" />}
{isSmallScreen ? (
<>
<ActiveRelaysTitlebarButton />
<HelpAndAccountMenu variant="titlebar" />
</>
) : null}
</div>
</div>
)

12
src/services/client.service.ts

@ -2758,6 +2758,18 @@ class ClientService extends EventTarget { @@ -2758,6 +2758,18 @@ class ClientService extends EventTarget {
return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? ''
}
/** Relay URLs in the pool whose WebSocket is currently connected (`listConnectionStatus`). */
getConnectedRelayUrls(): string[] {
const status = this.pool.listConnectionStatus()
const out: string[] = []
for (const [url, connected] of status) {
if (!connected) continue
const n = normalizeAnyRelayUrl(url) || url
out.push(n)
}
return [...new Set(out)].sort((a, b) => a.localeCompare(b))
}
trackEventSeenOn(eventId: string, relay: AbstractRelay) {
const key = canonicalSeenOnEventId(eventId)
let set = this.pool.seenOn.get(key)

Loading…
Cancel
Save