- Game categories
- Move stream setup/config to dashboard
- Reorg files / cleanup
- NSFW improvements
This commit is contained in:
2024-03-06 16:31:44 +00:00
parent 0a9bd35f43
commit a385ca3271
49 changed files with 824 additions and 513 deletions

View File

@ -1,5 +1,7 @@
import CategoryLink from "@/element/category-link";
import Pill from "@/element/pill";
import VideoGridSorted from "@/element/video-grid-sorted";
import useGameInfo from "@/hooks/game-info";
import { EventKind, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
@ -55,18 +57,29 @@ export const AllCategories = [
priority: 1,
className: "bg-category-gradient-6",
},
{
id: "science-and-technology",
name: <FormattedMessage defaultMessage="Science & Technology" />,
icon: "dice",
tags: ["science", "technology"],
priority: 1,
className: "bg-category-gradient-7",
},
];
export default function Category() {
const { id } = useParams();
const game = useGameInfo(id);
const cat = AllCategories.find(a => a.id === id);
const sub = useMemo(() => {
if (!cat) return;
const rb = new RequestBuilder(`category:${cat.id}`);
rb.withFilter().kinds([EventKind.LiveEvent]).tag("t", cat.tags);
if (!id) return;
const cat = AllCategories.find(a => a.id === id);
const rb = new RequestBuilder(`category:${id}`);
rb.withFilter().kinds([EventKind.LiveEvent]).tag("t", cat?.tags ?? [id]);
return rb;
}, [cat]);
}, [id]);
const results = useRequestBuilder(sub);
return (
<div>
@ -75,7 +88,17 @@ export default function Category() {
<CategoryLink key={a.id} {...a} />
))}
</div>
<h1 className="uppercase my-4">{id}</h1>
<div className="flex gap-8 py-8">
{game?.cover && <img src={game?.cover} className="h-[250px]" />}
<div className="flex flex-col gap-4">
<h1>{game?.name}</h1>
{game?.genres && <div className="flex gap-2">
{game?.genres?.map(a => <Pill>
{a}
</Pill>)}
</div>}
</div>
</div>
<VideoGridSorted evs={results} showAll={true} />
</div>
);

View File

