feat: raids
This commit is contained in:
parent
4336513184
commit
43bf7f2d00
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
76
src/element/raid-menu.tsx
Normal 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>
|
||||
}
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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",
|
||||
|
30
yarn.lock
30
yarn.lock
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user