feat: raids

This commit is contained in:
Kieran 2023-12-07 16:41:29 +00:00
parent 4336513184
commit 43bf7f2d00
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
11 changed files with 194 additions and 20 deletions

View File

@ -14,8 +14,8 @@
"@react-hook/resize-observer": "^1.2.6",
"@scure/base": "^1.1.3",
"@snort/shared": "^1.0.10",
"@snort/system": "^1.1.6",
"@snort/system-react": "^1.1.6",
"@snort/system": "^1.1.7",
"@snort/system-react": "^1.1.7",
"@snort/system-wasm": "^1.0.1",
"@snort/system-web": "^1.0.2",
"@szhsin/react-menu": "^4.0.2",

View File

@ -2,6 +2,7 @@ import { EventKind } from "@snort/system";
export const LIVE_STREAM = 30_311 as EventKind;
export const LIVE_STREAM_CHAT = 1_311 as EventKind;
export const LIVE_STREAM_RAID = 1_312 as EventKind;
export const EMOJI_PACK = 30_030 as EventKind;
export const USER_EMOJIS = 10_030 as EventKind;
export const GOAL = 9041 as EventKind;

View File

@ -5,7 +5,7 @@ import classNames from "classnames";
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
disabled?: boolean;
onClick(e: React.MouseEvent): Promise<void> | void;
onClick?: (e: React.MouseEvent) => Promise<void> | void;
children?: React.ReactNode;
}

View File

