Updates:
- Game categories - Move stream setup/config to dashboard - Reorg files / cleanup - NSFW improvements
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
22
src/pages/dashboard/button-raid.tsx
Normal file
22
src/pages/dashboard/button-raid.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
33
src/pages/dashboard/button-settings.tsx
Normal file
33
src/pages/dashboard/button-settings.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
10
src/pages/dashboard/card.tsx
Normal file
10
src/pages/dashboard/card.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
src/pages/dashboard/chat-list.tsx
Normal file
25
src/pages/dashboard/chat-list.tsx
Normal 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>
|
||||
));
|
||||
}
|
30
src/pages/dashboard/column-zaps.tsx
Normal file
30
src/pages/dashboard/column-zaps.tsx
Normal 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>
|
||||
);
|
||||
}
|
82
src/pages/dashboard/dashboard.tsx
Normal file
82
src/pages/dashboard/dashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
10
src/pages/dashboard/index.tsx
Normal file
10
src/pages/dashboard/index.tsx
Normal 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)} />;
|
||||
}
|
9
src/pages/dashboard/intro.tsx
Normal file
9
src/pages/dashboard/intro.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export default function DashboardIntro() {
|
||||
return <>
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Welcome to zap.stream!" />
|
||||
</h1>
|
||||
</>
|
||||
}
|
85
src/pages/dashboard/raid-menu.tsx
Normal file
85
src/pages/dashboard/raid-menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
15
src/pages/dashboard/stats-card.tsx
Normal file
15
src/pages/dashboard/stats-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
31
src/pages/dashboard/zap-highlight.tsx
Normal file
31
src/pages/dashboard/zap-highlight.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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"
|
||||
|
@ -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";
|
||||
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user