@ -1,194 +0,0 @@
import { ChatZap, LiveChat } from "@/element/live-chat";
import LiveVideoPlayer from "@/element/live-video-player";
import { MuteButton } from "@/element/mute-button";
import { Profile } from "@/element/profile";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useLogin } from "@/hooks/login";
import { extractStreamInfo } from "@/utils";
import { dedupe } from "@snort/shared";
import { NostrLink, NostrPrefix, ParsedZap, TaggedNostrEvent } from "@snort/system";
import { useEventReactions, useReactions } from "@snort/system-react";
import classNames from "classnames";
import { HTMLProps, ReactNode, useEffect, useMemo, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { Text } from "@/element/text";
import { StreamTimer } from "@/element/stream-time";
import { DashboardRaidMenu } from "@/element/raid-menu";
import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP } from "@/const";
export default function DashboardPage() {
const login = useLogin();
if (!login) return;
return <DashboardForLink link={new NostrLink(NostrPrefix.PublicKey, login.pubkey)} />;
}
function DashboardForLink({ link }: { link: NostrLink }) {
const streamEvent = useCurrentStreamFeed(link, true);
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
const { stream, status, image, participants } = extractStreamInfo(streamEvent);
const [maxParticipants, setMaxParticipants] = useState(0);
useEffect(() => {
if (participants) {
setMaxParticipants(v => (v < Number(participants) ? Number(participants) : v));
}
}, [participants]);
const feed = useReactions(
`live:${link?.id}:${streamLink?.author}:reactions`,
streamLink ? [streamLink] : [],
rb => {
if (streamLink) {
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
}
},
true
);
if (!streamLink) return;
return (
<div className="grid grid-cols-3 gap-2 h-[calc(100%-48px-1rem)]">
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
<DashboardCard className="flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</h3>
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
<div className="flex gap-4">
<DashboardStatsCard
name={<FormattedMessage defaultMessage="Stream Time" id="miQKuZ" />}
value={<StreamTimer ev={streamEvent} />}
/>
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" id="37mth/" />} value={participants} />
<DashboardStatsCard
name={<FormattedMessage defaultMessage="Highest Viewers" id="jctiUc" />}
value={maxParticipants}
/>
</div>
<DashboardRaidButton link={streamLink} />
</DashboardCard>
<DashboardCard className="flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Chat Users" id="RtYNX5" />
</h3>
<div className="h-[calc(100%-4rem)] overflow-y-scroll">
<DashboardChatList feed={feed} />
</div>
</DashboardCard>
</div>
<DashboardZapColumn link={streamLink} feed={feed} />
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
</div>
);
}
function DashboardCard(props: HTMLProps<HTMLDivElement>) {
return (
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-layer-1", props.className)}>
{props.children}
</div>
);
}
function DashboardStatsCard({
name,
value,
...props
}: { name: ReactNode; value: ReactNode } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) {
return (
<div
{...props}
className={classNames("flex-1 bg-layer-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
<div className="text-layer-4 font-medium">{name}</div>
<div>{value}</div>
</div>
);
}
function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent> }) {
const pubkeys = useMemo(() => {
return dedupe(feed.map(a => a.pubkey));
}, [feed]);
return pubkeys.map(a => (
<div className="flex justify-between items-center px-4 py-2 border-b border-layer-1">
<Profile pubkey={a} avatarSize={32} gap={4} />
<div className="flex gap-2">
<MuteButton pubkey={a} />
<DefaultButton onClick={() => {}} className="font-bold">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton>
</div>
</div>
));
}
function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent> }) {
const reactions = useEventReactions(link, feed);
const sortedZaps = useMemo(
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
[reactions.zaps]
);
const latestZap = sortedZaps.at(0);
return (
<DashboardCard className="min-h-0 h-full flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</h3>
<div className="flex flex-col gap-2 overflow-y-scroll">
{latestZap && <DashboardHighlightZap zap={latestZap} />}
{sortedZaps.slice(1).map(a => (
<ChatZap zap={a} />
))}
</div>
</DashboardCard>
);
}
function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
return (
<div className="px-4 py-6 bg-layer-1 flex flex-col gap-4 rounded-xl animate-flash">
<div className="flex justify-between items-center text-zap text-2xl font-semibold">
<Profile
pubkey={zap.sender ?? "anon"}
options={{
showAvatar: false,
}}
/>
<span>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: <FormattedNumber value={zap.amount} />,
}}
/>
</span>
</div>
{zap.content && (
<div className="text-2xl">
<Text content={zap.content} tags={[]} />
</div>
)}
</div>
);
}
function DashboardRaidButton({ link }: { link: NostrLink }) {
const [show, setShow] = useState(false);
return (
<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</DefaultButton>
{show && (
<Modal id="raid-menu" onClose={() => setShow(false)}>
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Modal>
)}
</>
);
}

View File

@ -0,0 +1,22 @@
import { NostrLink } from "@snort/system";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { DashboardRaidMenu } from "./raid-menu";
import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
export function DashboardRaidButton({ link }: { link: NostrLink; }) {
const [show, setShow] = useState(false);
return (
<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</DefaultButton>
{show && (
<Modal id="raid-menu" onClose={() => setShow(false)}>
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Modal>
)}
</>
);
}

View File

@ -0,0 +1,33 @@
import { TaggedNostrEvent } from "@snort/system";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
import { getCurrentStreamProvider } from "@/hooks/stream-provider";
import NostrProviderDialog from "@/element/provider/nostr";
import { NostrStreamProvider } from "@/providers";
export function DashboardSettingsButton({ ev }: { ev?: TaggedNostrEvent }) {
const [show, setShow] = useState(false);
const provider = getCurrentStreamProvider(ev) as NostrStreamProvider;
return (
<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Settings" />
</DefaultButton>
{show && (
<Modal id="dashboard-settings" onClose={() => setShow(false)}>
<div className="flex flex-col gap-4">
<NostrProviderDialog
provider={provider}
ev={ev}
showEndpoints={true}
showForwards={true}
showEditor={false}
/>
</div>
</Modal>
)}
</>
);
}

View File

@ -0,0 +1,10 @@
import classNames from "classnames";
import { HTMLProps } from "react";
export function DashboardCard(props: HTMLProps<HTMLDivElement>) {
return (
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-layer-1", props.className)}>
{props.children}
</div>
);
}

View File

