Show streamer cards in stream view on desktop

This commit is contained in:
hzrd149
2023-08-23 11:57:10 -05:00
parent e4927e7d22
commit f83d1ad7df
7 changed files with 156 additions and 16 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show streamer cards in stream view on desktop

View File

@@ -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<BoxProps, "children">) => {
const content = buildContents(event);
export const NoteContents = React.memo(
({ event, noOpenGraphLinks, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
const content = buildContents(event, noOpenGraphLinks);
return (
<LightboxProvider>
<Box whiteSpace="pre-wrap" {...props}>
{content}
</Box>
</LightboxProvider>
);
});
return (
<LightboxProvider>
<Box whiteSpace="pre-wrap" {...props}>
{content}
</Box>
</LightboxProvider>
);
},
);

View File

@@ -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,
};
}

View File

@@ -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;
}

View File

@@ -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 && (
<Flex gap="2" wrap="wrap">
{stream.tags.map((tag) => (
<Badge key={tag}>{tag}</Badge>
<Tag key={tag}>{tag}</Tag>
))}
</Flex>
)}

View File

@@ -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 (
<Card as={LinkBox} {...props}>
{image && <Image src={image} />}
{title && (
<CardHeader p="2">
<Heading size="md">{title}</Heading>
</CardHeader>
)}
<CardBody p="2">
<NoteContents event={card} noOpenGraphLinks />
{link && (
<LinkOverlay isExternal href={link} color="blue.500">
{link}
</LinkOverlay>
)}
</CardBody>
</Card>
);
}
export default function StreamerCards({ pubkey }: { pubkey: string }) {
const contextRelays = useRelaySelectionRelays();
const readRelays = useReadRelayUrls(contextRelays);
const cardCords = useStreamerCardsCords(pubkey, readRelays);
return (
<Flex wrap="wrap" gap="2">
{cardCords.map(([_, cord, relay]) => (
<StreamerCard key={cord} cord={cord} relay={relay} maxW="lg" />
))}
</Flex>
);
}

View File

@@ -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 && (
<Flex gap={vertical ? "2" : "4"} direction="column" flexGrow={vertical ? 0 : 1}>
<Flex gap={vertical ? "2" : "4"} direction="column" flexGrow={vertical ? 0 : 1} pb="4">
<LiveVideoPlayer
stream={stream.streaming || stream.recording}
autoPlay={!!stream.streaming}
@@ -113,6 +125,14 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
<Button onClick={() => navigate(-1)}>Back</Button>
</Flex>
<StreamSummaryContent stream={stream} px={vertical ? "2" : 0} />
{stream.tags.length > 0 && (
<Flex gap="2" wrap="wrap">
{stream.tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Flex>
)}
{!vertical && <StreamerCards pubkey={stream.host} />}
</Flex>
)}
<StreamChat