@ -1,8 +1,8 @@
import "./live-chat.css";
import { FormattedMessage } from "react-intl";
import { EventKind, NostrEvent, NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { EventKind, NostrEvent, NostrLink, ParsedZap, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { useEventReactions, useUserProfile } from "@snort/system-react";
import { unixNow, unwrap } from "@snort/shared";
import { useMemo } from "react";
import { Icon } from "./icon";
@ -18,11 +18,13 @@ import { useLiveChatFeed } from "@/hooks/live-chat";
import { useMutedPubkeys } from "@/hooks/lists";
import { useBadges } from "@/hooks/badges";
import { useLogin } from "@/hooks/login";
import { useAddress } from "@/hooks/event";
import { useAddress, useEvent } from "@/hooks/event";
import { formatSats } from "@/number";
import { LIVE_STREAM_CHAT, WEEK } from "@/const";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, WEEK } from "@/const";
import { findTag, getHost, getTagValues, uniqBy } from "@/utils";
import { TopZappers } from "./top-zappers";
import { Mention } from "./mention";
import { Link } from "react-router-dom";
export interface LiveChatOptions {
canWrite?: boolean;
@ -148,6 +150,9 @@ export function LiveChat({
/>
);
}
case LIVE_STREAM_RAID: {
return <ChatRaid ev={a} link={link} />;
}
case EventKind.ZapReceipt: {
const zap = reactions.zaps.find(b => b.id === a.id && b.receiver === host);
if (zap) {
@ -207,3 +212,25 @@ export function ChatZap({ zap }: { zap: ParsedZap }) {
</div>
);
}
export function ChatRaid({ link, ev }: { link: NostrLink, ev: TaggedNostrEvent }) {
const from = ev.tags.find(a => a[0] === "a" && a[3] === "root");
const to = ev.tags.find(a => a[0] === "a" && a[3] === "mention");
const isRaiding = link.toEventTag()?.at(1) === from?.at(1);
const otherLink = NostrLink.fromTag(unwrap(isRaiding ? to : from));
const otherEvent = useEvent(otherLink);
const otherProfile = useUserProfile(getHost(otherEvent));
if (isRaiding) {
return <Link to={`/${otherLink.encode()}`} className="px-3 py-2 text-center rounded-xl bg-primary uppercase pointer font-bold">
<FormattedMessage defaultMessage="Raiding {name}" id="j/jueq" values={{
name: otherProfile?.name
}} />
</Link>;
}
return <div className="px-3 py-2 text-center rounded-xl bg-primary uppercase pointer font-bold">
<FormattedMessage defaultMessage="Raid from {name}" id="69hmpj" values={{
name: otherProfile?.name
}} />
</div>;
}

76
src/element/raid-menu.tsx Normal file
View File

@ -0,0 +1,76 @@
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 "./profile";
import { useLogin } from "@/hooks/login";
import { useContext, useState } from "react";
import { NostrLink, parseNostrLink } from "@snort/system";
import AsyncButton from "./async-button";
import { SnortContext } from "@snort/system-react";
import { LIVE_STREAM_RAID } from "@/const";
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink, onClose: () => void }) {
const system = useContext(SnortContext);
const login = useLogin();
const { live } = useStreamsFeed();
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 p-6">
<h2>
<FormattedMessage defaultMessage="Start Raid" id="MTHO1W" />
</h2>
<div className="flex flex-col gap-1">
<p className="text-gray-3 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-gray-1 rounded-full px-4 py-2 bg-gray-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-gray-3 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-gray-3 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>
<AsyncButton className="btn btn-primary" onClick={raid}>
<FormattedMessage defaultMessage="Raid!" id="aqjZxs" />
</AsyncButton>
</div>
}

View File

@ -2,7 +2,7 @@ import { NostrLink, NoteCollection, RequestBuilder } from "@snort/system";
import { useReactions, useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import { LIVE_STREAM_CHAT, WEEK } from "@/const";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, WEEK } from "@/const";
export function useLiveChatFeed(link?: NostrLink, eZaps?: Array<string>, limit = 100) {
const since = useMemo(() => unixNow() - WEEK, [link?.id]);
@ -13,14 +13,14 @@ export function useLiveChatFeed(link?: NostrLink, eZaps?: Array<string>, limit =
leaveOpen: true,
});
const aTag = `${link.kind}:${link.author}:${link.id}`;
rb.withFilter().kinds([LIVE_STREAM_CHAT]).tag("a", [aTag]).limit(limit);
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID]).tag("a", [aTag]).limit(limit);
return rb;
}, [link?.id, since, eZaps]);
const feed = useRequestBuilder(NoteCollection, sub);
const messages = useMemo(() => {
return (feed.data ?? []).filter(ev => ev.kind === LIVE_STREAM_CHAT);
return (feed.data ?? []).filter(ev => ev.kind === LIVE_STREAM_CHAT || ev.kind === LIVE_STREAM_RAID);
}, [feed.data]);
const reactions = useReactions(

View File

@ -249,14 +249,16 @@ div.paper {
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
max-width: 550px;
max-height: 85vh;
overflow-y: auto;
}
.dialog-content .header-image {
width: 100%;
height: auto;
}
.dialog-content .content-inner {
width: 100%;
display: flex;
@ -369,4 +371,4 @@ div.paper {
.h-inhreit {
height: inherit;
}
}

View File

@ -5,6 +5,9 @@
"+AcVD+": {
"defaultMessage": "No emails, just awesomeness!"
},
"+sdKx8": {
"defaultMessage": "Live now"
},
"+vVZ/G": {
"defaultMessage": "Connect"
},
@ -53,6 +56,9 @@
"47FYwb": {
"defaultMessage": "Cancel"
},
"4iBdw1": {
"defaultMessage": "Raid"
},
"4l69eO": {
"defaultMessage": "Hmm, your lightning address looks wrong"
},
@ -74,6 +80,9 @@
"5tM0VD": {
"defaultMessage": "Stream Started"
},
"69hmpj": {
"defaultMessage": "Raid from {name}"
},
"6Z2pvJ": {
"defaultMessage": "Stream Providers"
},
@ -191,6 +200,9 @@
"LknBsU": {
"defaultMessage": "Stream Key"
},
"MTHO1W": {
"defaultMessage": "Start Raid"
},
"My6HwN": {
"defaultMessage": "Ok, it's safe"
},
@ -233,6 +245,9 @@
"RJOmzk": {
"defaultMessage": "I have read and agree with {provider}''s {terms}."
},
"RS6smY": {
"defaultMessage": "Raid Message"
},
"RXQdxR": {
"defaultMessage": "Please login to write messages!"
},
@ -282,9 +297,15 @@
"ZmqxZs": {
"defaultMessage": "You can change this later"
},
"Zse7yG": {
"defaultMessage": "Raid target"
},
"acrOoz": {
"defaultMessage": "Continue"
},
"aqjZxs": {
"defaultMessage": "Raid!"
},
"bfvyfs": {
"defaultMessage": "Anon"
},
@ -342,6 +363,9 @@
"izWS4J": {
"defaultMessage": "Unfollow"
},
"j/jueq": {
"defaultMessage": "Raiding {name}"
},
"jctiUc": {
"defaultMessage": "Highest Viewers"
},

View File

@ -15,6 +15,8 @@ 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 * as Dialog from "@radix-ui/react-dialog";
import { DashboardRaidMenu } from "@/element/raid-menu";
export default function DashboardPage() {
const login = useLogin();
@ -47,6 +49,7 @@ function DashboardForLink({ link }: { link: NostrLink }) {
<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>
@ -132,4 +135,19 @@ function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
<Text content={zap.content} tags={[]} />
</div>}
</div>;
}
}
function DashboardRaidButton({ link }: { link: NostrLink }) {
const [show, setShow] = useState(false);
return <Dialog.Root open={show} onOpenChange={setShow}>
<AsyncButton className="btn btn-primary" onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</AsyncButton>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
}

View File

@ -1,6 +1,7 @@
{
"+0zv6g": "Image",
"+AcVD+": "No emails, just awesomeness!",
"+sdKx8": "Live now",
"+vVZ/G": "Connect",
"/0TOL5": "Amount",
"/EvlqN": "nostr signer extension",
@ -17,6 +18,7 @@
"3adEeb": "{n} viewers",
"3df560": "Login with private key",
"47FYwb": "Cancel",
"4iBdw1": "Raid",
"4l69eO": "Hmm, your lightning address looks wrong",
"4l6vz1": "Copy",
"4uI538": "Resolutions",
@ -24,6 +26,7 @@
"5QYdPU": "Start Time",
"5kx+2v": "Server Url",
"5tM0VD": "Stream Started",
"69hmpj": "Raid from {name}",
"6Z2pvJ": "Stream Providers",
"6pr6hJ": "Minimum amount for text to speech",
"79lLl+": "Music",
@ -63,6 +66,7 @@
"KdYELp": "Get stream key",
"KkIL3s": "No, I am under 18",
"LknBsU": "Stream Key",
"MTHO1W": "Start Raid",
"My6HwN": "Ok, it's safe",
"O2Cy6m": "Yes, I am over 18",
"OEW7yJ": "Zaps",
@ -77,6 +81,7 @@
"Qe1MJu": "{name} with {amount}",
"RJ2VxG": "A new version has been detected",
"RJOmzk": "I have read and agree with {provider}''s {terms}.",
"RS6smY": "Raid Message",
"RXQdxR": "Please login to write messages!",
"RrCui3": "Summary",
"RtYNX5": "Chat Users",
@ -93,7 +98,9 @@
"YagVIe": "{n}p",
"Z8ZOEY": "This method is insecure. We recommend using a {nostrlink}",
"ZmqxZs": "You can change this later",
"Zse7yG": "Raid target",
"acrOoz": "Continue",
"aqjZxs": "Raid!",
"bfvyfs": "Anon",
"cPIKU2": "Following",
"cvAsEh": "Streamed on {date}",
@ -113,6 +120,7 @@
"ieGrWo": "Follow",
"itPgxd": "Profile",
"izWS4J": "Unfollow",
"j/jueq": "Raiding {name}",
"jctiUc": "Highest Viewers",
"jgOqxt": "Widgets",
"jkAQj5": "Stream Ended",

View File

@ -2896,14 +2896,14 @@ __metadata:
languageName: node
linkType: hard
"@snort/system-react@npm:^1.1.6":
version: 1.1.6
resolution: "@snort/system-react@npm:1.1.6"
"@snort/system-react@npm:^1.1.7":
version: 1.1.7
resolution: "@snort/system-react@npm:1.1.7"
dependencies:
"@snort/shared": ^1.0.10
"@snort/system": ^1.1.6
react: ^18.2.0
checksum: ea7658d6cf14508e87b6239346b89de34db848817beac7da7cc1b33e1f776920ecc6d079b3470eece3e60c6ec37b50fca94b84bd57b6d7ee24234fa3eb1fc945
checksum: 6e958a8b03c473c9c7878893cc1c79e678a713ea8ab717b2c56eb2b5702809fcfd9944fd9215db3f4e5336acef2d27959d2cfff27a753630b28b037a1e74b2a1
languageName: node
linkType: hard
@ -2961,6 +2961,24 @@ __metadata:
languageName: node
linkType: hard
"@snort/system@npm:^1.1.7":
version: 1.1.7
resolution: "@snort/system@npm:1.1.7"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
"@scure/base": ^1.1.2
"@snort/shared": ^1.0.10
"@stablelib/xchacha20": ^1.0.1
debug: ^4.3.4
eventemitter3: ^5.0.1
isomorphic-ws: ^5.0.0
uuid: ^9.0.0
ws: ^8.14.0
checksum: 881101fc44babb7b7ff081ac5c67c78afc81377b019bc9316be13789a7eaf5cd55dd6001248c8a3c3f837ac90b5082d3509d83ea63dff69103daf357b353ab9b
languageName: node
linkType: hard
"@stablelib/binary@npm:^1.0.1":
version: 1.0.1
resolution: "@stablelib/binary@npm:1.0.1"
@ -7806,8 +7824,8 @@ __metadata:
"@react-hook/resize-observer": ^1.2.6
"@scure/base": ^1.1.3
"@snort/shared": ^1.0.10
"@snort/system": ^1.1.6
"@snort/system-react": ^1.1.6
"@snort/system": ^1.1.7
"@snort/system-react": ^1.1.7
"@snort/system-wasm": ^1.0.1
"@snort/system-web": ^1.0.2
"@szhsin/react-menu": ^4.0.2