feat: message zaps

This commit is contained in:
Alejandro Gomez 2023-07-02 19:53:13 +02:00
parent 3224b03a98
commit a48991c13e
No known key found for this signature in database
GPG Key ID: 4DF39E566658C817
6 changed files with 204 additions and 28 deletions

View File

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

View File

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

View File

@ -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 (
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
{(options?.showHeader ?? true) && (
@ -85,20 +100,26 @@ export function LiveChat({
</div>
)}
<div className="messages">
{[...(messages.data ?? [])]
.sort((a, b) => b.created_at - a.created_at)
.map((a) => {
switch (a.kind) {
case 1311: {
return <ChatMessage ev={a} link={link} key={a.id} />;
}
case EventKind.ZapReceipt: {
return <ChatZap ev={a} key={a.id} />;
}
{events.map((a) => {
switch (a.kind) {
case LIVE_STREAM_CHAT: {
return (
<ChatMessage
streamer={link.author ?? ""}
ev={a}
link={link}
key={a.id}
reactions={reactions}
/>
);
}
return null;
})}
{messages.data === undefined && <Spinner />}
case EventKind.ZapReceipt: {
return <ChatZap streamer={link.author ?? ""} ev={a} key={a.id} />;
}
}
return null;
})}
{feed.messages.length === 0 && <Spinner />}
</div>
{(options?.canWrite ?? true) && (
<div className="write-message">
@ -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 (
<div className={`message${link.author === ev.pubkey ? " streamer" : ""}`}>
<Profile pubkey={ev.pubkey} />
<Text content={ev.content} tags={ev.tags} />
</div>
<>
<div
className={`message${link.author === ev.pubkey ? " streamer" : ""}`}
ref={ref}
>
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
aTag={
streamer === ev.pubkey
? `${link.kind}:${link.author}:${link.id}`
: undefined
}
eTag={ev.id}
pubkey={ev.pubkey}
button={
isHovering ? (
<div className="message-zap-container">
<button className="message-zap-button">
<Icon
name="zap-filled"
className="message-zap-button-icon"
/>
</button>
</div>
) : (
<></>
)
}
targetName={profile?.name || ev.pubkey}
/>
)}
<Profile pubkey={ev.pubkey} />
<Text content={ev.content} tags={ev.tags} />
{totalZaps !== 0 && (
<div className="zap-pill">
<Icon name="zap-filled" className="zap-pill-icon" />
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
</div>
)}
</div>
</>
);
}
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 ? (
<div className="zap-container">
<div className="zap">
<Icon name="zap-filled" className="zap-icon" />
@ -158,7 +236,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
</div>
{parsed.content && <div className="zap-content">{parsed.content}</div>}
</div>
);
) : 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();

View File

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

View File

@ -22,5 +22,38 @@ export function useLiveChatFeed(link: NostrLink) {
return rb;
}, [link]);
return useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
const feed = useRequestBuilder<FlatNoteStore>(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<FlatNoteStore>(
System,
FlatNoteStore,
esub
);
const reactions = relatedZaps.data ?? [];
return { messages, zaps, reactions };
}

View File

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