diff --git a/package.json b/package.json index d36970b..9318fca 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "@react-hook/resize-observer": "^1.2.6", "@scure/base": "^1.1.1", "@snort/shared": "^1.0.9", - "@snort/system": "^1.1.1", - "@snort/system-react": "^1.1.1", + "@snort/system": "^1.1.4", + "@snort/system-react": "^1.1.4", "@snort/system-web": "^1.0.2", "@szhsin/react-menu": "^4.0.2", "@testing-library/jest-dom": "^5.14.1", diff --git a/src/element/chat-message.tsx b/src/element/chat-message.tsx index 7f724da..f8bf470 100644 --- a/src/element/chat-message.tsx +++ b/src/element/chat-message.tsx @@ -55,7 +55,7 @@ export function ChatMessage({ const profile = useUserProfile(inView?.isIntersecting ? ev.pubkey : undefined); const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey; const zapTarget = profile?.lud16 ?? profile?.lud06; - const { zaps, reactions } = useEventReactions(ev, related); + const { zaps, reactions } = useEventReactions(link, related); const emojiNames = emojiPacks.map(p => p.emojis).flat(); const filteredReactions = useMemo(() => { @@ -175,15 +175,15 @@ export function ChatMessage({ style={ isTablet ? { - display: showZapDialog || isHovering ? "flex" : "none", - } + display: showZapDialog || isHovering ? "flex" : "none", + } : { - position: "fixed", - top: topOffset ? topOffset - 12 : 0, - left: leftOffset ? leftOffset - 32 : 0, - opacity: showZapDialog || isHovering ? 1 : 0, - pointerEvents: showZapDialog || isHovering ? "auto" : "none", - } + position: "fixed", + top: topOffset ? topOffset - 12 : 0, + left: leftOffset ? leftOffset - 32 : 0, + opacity: showZapDialog || isHovering ? 1 : 0, + pointerEvents: showZapDialog || isHovering ? "auto" : "none", + } }> {zapTarget && ( { - return zaps.filter(z => z.receiver === ev.pubkey && z.event?.id === ev.id).reduce((acc, z) => acc + z.amount, 0); + return zaps.filter(z => z.receiver === ev.pubkey && z.targetEvents.some(a => a.matchesEvent(ev))).reduce((acc, z) => acc + z.amount, 0); }, [zaps]); const progress = Math.max(0, Math.min(100, (soFar / goalAmount) * 100)); diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 3bfc7a1..7dca967 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -1,8 +1,9 @@ import "./live-chat.css"; import { FormattedMessage } from "react-intl"; -import { EventKind, NostrPrefix, NostrLink, ParsedZap, NostrEvent, parseZap, encodeTLV } from "@snort/system"; +import { EventKind, NostrLink, ParsedZap, NostrEvent } from "@snort/system"; +import { useEventReactions } from "@snort/system-react"; import { unixNow } from "@snort/shared"; -import { useContext, useMemo } from "react"; +import { useMemo } from "react"; import uniqBy from "lodash.uniqby"; import { Icon } from "element/icon"; @@ -23,7 +24,6 @@ import { formatSats } from "number"; import { WEEK, LIVE_STREAM_CHAT } from "const"; import { findTag, getTagValues, getHost } from "utils"; import { TopZappers } from "element/top-zappers"; -import { SnortContext } from "@snort/system-react"; export interface LiveChatOptions { canWrite?: boolean; @@ -61,7 +61,6 @@ export function LiveChat({ options?: LiveChatOptions; height?: number; }) { - const system = useContext(SnortContext); const host = getHost(ev); const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined); const login = useLogin(); @@ -80,15 +79,11 @@ export function LiveChat({ return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId); }, [userEmojiPacks, channelEmojiPacks]); - const zaps = feed.zaps.map(ev => parseZap(ev, system.ProfileLoader.Cache)).filter(z => z && z.valid); + const reactions = useEventReactions(link, feed.reactions); const events = useMemo(() => { - return [...feed.messages, ...feed.zaps, ...awards].sort((a, b) => b.created_at - a.created_at); - }, [feed.messages, feed.zaps, awards]); - const naddr = useMemo(() => { - if (ev) { - return encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey); - } - }, [ev]); + return [...feed.messages, ...feed.reactions, ...awards].sort((a, b) => b.created_at - a.created_at); + }, [feed.messages, feed.reactions, awards]); + const filteredEvents = useMemo(() => { return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey)); }, [events, mutedPubkeys, hostMutedPubkeys]); @@ -104,17 +99,17 @@ export function LiveChat({ name="link" className="secondary" size={32} - onClick={() => window.open(`/chat/${naddr}?chat=true`, "_blank", "popup,width=400,height=800")} + onClick={() => window.open(`/chat/${link.encode()}?chat=true`, "_blank", "popup,width=400,height=800")} /> )} - {zaps.length > 0 && ( + {reactions.zaps.length > 0 && (

- +
{goal && }
@@ -138,7 +133,7 @@ export function LiveChat({ ); } case EventKind.ZapReceipt: { - const zap = zaps.find(b => b.id === a.id && b.receiver === host); + const zap = reactions.zaps.find(b => b.id === a.id && b.receiver === host); if (zap) { return ; } diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index 0a33353..96b0b6c 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -1,25 +1,18 @@ -import { NostrLink, RequestBuilder, EventKind, NoteCollection } from "@snort/system"; -import { SnortContext, useRequestBuilder } from "@snort/system-react"; -import { unixNow, unwrap } from "@snort/shared"; -import { useContext, useEffect, useMemo } from "react"; +import { NostrLink, RequestBuilder, NoteCollection } 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 { findTag } from "utils"; export function useLiveChatFeed(link: NostrLink, eZaps?: Array) { - const system = useContext(SnortContext); const since = useMemo(() => unixNow() - WEEK, [link.id]); const sub = useMemo(() => { const rb = new RequestBuilder(`live:${link.id}:${link.author}`); rb.withOptions({ leaveOpen: true, }); - const aTag = `${link.kind}:${link.author}:${link.id}`; rb.withFilter().kinds([LIVE_STREAM_CHAT]).tag("a", [aTag]).limit(100); - rb.withFilter().kinds([EventKind.ZapReceipt]).tag("a", [aTag]).since(since); - if (eZaps) { - rb.withFilter().kinds([EventKind.ZapReceipt]).tag("e", eZaps); - } return rb; }, [link.id, since, eZaps]); @@ -28,33 +21,7 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array) { const messages = useMemo(() => { return (feed.data ?? []).filter(ev => ev.kind === LIVE_STREAM_CHAT); }, [feed.data]); - const zaps = useMemo(() => { - return (feed.data ?? []).filter(ev => ev.kind === EventKind.ZapReceipt); - }, [feed.data]); - const etags = useMemo(() => { - return messages.map(e => e.id); - }, [messages]); - - const esub = useMemo(() => { - if (etags.length === 0) return null; - const rb = new RequestBuilder(`reactions:${link.id}:${link.author}`); - rb.withOptions({ - leaveOpen: true, - }); - rb.withFilter().kinds([EventKind.Reaction, EventKind.ZapReceipt]).tag("e", etags); - return rb; - }, [etags]); - - useEffect(() => { - const pubkeys = [...new Set(zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))]; - system.ProfileLoader.TrackMetadata(pubkeys); - return () => system.ProfileLoader.UntrackMetadata(pubkeys); - }, [zaps]); - - const reactionsSub = useRequestBuilder(NoteCollection, esub); - - const reactions = reactionsSub.data ?? []; - - return { messages, zaps, reactions }; + const reactions = useReactions(`live:${link.id}:${link.author}:reactions`, messages.map(a => NostrLink.fromEvent(a)).concat(link), undefined, true); + return { messages, reactions: reactions.data ?? [] }; } diff --git a/src/hooks/profile.ts b/src/hooks/profile.ts index 2fce292..2d6dfaf 100644 --- a/src/hooks/profile.ts +++ b/src/hooks/profile.ts @@ -1,11 +1,10 @@ -import { useContext, useMemo } from "react"; -import { RequestBuilder, NoteCollection, NostrLink, EventKind, parseZap } from "@snort/system"; -import { SnortContext, useRequestBuilder } from "@snort/system-react"; +import { useMemo } from "react"; +import { RequestBuilder, NoteCollection, NostrLink } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; import { LIVE_STREAM } from "const"; -import { findTag } from "utils"; +import { useZaps } from "./zaps"; export function useProfile(link: NostrLink, leaveOpen = false) { - const system = useContext(SnortContext); const sub = useMemo(() => { const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`); b.withOptions({ @@ -20,35 +19,11 @@ export function useProfile(link: NostrLink, leaveOpen = false) { return b; }, [link, leaveOpen]); - const { data: streamsData } = useRequestBuilder(NoteCollection, sub); - const streams = streamsData ?? []; - - const addresses = useMemo(() => { - if (streamsData) { - return streamsData.map(e => `${e.kind}:${e.pubkey}:${findTag(e, "d")}`); - } - return []; - }, [streamsData]); - - const zapsSub = useMemo(() => { - const b = new RequestBuilder(`profile-zaps:${link.id.slice(0, 12)}`); - b.withOptions({ - leaveOpen, - }) - .withFilter() - .kinds([EventKind.ZapReceipt]) - .tag("a", addresses); - return b; - }, [link, addresses, leaveOpen]); - - const { data: zapsData } = useRequestBuilder(NoteCollection, zapsSub); - const zaps = (zapsData ?? []) - .map(ev => parseZap(ev, system.ProfileLoader.Cache)) - .filter(z => z && z.valid && z.receiver === link.id); + const streams = useRequestBuilder(NoteCollection, sub); + const zaps = useZaps(link); const sortedStreams = useMemo(() => { - const sorted = [...streams]; - sorted.sort((a, b) => b.created_at - a.created_at); + const sorted = [...(streams.data ?? [])].sort((a, b) => b.created_at - a.created_at); return sorted; }, [streams]); diff --git a/src/hooks/zaps.ts b/src/hooks/zaps.ts index 1e9c5f0..79f6d43 100644 --- a/src/hooks/zaps.ts +++ b/src/hooks/zaps.ts @@ -1,22 +1,13 @@ -import { useContext, useMemo, useEffect } from "react"; -import { unwrap } from "@snort/shared"; -import { NostrLink, RequestBuilder, NostrPrefix, EventKind, NoteCollection, parseZap } from "@snort/system"; -import { SnortContext, useRequestBuilder } from "@snort/system-react"; -import { findTag } from "utils"; +import { useMemo } from "react"; +import { NostrLink, RequestBuilder, EventKind, NoteCollection, parseZap } from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; export function useZaps(link?: NostrLink, leaveOpen = false) { - const system = useContext(SnortContext); const sub = useMemo(() => { if (link) { const b = new RequestBuilder(`zaps:${link.id}`); b.withOptions({ leaveOpen }); - if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) { - b.withFilter().kinds([EventKind.ZapReceipt]).tag("e", [link.id]); - } else if (link.type === NostrPrefix.Address) { - b.withFilter() - .kinds([EventKind.ZapReceipt]) - .tag("a", [`${link.kind}:${link.author}:${link.id}`]); - } + b.withFilter().kinds([EventKind.ZapReceipt]).replyToLink([link]); return b; } return null; @@ -24,16 +15,10 @@ export function useZaps(link?: NostrLink, leaveOpen = false) { const { data: zaps } = useRequestBuilder(NoteCollection, sub); - useEffect(() => { - const pubkeys = zaps ? [...new Set(zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))] : []; - system.ProfileLoader.TrackMetadata(pubkeys); - return () => system.ProfileLoader.UntrackMetadata(pubkeys); - }, [zaps]); - return ( [...(zaps ?? [])] .sort((a, b) => (b.created_at > a.created_at ? 1 : -1)) - .map(ev => parseZap(ev, system.ProfileLoader.Cache)) + .map(ev => parseZap(ev)) .filter(z => z && z.valid) ?? [] ); } diff --git a/src/pages/chat-popout.tsx b/src/pages/chat-popout.tsx index 1b3b547..ec327cd 100644 --- a/src/pages/chat-popout.tsx +++ b/src/pages/chat-popout.tsx @@ -5,11 +5,13 @@ import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system"; import { unwrap } from "@snort/shared"; import { useCurrentStreamFeed } from "hooks/current-stream-feed"; import { findTag } from "utils"; +import { useZapGoal } from "hooks/goals"; export function ChatPopout() { const params = useParams(); const link = parseNostrLink(unwrap(params.id)); const ev = useCurrentStreamFeed(link, true); + const goal = useZapGoal(findTag(ev, "goal")); const lnk = parseNostrLink(encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev?.kind, ev?.pubkey)); const chat = Boolean(new URL(window.location.href).searchParams.get("chat")); @@ -22,6 +24,7 @@ export function ChatPopout() { canWrite: chat, showHeader: false, }} + goal={goal} /> ); diff --git a/src/pages/widgets.tsx b/src/pages/widgets.tsx index d5751b3..f9e4055 100644 --- a/src/pages/widgets.tsx +++ b/src/pages/widgets.tsx @@ -77,6 +77,7 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) { errors: [], sender: login?.pubkey, amount: 1_000_000, + targetEvents: [] }} />
diff --git a/yarn.lock b/yarn.lock index caad121..dbfb57d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2492,14 +2492,14 @@ __metadata: languageName: node linkType: hard -"@snort/system-react@npm:^1.1.1": - version: 1.1.1 - resolution: "@snort/system-react@npm:1.1.1" +"@snort/system-react@npm:^1.1.4": + version: 1.1.4 + resolution: "@snort/system-react@npm:1.1.4" dependencies: "@snort/shared": ^1.0.9 - "@snort/system": ^1.1.1 + "@snort/system": ^1.1.4 react: ^18.2.0 - checksum: fab77abfc738a5e415c2369aa509200b484faa09ff9dbf328872d0c527e315811a1adfcd991956837a3699e23a61d379b2eb536c9cf8dd45d013f8122c4824d5 + checksum: 22a9a4f5bdc0ad1428d0c051a5d13c7168591ee0d1e30921acc5a6cc3aee98172f76439b777b9cfb03d689098ee052ed0276c77bbcd79af0944750dd90975de0 languageName: node linkType: hard @@ -2532,6 +2532,24 @@ __metadata: languageName: node linkType: hard +"@snort/system@npm:^1.1.4": + version: 1.1.4 + resolution: "@snort/system@npm:1.1.4" + dependencies: + "@noble/curves": ^1.2.0 + "@noble/hashes": ^1.3.2 + "@scure/base": ^1.1.2 + "@snort/shared": ^1.0.9 + "@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: f0c7d920e7ea6aa1d8ca3b24a69841dbb33dec0e560d5b2f665c4ab93ccf1604bb01ec9733422daf3d275706e1c5b518fec80d2440a3c9136ba8d4e91d42ba2b + languageName: node + linkType: hard + "@stablelib/binary@npm:^1.0.1": version: 1.0.1 resolution: "@stablelib/binary@npm:1.0.1" @@ -9482,8 +9500,8 @@ __metadata: "@react-hook/resize-observer": ^1.2.6 "@scure/base": ^1.1.1 "@snort/shared": ^1.0.9 - "@snort/system": ^1.1.1 - "@snort/system-react": ^1.1.1 + "@snort/system": ^1.1.4 + "@snort/system-react": ^1.1.4 "@snort/system-web": ^1.0.2 "@szhsin/react-menu": ^4.0.2 "@testing-library/dom": ^9.3.1