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"