feat: message zaps
This commit is contained in:
parent
3224b03a98
commit
a48991c13e
@ -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"
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
{events.map((a) => {
|
||||
switch (a.kind) {
|
||||
case 1311: {
|
||||
return <ChatMessage ev={a} link={link} key={a.id} />;
|
||||
case LIVE_STREAM_CHAT: {
|
||||
return (
|
||||
<ChatMessage
|
||||
streamer={link.author ?? ""}
|
||||
ev={a}
|
||||
link={link}
|
||||
key={a.id}
|
||||
reactions={reactions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
return <ChatZap ev={a} key={a.id} />;
|
||||
return <ChatZap streamer={link.author ?? ""} ev={a} key={a.id} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{messages.data === undefined && <Spinner />}
|
||||
{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" : ""}`}>
|
||||
<>
|
||||
<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();
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user