@ -0,0 +1,25 @@
import { MuteButton } from "@/element/mute-button";
import { Profile } from "@/element/profile";
import { dedupe } from "@snort/shared";
import { TaggedNostrEvent } from "@snort/system";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { DefaultButton } from "@/element/buttons";
export function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent>; }) {
const pubkeys = useMemo(() => {
return dedupe(feed.map(a => a.pubkey));
}, [feed]);
return pubkeys.map(a => (
<div className="flex justify-between items-center px-4 py-2 border-b border-layer-1">
<Profile pubkey={a} avatarSize={32} gap={4} />
<div className="flex gap-2">
<MuteButton pubkey={a} />
<DefaultButton onClick={() => { }} className="font-bold">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton>
</div>
</div>
));
}

View File

@ -0,0 +1,30 @@
import { ChatZap } from "@/element/live-chat";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { DashboardCard } from "./card";
import { DashboardHighlightZap } from "./zap-highlight";
export function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent>; }) {
const reactions = useEventReactions(link, feed);
const sortedZaps = useMemo(
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
[reactions.zaps]
);
const latestZap = sortedZaps.at(0);
return (
<DashboardCard className="min-h-0 h-full flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</h3>
<div className="flex flex-col gap-2 overflow-y-scroll">
{latestZap && <DashboardHighlightZap zap={latestZap} />}
{sortedZaps.slice(1).map(a => (
<ChatZap zap={a} />
))}
</div>
</DashboardCard>
);
}

View File

@ -0,0 +1,82 @@
import { LiveChat } from "@/element/live-chat";
import LiveVideoPlayer from "@/element/live-video-player";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { extractStreamInfo } from "@/utils";
import { NostrLink } from "@snort/system";
import { useReactions } from "@snort/system-react";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { StreamTimer } from "@/element/stream-time";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP } from "@/const";
import { DashboardRaidButton } from "./button-raid";
import { DashboardZapColumn } from "./column-zaps";
import { DashboardChatList } from "./chat-list";
import { DashboardStatsCard } from "./stats-card";
import { DashboardCard } from "./card";
import { NewStreamDialog } from "@/element/new-stream";
import { DashboardSettingsButton } from "./button-settings";
import DashboardIntro from "./intro";
export function DashboardForLink({ link }: { link: NostrLink; }) {
const streamEvent = useCurrentStreamFeed(link, true);
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
const { stream, status, image, participants } = extractStreamInfo(streamEvent);
const [maxParticipants, setMaxParticipants] = useState(0);
useEffect(() => {
if (participants) {
setMaxParticipants(v => (v < Number(participants) ? Number(participants) : v));
}
}, [participants]);
const feed = useReactions(
`live:${link?.id}:${streamLink?.author}:reactions`,
streamLink ? [streamLink] : [],
rb => {
if (streamLink) {
rb.withFilter()
.kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP])
.replyToLink([streamLink]);
}
},
true
);
if (!streamLink) return <DashboardIntro />;
return (
<div className="grid grid-cols-3 gap-2 h-[calc(100%-48px-1rem)]">
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
<DashboardCard className="flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</h3>
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
<div className="flex gap-4">
<DashboardStatsCard
name={<FormattedMessage defaultMessage="Stream Time" id="miQKuZ" />}
value={<StreamTimer ev={streamEvent} />} />
<DashboardStatsCard name={<FormattedMessage defaultMessage="Viewers" id="37mth/" />} value={participants} />
<DashboardStatsCard
name={<FormattedMessage defaultMessage="Highest Viewers" id="jctiUc" />}
value={maxParticipants} />
</div>
<div className="grid gap-2 grid-cols-3">
<DashboardRaidButton link={streamLink} />
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
<DashboardSettingsButton ev={streamEvent} />
</div>
</DashboardCard>
<DashboardCard className="flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Chat Users" id="RtYNX5" />
</h3>
<div className="h-[calc(100%-4rem)] overflow-y-scroll">
<DashboardChatList feed={feed} />
</div>
</DashboardCard>
</div>
<DashboardZapColumn link={streamLink} feed={feed} />
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
</div>
);
}

View File

@ -0,0 +1,10 @@
import { useLogin } from "@/hooks/login";
import { NostrLink, NostrPrefix } from "@snort/system";
import { DashboardForLink } from "./dashboard";
export default function DashboardPage() {
const login = useLogin();
if (!login) return;
return <DashboardForLink link={new NostrLink(NostrPrefix.PublicKey, login.pubkey)} />;
}

View File

