From 21c6be919533debe24e96f9ae63c6fa6a793ddaa Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 28 May 2025 21:15:11 +0100 Subject: [PATCH] feat: link to short link --- src/const.ts | 2 ++ src/element/stream/stream-tile.tsx | 43 +++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/const.ts b/src/const.ts index 91e17f1..21459d3 100644 --- a/src/const.ts +++ b/src/const.ts @@ -54,3 +54,5 @@ function loadWhitelist() { } export const WHITELIST: Array | undefined = loadWhitelist(); + +export const NIP5_DOMAIN: string = import.meta.env.VITE_NIP5_DOMAIN || "zap.stream"; \ No newline at end of file diff --git a/src/element/stream/stream-tile.tsx b/src/element/stream/stream-tile.tsx index 5d90626..ea519b6 100644 --- a/src/element/stream/stream-tile.tsx +++ b/src/element/stream/stream-tile.tsx @@ -3,19 +3,43 @@ import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; import { getName } from "../profile"; -import { StreamState } from "@/const"; +import { NIP5_DOMAIN, StreamState } from "@/const"; import useImgProxy from "@/hooks/img-proxy"; import { formatSats } from "@/number"; import { extractStreamInfo, getHost, profileLink } from "@/utils"; import { useUserProfile } from "@snort/system-react"; import classNames from "classnames"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Avatar } from "../avatar"; import Logo from "../logo"; import { useContentWarning } from "../nsfw"; import PillOpaque from "../pill-opaque"; import { RelativeTime } from "../relative-time"; import { StatePill } from "../state-pill"; +import { NostrJson } from "@snort/shared"; + +const nameCache = new Map(); +async function fetchNostrAddresByPubkey(pubkey: string, domain: string, timeout = 2_000): Promise { + if (!pubkey || !domain) { + return undefined; + } + const cacheKey = `${pubkey}@${domain}`; + if (nameCache.has(cacheKey)) { + return nameCache.get(cacheKey); + } + try { + const res = await fetch(`https://${domain}/.well-known/nostr.json?pubkey=${pubkey}`, { + signal: AbortSignal.timeout(timeout), + }); + const ret = (await res.json()) as NostrJson; + nameCache.set(cacheKey, ret); + + return ret; + } catch { + // ignored + } + return undefined; +} export function StreamTile({ ev, @@ -34,11 +58,22 @@ export function StreamTile({ }) { const { title, image, status, participants, contentWarning, recording, ends } = extractStreamInfo(ev); const host = getHost(ev); + const link = NostrLink.fromEvent(ev); const hostProfile = useUserProfile(host); const isGrownUp = useContentWarning(); const { proxy } = useImgProxy(); + const [videoLink, setVideoLink] = useState(`/${link.encode()}`) - const link = NostrLink.fromEvent(ev); + useEffect(() => { + fetchNostrAddresByPubkey(host, NIP5_DOMAIN).then((h) => { + if (h) { + const names = Object.entries(h.names); + if (names.length > 0) { + setVideoLink(`/${names[0][0]}`); + } + } + }); + }, [videoLink]); const [hasImg, setHasImage] = useState((image?.length ?? 0) > 0 || (recording?.length ?? 0) > 0); return (