diff --git a/.changeset/beige-waves-wash.md b/.changeset/beige-waves-wash.md new file mode 100644 index 000000000..48df44f93 --- /dev/null +++ b/.changeset/beige-waves-wash.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Show streamer cards in stream view on desktop diff --git a/src/components/note/note-contents.tsx b/src/components/note/note-contents.tsx index c3a0b6aa4..1767623a5 100644 --- a/src/components/note/note-contents.tsx +++ b/src/components/note/note-contents.tsx @@ -18,11 +18,12 @@ import { embedEmoji, renderOpenGraphUrl, embedImageGallery, + renderGenericUrl, } from "../embed-types"; import { LightboxProvider } from "../lightbox-provider"; import { renderRedditUrl } from "../embed-types/reddit"; -function buildContents(event: NostrEvent | DraftNostrEvent) { +function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false) { let content: EmbedableContent = [event.content.trim()]; // image gallery @@ -39,7 +40,7 @@ function buildContents(event: NostrEvent | DraftNostrEvent) { renderTidalUrl, renderImageUrl, renderVideoUrl, - renderOpenGraphUrl, + simpleLinks ? renderGenericUrl : renderOpenGraphUrl, ]); // bitcoin @@ -56,16 +57,19 @@ function buildContents(event: NostrEvent | DraftNostrEvent) { export type NoteContentsProps = { event: NostrEvent | DraftNostrEvent; + noOpenGraphLinks?: boolean; }; -export const NoteContents = React.memo(({ event, ...props }: NoteContentsProps & Omit) => { - const content = buildContents(event); +export const NoteContents = React.memo( + ({ event, noOpenGraphLinks, ...props }: NoteContentsProps & Omit) => { + const content = buildContents(event, noOpenGraphLinks); - return ( - - - {content} - - - ); -}); + return ( + + + {content} + + + ); + }, +); diff --git a/src/helpers/nostr/event.ts b/src/helpers/nostr/event.ts index 45f510868..add1cbdb8 100644 --- a/src/helpers/nostr/event.ts +++ b/src/helpers/nostr/event.ts @@ -210,3 +210,19 @@ export function parseRTag(tag: RTag): RelayConfig { return { url: tag[1], mode: RelayMode.ALL }; } } + +export function parseCoordinate(a: string) { + const parts = a.split(":") as (string | undefined)[]; + const kind = parts[0] && parseInt(parts[0]); + const pubkey = parts[1]; + const d = parts[2]; + + if (!kind) return null; + if (!pubkey) return null; + + return { + kind, + pubkey, + d, + }; +} diff --git a/src/types/nostr-event.ts b/src/types/nostr-event.ts index 021e0f045..00031300e 100644 --- a/src/types/nostr-event.ts +++ b/src/types/nostr-event.ts @@ -1,8 +1,9 @@ export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string]; +export type ATag = ["a", string] | ["a", string, string]; export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string]; export type RTag = ["r", string] | ["r", string, string]; export type DTag = ["d"] | ["d", string]; -export type Tag = string[] | ETag | PTag | RTag | DTag; +export type Tag = string[] | ETag | PTag | RTag | DTag | ATag; export type NostrEvent = { id: string; @@ -34,3 +35,6 @@ export function isRTag(tag: Tag): tag is RTag { export function isDTag(tag: Tag): tag is DTag { return tag[0] === "d"; } +export function isATag(tag: Tag): tag is ATag { + return tag[0] === "a" && tag[1] !== undefined; +} diff --git a/src/views/streams/components/stream-card.tsx b/src/views/streams/components/stream-card.tsx index 37af36693..ff07278a5 100644 --- a/src/views/streams/components/stream-card.tsx +++ b/src/views/streams/components/stream-card.tsx @@ -13,6 +13,7 @@ import { LinkBox, LinkOverlay, Spacer, + Tag, Text, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; @@ -52,7 +53,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P {stream.tags.length > 0 && ( {stream.tags.map((tag) => ( - {tag} + {tag} ))} )} diff --git a/src/views/streams/components/streamer-cards.tsx b/src/views/streams/components/streamer-cards.tsx new file mode 100644 index 000000000..ab7622b43 --- /dev/null +++ b/src/views/streams/components/streamer-cards.tsx @@ -0,0 +1,90 @@ +import { useMemo } from "react"; +import { useReadRelayUrls } from "../../../hooks/use-client-relays"; +import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider"; +import replaceableEventLoaderService from "../../../services/replaceable-event-requester"; +import useSubject from "../../../hooks/use-subject"; +import { + Card, + CardBody, + CardHeader, + CardProps, + Code, + Flex, + Heading, + Image, + Link, + LinkBox, + LinkOverlay, +} from "@chakra-ui/react"; +import { NoteContents } from "../../../components/note/note-contents"; +import { isATag } from "../../../types/nostr-event"; +import {} from "nostr-tools"; +import { parseCoordinate } from "../../../helpers/nostr/event"; + +export const STREAMER_CARDS_TYPE = 17777; +export const STREAMER_CARD_TYPE = 37777; + +function useStreamerCardsCords(pubkey: string, relays: string[]) { + const sub = useMemo( + () => replaceableEventLoaderService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey), + [pubkey, relays.join("|")], + ); + const streamerCards = useSubject(sub); + + return streamerCards?.tags.filter(isATag) ?? []; +} +function useStreamerCard(cord: string, relays: string[]) { + const sub = useMemo(() => { + const parsed = parseCoordinate(cord); + if (!parsed || !parsed.d || parsed.kind !== STREAMER_CARD_TYPE) return; + + return replaceableEventLoaderService.requestEvent(relays, STREAMER_CARD_TYPE, parsed.pubkey, parsed.d); + }, [cord, relays.join("|")]); + return useSubject(sub); +} + +function StreamerCard({ cord, relay, ...props }: { cord: string; relay?: string } & CardProps) { + const contextRelays = useRelaySelectionRelays(); + const readRelays = useReadRelayUrls(relay ? [...contextRelays, relay] : contextRelays); + + const card = useStreamerCard(cord, readRelays); + if (!card) return null; + + const title = card.tags.find((t) => t[0] === "title")?.[1]; + const image = card.tags.find((t) => t[0] === "image")?.[1]; + const link = card.tags.find((t) => t[0] === "r")?.[1]; + + return ( + + {image && } + {title && ( + + {title} + + )} + + + {link && ( + + {link} + + )} + + + ); +} + +export default function StreamerCards({ pubkey }: { pubkey: string }) { + const contextRelays = useRelaySelectionRelays(); + const readRelays = useReadRelayUrls(contextRelays); + + const cardCords = useStreamerCardsCords(pubkey, readRelays); + + return ( + + {cardCords.map(([_, cord, relay]) => ( + + ))} + + ); +} diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx index 32da5e244..462c142b2 100644 --- a/src/views/streams/stream/index.tsx +++ b/src/views/streams/stream/index.tsx @@ -1,6 +1,17 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useScroll } from "react-use"; -import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text, useBreakpointValue } from "@chakra-ui/react"; +import { + Box, + Button, + ButtonGroup, + Flex, + Heading, + Spacer, + Spinner, + Tag, + Text, + useBreakpointValue, +} from "@chakra-ui/react"; import { useParams, Navigate, useSearchParams, useNavigate } from "react-router-dom"; import { nip19 } from "nostr-tools"; import { Global, css } from "@emotion/react"; @@ -21,6 +32,7 @@ import replaceableEventLoaderService from "../../../services/replaceable-event-r import useSubject from "../../../hooks/use-subject"; import RelaySelectionButton from "../../../components/relay-selection/relay-selection-button"; import RelaySelectionProvider from "../../../providers/relay-selection-provider"; +import StreamerCards from "../components/streamer-cards"; function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) { const vertical = useBreakpointValue({ base: true, lg: false }); @@ -92,7 +104,7 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode /> )} {!displayMode && ( - + navigate(-1)}>Back + {stream.tags.length > 0 && ( + + {stream.tags.map((tag) => ( + {tag} + ))} + + )} + {!vertical && } )}