@ -0,0 +1,9 @@
import { FormattedMessage } from "react-intl";
export default function DashboardIntro() {
return <>
<h1>
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
</h1>
</>
}

View File

@ -0,0 +1,85 @@
import { useStreamsFeed } from "@/hooks/live-streams";
import { getHost, getTagValues } from "@/utils";
import { dedupe, unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
import { Profile } from "../../element/profile";
import { useLogin } from "@/hooks/login";
import { useContext, useState } from "react";
import { NostrLink, parseNostrLink } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { LIVE_STREAM_RAID } from "@/const";
import { DefaultButton } from "../../element/buttons";
import { useSortedStreams } from "@/hooks/useLiveStreams";
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: () => void }) {
const system = useContext(SnortContext);
const login = useLogin();
const streams = useStreamsFeed();
const { live } = useSortedStreams(streams);
const [raiding, setRaiding] = useState("");
const [msg, setMsg] = useState("");
const mutedHosts = new Set(getTagValues(login?.muted.tags ?? [], "p"));
const livePubkeys = dedupe(live.map(a => getHost(a))).filter(a => !mutedHosts.has(a));
async function raid() {
if (login) {
const ev = await login.publisher().generic(eb => {
return eb
.kind(LIVE_STREAM_RAID)
.tag(unwrap(link.toEventTag("root")))
.tag(unwrap(parseNostrLink(raiding).toEventTag("mention")))
.content(msg);
});
await system.BroadcastEvent(ev);
onClose();
}
}
return (
<div className="flex flex-col gap-4">
<h2>
<FormattedMessage defaultMessage="Start Raid" id="MTHO1W" />
</h2>
<div className="flex flex-col gap-1">
<p className="text-layer-4 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Live now" id="+sdKx8" />
</p>
<div className="flex gap-2 flex-wrap">
{livePubkeys.map(a => (
<div
className="border border-layer-1 rounded-full px-4 py-2 bg-layer-2 pointer"
onClick={() => {
const liveEvent = live.find(b => getHost(b) === a);
if (liveEvent) {
setRaiding(NostrLink.fromEvent(liveEvent).encode());
}
}}>
<Profile pubkey={a} options={{ showAvatar: false }} linkToProfile={false} />
</div>
))}
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-layer-4 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Raid target" id="Zse7yG" />
</p>
<div className="paper">
<input type="text" placeholder="naddr.." value={raiding} onChange={e => setRaiding(e.target.value)} />
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-layer-4 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Raid Message" id="RS6smY" />
</p>
<div className="paper">
<input type="text" value={msg} onChange={e => setMsg(e.target.value)} />
</div>
</div>
<DefaultButton onClick={raid}>
<FormattedMessage defaultMessage="Raid!" id="aqjZxs" />
</DefaultButton>
</div>
);
}

View File

@ -0,0 +1,15 @@
import classNames from "classnames";
import { HTMLProps, ReactNode } from "react";
export function DashboardStatsCard({
name, value, ...props
}: { name: ReactNode; value: ReactNode; } & Omit<HTMLProps<HTMLDivElement>, "children" | "name" | "value">) {
return (
<div
{...props}
className={classNames("flex-1 bg-layer-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
<div className="text-layer-4 font-medium">{name}</div>
<div>{value}</div>
</div>
);
}

View File

@ -0,0 +1,31 @@
import { Profile } from "@/element/profile";
import { ParsedZap } from "@snort/system";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { Text } from "@/element/text";
export function DashboardHighlightZap({ zap }: { zap: ParsedZap; }) {
return (
<div className="px-4 py-6 bg-layer-1 flex flex-col gap-4 rounded-xl animate-flash">
<div className="flex justify-between items-center text-zap text-2xl font-semibold">
<Profile
pubkey={zap.sender ?? "anon"}
options={{
showAvatar: false,
}} />
<span>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: <FormattedNumber value={zap.amount} />,
}} />
</span>
</div>
{zap.content && (
<div className="text-2xl">
<Text content={zap.content} tags={[]} />
</div>
)}
</div>
);
}

View File

