import { SnortContext, useEventReactions, useUserProfile } from "@snort/system-react"; import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system"; import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "react"; import { useHover, useIntersectionObserver, useMediaQuery, useOnClickOutside } from "usehooks-ts"; import { dedupe } from "@snort/shared"; const EmojiPicker = lazy(() => import("./emoji-picker")); import { Icon } from "./icon"; import { Emoji as EmojiComponent } from "./emoji"; import { Profile } from "./profile"; import { Text } from "./text"; import { useMute } from "./mute-button"; import { SendZapsDialog } from "./send-zap"; import { CollapsibleEvent } from "./collapsible"; import { useLogin } from "@/hooks/login"; import { formatSats } from "@/number"; import type { Badge, Emoji, EmojiPack } from "@/types"; function emojifyReaction(reaction: string) { if (reaction === "+") { return "💜"; } if (reaction === "-") { return "👎"; } return reaction; } export function ChatMessage({ streamer, ev, related, emojiPacks, badges, }: { ev: TaggedNostrEvent; streamer: string; related: ReadonlyArray; emojiPacks: EmojiPack[]; badges: Badge[]; }) { const system = useContext(SnortContext); const ref = useRef(null); const inView = useIntersectionObserver(ref, { freezeOnceVisible: true, }); const emojiRef = useRef(null); const link = NostrLink.fromEvent(ev); const isTablet = useMediaQuery("(max-width: 1020px)"); const isHovering = useHover(ref); const { mute } = useMute(ev.pubkey); const [showZapDialog, setShowZapDialog] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const login = useLogin(); const profile = useUserProfile(inView?.isIntersecting ? ev.pubkey : undefined); const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey; const zapTarget = profile?.lud16 ?? profile?.lud06; const { zaps, reactions } = useEventReactions(link, related); const emojiNames = emojiPacks.map(p => p.emojis).flat(); const filteredReactions = useMemo(() => { return reactions.all.filter(a => link.isReplyToThis(a)); }, [ev, reactions.all]); const hasReactions = filteredReactions.length > 0; const totalZaps = useMemo(() => { return zaps.filter(a => a.event?.id === ev.id).reduce((acc, z) => acc + z.amount, 0); }, [zaps, ev]); const hasZaps = totalZaps > 0; const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey)); 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 if (emoji.id) { 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[1], e[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(e: React.MouseEvent) { e.stopPropagation(); setShowEmojiPicker(!showEmojiPicker); } function muteUser(e: React.MouseEvent) { e.stopPropagation(); mute(); } return ( <>
) : ( awardedBadges.map(badge => { return ( {badge.name} ); }) ) } pubkey={ev.pubkey} profile={profile} /> {(hasReactions || hasZaps) && (
{hasZaps && (
{formatSats(totalZaps)}
)} {dedupe(filteredReactions.map(v => emojifyReaction(v.content))).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} /> )} {shouldShowMuteButton && ( )}
)}
{showEmojiPicker && ( setShowEmojiPicker(false)} ref={emojiRef} /> )} ); }