From 43bf7f2d00b722c585755d6185a8a0b256b0daef Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 7 Dec 2023 16:41:29 +0000 Subject: [PATCH] feat: raids --- package.json | 4 +- src/const.ts | 1 + src/element/async-button.tsx | 2 +- src/element/live-chat.tsx | 37 +++++++++++++++--- src/element/raid-menu.tsx | 76 ++++++++++++++++++++++++++++++++++++ src/hooks/live-chat.tsx | 6 +-- src/index.css | 6 ++- src/lang.json | 24 ++++++++++++ src/pages/dashboard.tsx | 20 +++++++++- src/translations/en.json | 8 ++++ yarn.lock | 30 +++++++++++--- 11 files changed, 194 insertions(+), 20 deletions(-) create mode 100644 src/element/raid-menu.tsx diff --git a/package.json b/package.json index 1b1bc39..447a414 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/const.ts b/src/const.ts index da1b40c..3478a9c 100644 --- a/src/const.ts +++ b/src/const.ts @@ -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; diff --git a/src/element/async-button.tsx b/src/element/async-button.tsx index 8b9c99f..193307f 100644 --- a/src/element/async-button.tsx +++ b/src/element/async-button.tsx @@ -5,7 +5,7 @@ import classNames from "classnames"; interface AsyncButtonProps extends React.ButtonHTMLAttributes { disabled?: boolean; - onClick(e: React.MouseEvent): Promise | void; + onClick?: (e: React.MouseEvent) => Promise | void; children?: React.ReactNode; } diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 6b99303..74804ac 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -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 ; + } 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 }) { ); } + +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 + + ; + } + return
+ +
; +} \ No newline at end of file diff --git a/src/element/raid-menu.tsx b/src/element/raid-menu.tsx new file mode 100644 index 0000000..675a4eb --- /dev/null +++ b/src/element/raid-menu.tsx @@ -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
+

+ +

+
+

+ +

+
+ {livePubkeys.map(a =>
{ + const liveEvent = live.find(b => getHost(b) === a); + if (liveEvent) { + setRaiding(NostrLink.fromEvent(liveEvent).encode()); + } + }}> + +
)} +
+
+
+

+ +

+
+ setRaiding(e.target.value)} /> +
+
+
+

+ +

+
+ setMsg(e.target.value)} /> +
+
+ + + +
+} \ No newline at end of file diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index f1a1b36..7c62ece 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -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, limit = 100) { const since = useMemo(() => unixNow() - WEEK, [link?.id]); @@ -13,14 +13,14 @@ export function useLiveChatFeed(link?: NostrLink, eZaps?: Array, 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( diff --git a/src/index.css b/src/index.css index 482840b..c538ea4 100644 --- a/src/index.css +++ b/src/index.css @@ -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; -} \ No newline at end of file +} diff --git a/src/lang.json b/src/lang.json index ffd235b..d885f9e 100644 --- a/src/lang.json +++ b/src/lang.json @@ -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" }, diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index 5673591..a7b2f25 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -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 }) { } value={participants} /> } value={maxParticipants} /> +

@@ -132,4 +135,19 @@ function DashboardHighlightZap({ zap }: { zap: ParsedZap }) { } ; -} \ No newline at end of file +} + +function DashboardRaidButton({ link }: { link: NostrLink }) { + const [show, setShow] = useState(false); + return + setShow(true)}> + + + + + + setShow(false)} /> + + + +} diff --git a/src/translations/en.json b/src/translations/en.json index d77849c..25b35e4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", diff --git a/yarn.lock b/yarn.lock index c81f248..fd830d9 100644 --- a/yarn.lock +++ b/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