@ -10,13 +10,12 @@ import { hexToBech32 } from "@snort/shared";
import { Icon } from "@/element/icon";
import { useLogin, useLoginEvents } from "@/hooks/login";
import { Profile } from "@/element/profile";
import { NewStreamDialog } from "@/element/new-stream";
import { LoginSignup } from "@/element/login-signup";
import { Login } from "@/login";
import { useLang } from "@/hooks/lang";
import { AllLocales } from "@/intl";
import { trackEvent } from "@/utils";
import { BorderButton } from "@/element/buttons";
import { BorderButton, DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
import Logo from "@/element/logo";
@ -64,7 +63,14 @@ export function LayoutPage() {
return (
<>
{(!import.meta.env.VITE_SINGLE_PUBLISHER || import.meta.env.VITE_SINGLE_PUBLISHER === login.pubkey) && (
<NewStreamDialog btnClassName="btn btn-primary" />
<Link to="/dashboard">
<DefaultButton>
<span className="max-lg:hidden">
<FormattedMessage defaultMessage="Stream" />
</span>
<Icon name="signal" />
</DefaultButton>
</Link>
)}
<Menu
menuClassName="ctx-menu"

View File

@ -1,4 +1,4 @@
import { NostrProviderDialog } from "@/element/nostr-provider-dialog";
import NostrProviderDialog from "@/element/provider/nostr";
import { useStreamProvider } from "@/hooks/stream-provider";
import { NostrStreamProvider } from "@/providers";
import { unwrap } from "@snort/shared";

View File

@ -1,10 +1,10 @@
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
import { NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { useLocation, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { NostrEvent } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { FormattedMessage } from "react-intl";
import { Suspense, lazy, useContext } from "react";
import { useMediaQuery } from "usehooks-ts";
const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
import { extractStreamInfo, findTag, getEventFromLocationState, getHost } from "@/utils";
@ -20,7 +20,7 @@ import { StreamCards } from "@/element/stream-cards";
import { formatSats } from "@/number";
import { StreamTimer } from "@/element/stream-time";
import { ShareMenu } from "@/element/share-menu";
import { ContentWarningOverlay, isContentWarningAccepted } from "@/element/content-warning";
import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useStreamLink } from "@/hooks/stream-link";
import { FollowButton } from "@/element/follow-button";
@ -29,8 +29,8 @@ import { StreamState } from "@/const";
import { NotificationsButton } from "@/element/notifications-button";
import { WarningButton } from "@/element/buttons";
import Pill from "@/element/pill";
import { useMediaQuery } from "usehooks-ts";
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
import GameInfoCard from "@/element/game-info";
function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
const system = useContext(SnortContext);
@ -40,7 +40,7 @@ function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEve
const profile = useUserProfile(host);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const { status, participants, title, summary, service } = extractStreamInfo(ev);
const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev);
const isMine = ev?.pubkey === login?.pubkey;
async function deleteStream() {
@ -60,6 +60,7 @@ function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEve
<div className="grow flex flex-col gap-2 max-xl:hidden">
<h1>{title}</h1>
<p>{summary}</p>
<div className="flex gap-2 flex-wrap">
<StatePill state={status as StreamState} />
<Pill>
@ -70,13 +71,16 @@ function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEve
<StreamTimer ev={ev} />
</Pill>
)}
<Pill>
<GameInfoCard gameId={gameId} gameInfo={gameInfo} showImage={false} link={true} />
</Pill>
{ev && <Tags ev={ev} />}
</div>
{isMine && (
<div className="flex gap-4">
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
{ev && <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />}
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" id="K3r6DQ" />
<FormattedMessage defaultMessage="Delete" />
</WarningButton>
</div>
)}
@ -115,18 +119,18 @@ export function StreamPageHandler() {
if (!link) return;
if (link.kind === EventKind.LiveEvent) {
return <StreamPage link={link} evPreload={evPreload} />;
} else {
if (link.type === NostrPrefix.Event) {
return (
<div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">
<NostrEventElement link={link} />
</div>
);
} else {
return <StreamPage link={link} evPreload={evPreload} />;
}
}
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link: NostrLink }) {
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
const ev = useCurrentStreamFeed(link, true, evPreload);
const host = getHost(ev);
const evLink = ev ? NostrLink.fromEvent(ev) : undefined;
@ -143,8 +147,9 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
} = extractStreamInfo(ev);
const goal = useZapGoal(goalTag);
const isDesktop = useMediaQuery("(min-width: 1280px)");
const isGrownUp = useContentWarning();
if (contentWarning && !isContentWarningAccepted()) {
if (contentWarning && !isGrownUp) {
return <ContentWarningOverlay />;
}