fix: stream zaps

fixes #93
This commit is contained in:
Kieran 2023-11-14 14:04:07 +00:00
parent 9e3b2c1ee7
commit 148e399775
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
10 changed files with 70 additions and 126 deletions

View File

@ -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",

View File

@ -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 && (
<SendZapsDialog

View File

@ -30,7 +30,7 @@ export function Goal({ ev }: { ev: NostrEvent }) {
}
const soFar = useMemo(() => {
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));

View File

@ -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")}
/>
</div>
)}
{zaps.length > 0 && (
{reactions.zaps.length > 0 && (
<div className="top-zappers">
<h3>
<FormattedMessage defaultMessage="Top zappers" />
</h3>
<div className="top-zappers-container">
<TopZappers zaps={zaps} />
<TopZappers zaps={reactions.zaps} />
</div>
{goal && <Goal ev={goal} />}
</div>
@ -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 <ChatZap zap={zap} key={a.id} />;
}

View File

@ -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<string>) {
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<string>) {
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 ?? [] };
}

View File

@ -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]);

View File

@ -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) ?? []
);
}

View File

@ -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}
/>
</div>
);

View File

@ -77,6 +77,7 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
errors: [],
sender: login?.pubkey,
amount: 1_000_000,
targetEvents: []
}}
/>
<div className="text-to-speech-settings">

View File

@ -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