import { useUserProfile } from "@snort/system-react"; import { NostrEvent, parseZap, EventKind } from "@snort/system"; import React, { useRef, useState, useMemo } from "react"; import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver, } from "usehooks-ts"; import { System } from "../index"; import { formatSats } from "../number"; import { EmojiPicker } from "./emoji-picker"; import { Icon } from "./icon"; import { Emoji } from "./emoji"; import { Profile } from "./profile"; import { Text } from "./text"; import { SendZapsDialog } from "./send-zap"; import { findTag } from "../utils"; import type { EmojiPack } from "../hooks/emoji"; import { useLogin } from "../hooks/login"; interface Emoji { id: string; native?: string; } function emojifyReaction(reaction: string) { if (reaction === "+") { return "💜"; } if (reaction === "-") { return "👎"; } return reaction; } export function ChatMessage({ streamer, ev, reactions, emojiPacks, }: { ev: NostrEvent; streamer: string; reactions: readonly NostrEvent[]; emojiPacks: EmojiPack[]; }) { const ref = useRef(null); const inView = useIntersectionObserver(ref, { freezeOnceVisible: true, }); const emojiRef = useRef(null); const isTablet = useMediaQuery("(max-width: 1020px)"); const isHovering = useHover(ref); const [showZapDialog, setShowZapDialog] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const login = useLogin(); const profile = useUserProfile( System, inView?.isIntersecting ? ev.pubkey : undefined, ); const zapTarget = profile?.lud16 ?? profile?.lud06; const zaps = useMemo(() => { return reactions .filter((a) => a.kind === EventKind.ZapReceipt) .map((a) => parseZap(a, System.ProfileLoader.Cache)) .filter((a) => a && a.valid); }, [reactions]); const emojiReactions = useMemo(() => { const emojified = reactions .filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id) .map((ev) => emojifyReaction(ev.content)); return [...new Set(emojified)]; }, [ev, reactions]); const emojiNames = emojiPacks.map((p) => p.emojis).flat(); const hasReactions = emojiReactions.length > 0; const totalZaps = useMemo(() => { const messageZaps = zaps.filter((z) => z.event === ev.id); return messageZaps.reduce((acc, z) => acc + z.amount, 0); }, [zaps, ev]); const hasZaps = totalZaps > 0; useOnClickOutside(ref, () => { setShowZapDialog(false); }); useOnClickOutside(emojiRef, () => { setShowEmojiPicker(false); }); function getEmojiById(id: string) { return emojiNames.find((e) => e.at(1) === id); } async function onEmojiSelect(emoji: Emoji) { setShowEmojiPicker(false); setShowZapDialog(false); let reply = null; try { const pub = login?.publisher(); if (emoji.native) { reply = await pub?.react(ev, emoji.native || "+1"); } else { const e = getEmojiById(emoji.id); if (e) { reply = await pub?.generic((eb) => { return eb .kind(EventKind.Reaction) .content(`:${emoji.id}:`) .tag(["e", ev.id]) .tag(["p", ev.pubkey]) .tag(["emoji", e.at(1)!, e.at(2)!]); }); } } if (reply) { console.debug(reply); System.BroadcastEvent(reply); } } catch { //ignore } } const topOffset = ref.current?.getBoundingClientRect().top; const leftOffset = ref.current?.getBoundingClientRect().left; function pickEmoji(ev: React.MouseEvent) { ev.stopPropagation(); setShowEmojiPicker(!showEmojiPicker); } return ( <>
setShowZapDialog(true)} > // TODO } pubkey={ev.pubkey} profile={profile} /> {(hasReactions || hasZaps) && (
{hasZaps && (
{formatSats(totalZaps)}
)} {emojiReactions.map((e) => { const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":"); const emojiName = e.replace(/:/g, ""); const emoji = isCustomEmojiReaction && getEmojiById(emojiName); return (
{isCustomEmojiReaction && emoji ? ( ) : ( {e} )}
); })}
)} {ref.current && (
{zapTarget && ( } targetName={profile?.name || ev.pubkey} /> )}
)}
{showEmojiPicker && ( setShowEmojiPicker(false)} ref={emojiRef} /> )} ); }