From a48991c13eb8f46b3c98e69faecfb3adde51e096 Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 2 Jul 2023 19:53:13 +0200 Subject: [PATCH] feat: message zaps --- package.json | 1 + src/element/live-chat.css | 51 +++++++++++++++ src/element/live-chat.tsx | 128 ++++++++++++++++++++++++++++++-------- src/element/send-zap.tsx | 12 +++- src/hooks/live-chat.tsx | 35 ++++++++++- yarn.lock | 5 ++ 6 files changed, 204 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 09e172d..596007d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react-intersection-observer": "^9.5.1", "react-router-dom": "^6.13.0", "semantic-sdp": "^3.26.2", + "usehooks-ts": "^2.9.1", "web-vitals": "^2.1.0", "webrtc-adapter": "^8.2.3" }, diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 681e023..57ab234 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -92,6 +92,7 @@ .live-chat .message { word-wrap: break-word; + position: relative; } .live-chat .message .profile { @@ -217,3 +218,53 @@ .zap-content { margin-top: 8px; } + +.zap-pill { + display: flex; + align-items: center; + margin-top: 4px; + padding: 0 4px; + justify-content: center; + gap: 2px; + border-radius: 8px; + background: #434343; + width: fit-content; +} + +.zap-pill-icon { + width: 12px; + height: 12px; + color: #FF8D2B; +} + +.zap-pill-amount { + color: #FFF; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; + text-transform: lowercase; +} + +.message-zap-button { + cursor: pointer; + position: absolute; + left: 12px; + top: -6px; + background: transparent; + border: none; + display: flex; + background: #434343; + border-radius: 8px; + width: 16px; + height: 16px; + justify-content: center; + align-items: center; +} + +.message-zap-button-icon { + color: #FF8D2B; + width: 12px; + height: 12px; + flex-shrink: 0; +} diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index d1880f7..b7f5765 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -10,9 +10,13 @@ import { import { useState, useEffect, + useMemo, + useRef, type KeyboardEvent, type ChangeEvent, + type LegacyRef, } from "react"; +import { useHover } from "usehooks-ts"; import useEmoji from "hooks/emoji"; import { System } from "index"; @@ -23,10 +27,12 @@ import { Icon } from "./icon"; import { Text } from "./text"; import { Textarea } from "./textarea"; import Spinner from "./spinner"; +import { SendZapsDialog } from "./send-zap"; import { useLogin } from "hooks/login"; import { useUserProfile } from "@snort/system-react"; import { formatSats } from "number"; import useTopZappers from "hooks/top-zappers"; +import { LIVE_STREAM_CHAT } from "const"; export interface LiveChatOptions { canWrite?: boolean; @@ -67,13 +73,22 @@ export function LiveChat({ options?: LiveChatOptions; height?: number; }) { - const messages = useLiveChatFeed(link); + const feed = useLiveChatFeed(link); const login = useLogin(); - const events = messages.data ?? []; - const zaps = events + const zaps = feed.zaps .filter((ev) => ev.kind === EventKind.ZapReceipt) .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) .filter((z) => z && z.valid); + const reactions = feed.reactions + .filter((ev) => ev.kind === EventKind.ZapReceipt) + .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) + .filter((z) => z && z.valid); + const events = useMemo(() => { + return [...feed.messages, ...feed.zaps].sort( + (a, b) => b.created_at - a.created_at + ); + }, [feed.messages, feed.zaps]); + return (
{(options?.showHeader ?? true) && ( @@ -85,20 +100,26 @@ export function LiveChat({
)}
- {[...(messages.data ?? [])] - .sort((a, b) => b.created_at - a.created_at) - .map((a) => { - switch (a.kind) { - case 1311: { - return ; - } - case EventKind.ZapReceipt: { - return ; - } + {events.map((a) => { + switch (a.kind) { + case LIVE_STREAM_CHAT: { + return ( + + ); } - return null; - })} - {messages.data === undefined && } + case EventKind.ZapReceipt: { + return ; + } + } + return null; + })} + {feed.messages.length === 0 && }
{(options?.canWrite ?? true) && (
@@ -113,16 +134,72 @@ export function LiveChat({ ); } -function ChatMessage({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) { +function ChatMessage({ + streamer, + ev, + link, + reactions, +}: { + streamer: string; + ev: TaggedRawEvent; + link: NostrLink; + reactions: ParsedZap[]; +}) { + const ref = useRef(null); + const isHovering = useHover(ref); + const profile = useUserProfile(System, ev.pubkey); + const zapTarget = profile?.lud16 ?? profile?.lud06; + const totalZaps = useMemo(() => { + const messageZaps = reactions.filter((z) => z.event === ev.id); + return messageZaps.reduce((acc, z) => acc + z.amount, 0); + }, [reactions, ev]); return ( -
- - -
+ <> +
+ {zapTarget && ( + + +
+ ) : ( + <> + ) + } + targetName={profile?.name || ev.pubkey} + /> + )} + + + {totalZaps !== 0 && ( +
+ + {formatSats(totalZaps)} +
+ )} +
+ ); } -function ChatZap({ ev }: { ev: TaggedRawEvent }) { +function ChatZap({ streamer, ev }: { streamer: string; ev: TaggedRawEvent }) { const parsed = parseZap(ev, System.ProfileLoader.Cache); useUserProfile(System, parsed.anonZap ? undefined : parsed.sender); @@ -141,7 +218,8 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) { if (!parsed.valid) { return null; } - return ( + + return parsed.receiver === streamer ? (
@@ -158,7 +236,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
{parsed.content &&
{parsed.content}
}
- ); + ) : null; } function WriteMessage({ link }: { link: NostrLink }) { @@ -184,7 +262,7 @@ function WriteMessage({ link }: { link: NostrLink }) { const emoji = [...emojiNames].map((name) => emojis.find((e) => e.at(1) === name) ); - eb.kind(1311 as EventKind) + eb.kind(LIVE_STREAM_CHAT as EventKind) .content(chat) .tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"]) .processContent(); diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index c89fc0e..27aa427 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -13,6 +13,7 @@ interface SendZapsProps { lnurl: string; pubkey?: string; aTag?: string; + eTag?: string; targetName?: string; onFinish: () => void; button?: ReactNode; @@ -22,6 +23,7 @@ function SendZaps({ lnurl, pubkey, aTag, + eTag, targetName, onFinish, }: SendZapsProps) { @@ -57,7 +59,7 @@ function SendZaps({ const amountInSats = isFiat ? Math.floor((amount / UsdRate) * 1e8) : amount; let zap: NostrEvent | undefined; - if (pubkey && aTag) { + if (pubkey) { zap = await pub.zap( amountInSats * 1000, pubkey, @@ -65,7 +67,13 @@ function SendZaps({ undefined, comment, (eb) => { - return eb.tag(["a", aTag]); + if (aTag) { + eb.tag(["a", aTag]); + } + if (eTag) { + eb.tag(["e", eTag]); + } + return eb; } ); } diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index 01a28fd..52a5e5a 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -22,5 +22,38 @@ export function useLiveChatFeed(link: NostrLink) { return rb; }, [link]); - return useRequestBuilder(System, FlatNoteStore, sub); + const feed = useRequestBuilder(System, FlatNoteStore, sub); + + 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(`msg-zaps:${link.id}:${link.author}`); + rb.withOptions({ + leaveOpen: true, + }); + rb.withFilter() + .kinds([EventKind.Reaction, EventKind.ZapReceipt]) + .tag("e", etags); + return rb; + }, [etags]); + + const relatedZaps = useRequestBuilder( + System, + FlatNoteStore, + esub + ); + + const reactions = relatedZaps.data ?? []; + + return { messages, zaps, reactions }; } diff --git a/yarn.lock b/yarn.lock index e2af84b..e36d511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6323,6 +6323,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +usehooks-ts@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.9.1.tgz#953d3284851ffd097432379e271ce046a8180b37" + integrity sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"