15 changed files with 208 additions and 104 deletions
@ -0,0 +1,85 @@ |
|||||||
|
import RelayIcon from '@/components/RelayIcon' |
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu' |
||||||
|
import { useSeenOnRelays } from '@/hooks/useSeenOnRelays' |
||||||
|
import { getKindDescription } from '@/lib/kind-description' |
||||||
|
import { toRelay } from '@/lib/link' |
||||||
|
import { simplifyUrl } from '@/lib/url' |
||||||
|
import { useSecondaryPage } from '@/PageManager' |
||||||
|
import type { Event } from 'nostr-tools' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
|
||||||
|
export default function NoteOptionsMetaHeader({ |
||||||
|
event, |
||||||
|
allowedRelays, |
||||||
|
onNavigate, |
||||||
|
inDropdown = false |
||||||
|
}: { |
||||||
|
event: Event |
||||||
|
allowedRelays?: readonly string[] |
||||||
|
onNavigate?: () => void |
||||||
|
inDropdown?: boolean |
||||||
|
}) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const relays = useSeenOnRelays(event.id, allowedRelays) |
||||||
|
const { description } = getKindDescription(event.kind, event) |
||||||
|
|
||||||
|
const relayRows = relays.map((relay) => { |
||||||
|
const label = ( |
||||||
|
<> |
||||||
|
<RelayIcon url={relay} className="size-4 shrink-0" /> |
||||||
|
<span className="min-w-0 truncate">{simplifyUrl(relay)}</span> |
||||||
|
</> |
||||||
|
) |
||||||
|
|
||||||
|
if (inDropdown) { |
||||||
|
return ( |
||||||
|
<DropdownMenuItem |
||||||
|
key={relay} |
||||||
|
className="min-w-0 gap-2" |
||||||
|
onSelect={() => { |
||||||
|
onNavigate?.() |
||||||
|
push(toRelay(relay)) |
||||||
|
}} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</DropdownMenuItem> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<li key={relay}> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
className="flex w-full min-w-0 items-center gap-2 rounded-md px-1 py-1 text-left text-sm text-foreground hover:bg-muted" |
||||||
|
onClick={() => { |
||||||
|
onNavigate?.() |
||||||
|
push(toRelay(relay)) |
||||||
|
}} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</button> |
||||||
|
</li> |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="space-y-2 border-b border-border px-3 py-2.5"> |
||||||
|
<p className="text-xs leading-snug text-muted-foreground/80" data-note-kind-label> |
||||||
|
{t('Note kind label line', { kind: event.kind, description })} |
||||||
|
</p> |
||||||
|
{relays.length > 0 ? ( |
||||||
|
<div className="space-y-1"> |
||||||
|
<p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground"> |
||||||
|
{t('Seen on')} |
||||||
|
</p> |
||||||
|
{inDropdown ? ( |
||||||
|
<div className="space-y-0.5">{relayRows}</div> |
||||||
|
) : ( |
||||||
|
<ul className="max-h-32 space-y-0.5 overflow-y-auto overscroll-y-contain">{relayRows}</ul> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) : null} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
import { filterRelaysToUserAllowlist } from '@/lib/relay-allowlist' |
||||||
|
import { normalizeAnyRelayUrl } from '@/lib/url' |
||||||
|
import client from '@/services/client.service' |
||||||
|
import { useEffect, useRef, useState } from 'react' |
||||||
|
|
||||||
|
export function useSeenOnRelays( |
||||||
|
eventId: string, |
||||||
|
allowedRelays?: readonly string[] |
||||||
|
): string[] { |
||||||
|
const [relays, setRelays] = useState<string[]>([]) |
||||||
|
const allowedRelaysRef = useRef(allowedRelays) |
||||||
|
allowedRelaysRef.current = allowedRelays |
||||||
|
const allowedRelaysKey = allowedRelays?.length |
||||||
|
? [...allowedRelays] |
||||||
|
.map((u) => normalizeAnyRelayUrl(u) || u.trim()) |
||||||
|
.filter(Boolean) |
||||||
|
.sort() |
||||||
|
.join('|') |
||||||
|
: '' |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
let cancelled = false |
||||||
|
let attempts = 0 |
||||||
|
const maxAttempts = 20 |
||||||
|
const apply = () => { |
||||||
|
const seenOn = client.getSeenEventRelayUrls(eventId) |
||||||
|
const allowlist = allowedRelaysRef.current |
||||||
|
const visible = |
||||||
|
allowlist?.length ? filterRelaysToUserAllowlist(seenOn, allowlist) : seenOn |
||||||
|
if (!cancelled) setRelays(visible) |
||||||
|
return visible.length > 0 |
||||||
|
} |
||||||
|
if (apply()) return |
||||||
|
const id = setInterval(() => { |
||||||
|
if (cancelled) return |
||||||
|
attempts++ |
||||||
|
if (apply() || attempts >= maxAttempts) clearInterval(id) |
||||||
|
}, 500) |
||||||
|
return () => { |
||||||
|
cancelled = true |
||||||
|
clearInterval(id) |
||||||
|
} |
||||||
|
}, [eventId, allowedRelaysKey]) |
||||||
|
|
||||||
|
return relays |
||||||
|
} |
||||||
Loading…
Reference in new issue