Skip to content

Commit

Permalink
cleanup stream components
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Dec 2, 2024
1 parent 9f46a7c commit a18c1ec
Show file tree
Hide file tree
Showing 37 changed files with 500 additions and 368 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Card, CardProps, Divider, Flex, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { NostrEvent } from "nostr-tools";
import { isATag } from "applesauce-core/helpers";

import { NostrEvent, isATag } from "../../../types/nostr-event";
import UserLink from "../../user/user-link";
import UserAvatar from "../../user/user-avatar";
import ChatMessageContent from "../../../views/streams/stream/stream-chat/chat-message-content";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { parseStreamEvent } from "../../../helpers/nostr/stream";
import StreamStatusBadge from "../../../views/streams/components/status-badge";
import { getSharableEventAddress } from "../../../services/event-relay-hint";
import { getStreamTitle } from "../../../helpers/nostr/stream";

export default function EmbeddedStreamMessage({
message,
...props
}: Omit<CardProps, "children"> & { message: NostrEvent }) {
const streamCoordinate = message.tags.find(isATag)?.[1];
const streamEvent = useReplaceableEvent(streamCoordinate);
const stream = streamEvent && parseStreamEvent(streamEvent);
const stream = useReplaceableEvent(streamCoordinate);

return (
<Card overflow="hidden" maxH="lg" display="block" p="2" {...props}>
Expand All @@ -25,11 +25,11 @@ export default function EmbeddedStreamMessage({
<Flex gap="2" alignItems="center">
<Link
as={RouterLink}
to={`/streams/${getSharableEventAddress(streamEvent) ?? ""}`}
to={`/streams/${getSharableEventAddress(stream) ?? ""}`}
fontWeight="bold"
fontSize="lg"
>
{stream.title}
{getStreamTitle(stream)}
</Link>
<StreamStatusBadge stream={stream} />
</Flex>
Expand Down
38 changes: 24 additions & 14 deletions src/components/embed-event/event-types/embedded-stream.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import { Card, CardBody, CardProps, Flex, Heading, Image, Link, Tag, Text } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { NostrEvent } from "nostr-tools";

import { parseStreamEvent } from "../../../helpers/nostr/stream";
import { NostrEvent } from "../../../types/nostr-event";
import StreamStatusBadge from "../../../views/streams/components/status-badge";
import UserLink from "../../user/user-link";
import UserAvatar from "../../user/user-avatar";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import Timestamp from "../../timestamp";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import {
getStreamHashtags,
getStreamHost,
getStreamImage,
getStreamRelays,
getStreamStartTime,
getStreamTitle,
} from "../../../helpers/nostr/stream";

export default function EmbeddedStream({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const stream = parseStreamEvent(event);
const naddr = useShareableEventAddress(stream.event, stream.relays);
export default function EmbeddedStream({ stream, ...props }: Omit<CardProps, "children"> & { stream: NostrEvent }) {
const naddr = useShareableEventAddress(stream, getStreamRelays(stream));
const isVertical = useBreakpointValue({ base: true, md: false });
const navigate = useNavigate();

const host = getStreamHost(stream);
const starts = getStreamStartTime(stream);
const hashtags = getStreamHashtags(stream);

return (
<Card {...props} position="relative">
<CardBody p="2" gap="2">
<StreamStatusBadge stream={stream} position="absolute" top="4" left="4" />
{isVertical ? (
<Image
src={stream.image}
src={getStreamImage(stream)}
borderRadius="md"
cursor="pointer"
onClick={() => navigate(`/streams/${naddr}`)}
Expand All @@ -32,7 +42,7 @@ export default function EmbeddedStream({ event, ...props }: Omit<CardProps, "chi
/>
) : (
<Image
src={stream.image}
src={getStreamImage(stream)}
borderRadius="md"
maxH="2in"
maxW="30%"
Expand All @@ -45,24 +55,24 @@ export default function EmbeddedStream({ event, ...props }: Omit<CardProps, "chi

<Heading size="md">
<Link as={RouterLink} to={`/streams/${naddr}`}>
{stream.title}
{getStreamTitle(stream)}
</Link>
</Heading>
<Flex gap="2" alignItems="center" my="2">
<UserAvatar pubkey={stream.host} size="xs" noProxy />
<UserAvatar pubkey={host} size="xs" noProxy />
<Heading size="sm">
<UserLink pubkey={stream.host} />
<UserLink pubkey={host} />
</Heading>
</Flex>

{stream.starts && (
{starts && (
<Text>
Started: <Timestamp timestamp={stream.starts} />
Started: <Timestamp timestamp={starts} />
</Text>
)}
{stream.tags.length > 0 && (
{hashtags.length > 0 && (
<Flex gap="2" wrap="wrap">
{stream.tags.map((tag) => (
{hashtags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion src/components/embed-event/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function EmbedEvent({
case kinds.EncryptedDirectMessage:
return <EmbeddedDM dm={event} {...cardProps} />;
case kinds.LiveEvent:
return <EmbeddedStream event={event} {...cardProps} />;
return <EmbeddedStream stream={event} {...cardProps} />;
case kinds.ZapGoal:
return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />;
case kinds.Emojisets:
Expand Down
33 changes: 16 additions & 17 deletions src/components/timeline-page/generic-note-timeline/stream-note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,57 @@ import {
Image,
LinkBox,
LinkOverlay,
Spacer,
Text,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { NostrEvent } from "nostr-tools";

import { NostrEvent } from "../../../types/nostr-event";
import { parseStreamEvent } from "../../../helpers/nostr/stream";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import UserAvatar from "../../user/user-avatar";
import UserLink from "../../user/user-link";
import StreamStatusBadge from "../../../views/streams/components/status-badge";
import { useAsync } from "react-use";
import Timestamp from "../../timestamp";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import { getStreamHashtags, getStreamHost, getStreamImage, getStreamTitle } from "../../../helpers/nostr/stream";

export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) {
const { value: stream, error } = useAsync(async () => parseStreamEvent(event), [event]);
const ref = useEventIntersectionRef(event);
export default function StreamNote({ stream, ...props }: CardProps & { stream: NostrEvent }) {
const ref = useEventIntersectionRef(stream);
const naddr = useShareableEventAddress(stream);

const naddr = useShareableEventAddress(event);

if (!stream || error) return null;
const host = getStreamHost(stream);
const title = getStreamTitle(stream);
const image = getStreamImage(stream);
const tags = getStreamHashtags(stream);

return (
<Card {...props} ref={ref}>
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
<Flex gap="2">
<Flex gap="2" direction="column">
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={stream.host} size="sm" noProxy />
<UserAvatar pubkey={host} size="sm" noProxy />
<Heading size="sm">
<UserLink pubkey={stream.host} />
<UserLink pubkey={host} />
</Heading>
</Flex>
{stream.image && <Image src={stream.image} alt={stream.title} borderRadius="lg" maxH="15rem" />}
{image && <Image src={image} alt={title} borderRadius="lg" maxH="15rem" />}
<Heading size="md">
<LinkOverlay as={RouterLink} to={`/streams/${naddr}`}>
{stream.title}
{title}
</LinkOverlay>
</Heading>
</Flex>
</Flex>
{stream.tags.length > 0 && (
{tags.length > 0 && (
<Flex gap="2" wrap="wrap">
{stream.tags.map((tag) => (
{tags.map((tag) => (
<Badge key={tag}>{tag}</Badge>
))}
</Flex>
)}
<Text>
Updated:
<Timestamp timestamp={stream.updated} />
<Timestamp timestamp={stream.created_at} />
</Text>
</LinkBox>
<Divider />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl
content = <RepostEvent event={event} />;
break;
case kinds.LiveEvent:
content = <StreamNote event={event} />;
content = <StreamNote stream={event} />;
break;
case kinds.RecommendRelay:
content = <RelayRecommendation event={event} />;
Expand Down
141 changes: 64 additions & 77 deletions src/helpers/nostr/stream.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,91 @@
import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
import { unique } from "../array";
import { ensureNotifyContentMentions } from "./post";
import { getEventCoordinate } from "./event";
import { kinds } from "nostr-tools";
import { getEventPointerFromTag, getTagValue, safeRelayUrl, unixNow } from "applesauce-core/helpers";

/** @deprecated use kinds.LiveEvent instead */
export const STREAM_KIND = kinds.LiveEvent;
/** @deprecated use kinds.LiveChatMessage instead */
export const STREAM_CHAT_MESSAGE_KIND = kinds.LiveChatMessage;

export type ParsedStream = {
event: NostrEvent;
author: string;
host: string;
title?: string;
summary?: string;
image?: string;
updated: number;
status: "live" | "ended" | string;
goal?: string;
starts?: number;
ends?: number;
identifier: string;
tags: string[];
streaming?: string;
recording?: string;
relays?: string[];
};
export type StreamStatus = "live" | "ended" | "planned";

export function getStreamTitle(stream: NostrEvent) {
return getTagValue(stream, "title");
}
export function getStreamSummary(stream: NostrEvent) {
return getTagValue(stream, "summary");
}
export function getStreamImage(stream: NostrEvent) {
return getTagValue(stream, "image");
}

export function getStreamStatus(stream: NostrEvent): StreamStatus {
return (getTagValue(stream, "status") as StreamStatus) || "ended";
}

export function getStreamHost(stream: NostrEvent) {
return stream.tags.filter(isPTag)[0]?.[1] ?? stream.pubkey;
}

export function parseStreamEvent(stream: NostrEvent): ParsedStream {
const title = stream.tags.find((t) => t[0] === "title")?.[1];
const summary = stream.tags.find((t) => t[0] === "summary")?.[1];
const image = stream.tags.find((t) => t[0] === "image")?.[1];
const starts = stream.tags.find((t) => t[0] === "starts")?.[1];
const ends = stream.tags.find((t) => t[0] === "ends")?.[1];
const streaming = stream.tags.find((t) => t[0] === "streaming")?.[1];
const recording = stream.tags.find((t) => t[0] === "recording")?.[1];
const goal = stream.tags.find((t) => t[0] === "goal")?.[1];
const identifier = stream.tags.find((t) => t[0] === "d")?.[1];

if (!identifier) throw new Error("Missing Identifier");

let relays = stream.tags.find((t) => t[0] === "relays");
// remove the first "relays" element
if (relays) {
relays = Array.from(relays);
relays.shift();
}
export function getStreamGoalPointer(stream: NostrEvent) {
const goalTag = stream.tags.find((t) => t[0] === "goal");
return goalTag && getEventPointerFromTag(goalTag);
}

const startTime = starts ? parseInt(starts) : undefined;
let endTime = ends ? parseInt(ends) : undefined;
/** Gets all the streaming urls for a stream */
export function getStreamStreamingURLs(stream: NostrEvent) {
return stream.tags.filter((t) => t[0] === "streaming").map((t) => t[1]);
}

let status = stream.tags.find((t) => t[0] === "status")?.[1] || "ended";
if (status === "ended" && endTime === undefined) endTime = stream.created_at;
if (endTime && endTime > dayjs().unix()) {
status = "ended";
}
export function getStreamRecording(stream: NostrEvent) {
return getTagValue(stream, "recording");
}

// if the stream has not been updated in a day consider it ended
if (stream.created_at < dayjs().subtract(1, "week").unix()) {
status = "ended";
export function getStreamRelays(stream: NostrEvent) {
let found = false;
const relays: string[] = [];

for (const tag of stream.tags) {
if (tag[0] === "relays") {
found = true;
for (let i = 1; i < tag.length; i++) {
const relay = safeRelayUrl(tag[i]);
if (relay && !relays.includes(relay)) relays.push(relay);
}
}
}

const tags = unique(stream.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1] as string));
return found ? relays : undefined;
}

/** Gets the stream start time if it has one */
export function getStreamStartTime(stream: NostrEvent) {
const str = getTagValue(stream, "starts");
return str ? parseInt(str) : undefined;
}

/** Gets the stream end time if it has one */
export function getStreamEndTime(stream: NostrEvent) {
const str = getTagValue(stream, "ends");
return str ? parseInt(str) : getStreamStatus(stream) === "ended" ? stream.created_at : undefined;
}

export function getStreamParticipants(stream: NostrEvent) {
const current = getTagValue(stream, "current_participants");
const total = getTagValue(stream, "total_participants");
return {
author: stream.pubkey,
host: getStreamHost(stream),
event: stream,
updated: stream.created_at,
streaming,
recording,
tags,
title,
summary,
image,
status,
starts: startTime,
ends: endTime,
goal,
identifier,
relays,
current: current ? parseInt(current) : undefined,
total: total ? parseInt(total) : undefined,
};
}

export function getATag(stream: ParsedStream) {
return getEventCoordinate(stream.event);
export function getStreamHashtags(stream: NostrEvent) {
return stream.tags.filter((t) => t[0] === "t").map((t) => t[1]);
}

export function buildChatMessage(stream: ParsedStream, content: string) {
export function buildChatMessage(stream: NostrEvent, content: string) {
let draft: DraftNostrEvent = {
tags: [["a", getATag(stream), "", "root"]],
tags: [["a", getEventCoordinate(stream), "", "root"]],
content,
created_at: dayjs().unix(),
kind: STREAM_CHAT_MESSAGE_KIND,
created_at: unixNow(),
kind: kinds.LiveChatMessage,
};

draft = ensureNotifyContentMentions(draft);
Expand Down
Loading

0 comments on commit a18c1ec

Please sign in to comment.