diff --git a/src/element/chat-message.tsx b/src/element/chat-message.tsx index f30a162..698d3f5 100644 --- a/src/element/chat-message.tsx +++ b/src/element/chat-message.tsx @@ -1,180 +1,222 @@ import { useUserProfile } from "@snort/system-react"; import { NostrEvent, parseZap, EventPublisher, EventKind } from "@snort/system"; import { useRef, useState, useMemo } from "react"; -import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver } from "usehooks-ts"; +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"; interface Emoji { - id: string; - native?: string; + id: string; + native?: string; } function emojifyReaction(reaction: string) { - if (reaction === "+") { - return "💜"; - } - if (reaction === "-") { - return "👎"; - } - return reaction; + if (reaction === "+") { + return "💜"; + } + if (reaction === "-") { + return "👎"; + } + return reaction; } export function ChatMessage({ - streamer, - ev, - reactions, + streamer, + ev, + reactions, + emojiPacks, }: { - ev: NostrEvent; - streamer: string; - reactions: readonly NostrEvent[]; + 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 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 emojis = 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 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 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 = emojis.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; + 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(ref, () => { + setShowZapDialog(false); + }); - useOnClickOutside(emojiRef, () => { - setShowEmojiPicker(false); - }); + useOnClickOutside(emojiRef, () => { + setShowEmojiPicker(false); + }); - async function onEmojiSelect(emoji: Emoji) { - setShowEmojiPicker(false); - setShowZapDialog(false); - try { - const pub = await EventPublisher.nip7(); - const reply = await pub?.react(ev, emoji.native || "+1"); - if (reply) { - console.debug(reply); - System.BroadcastEvent(reply); - } - } catch (error) { } - } + function getEmojiById(id: string) { + return emojiNames.find((e) => e.at(1) === id); + } - // @ts-expect-error - const topOffset = ref.current?.getBoundingClientRect().top; - // @ts-expect-error - const leftOffset = ref.current?.getBoundingClientRect().left; + async function onEmojiSelect(emoji: Emoji) { + setShowEmojiPicker(false); + setShowZapDialog(false); + let reply = null; + try { + const pub = await EventPublisher.nip7(); + 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 (error) {} + } - function pickEmoji(ev: any) { - ev.stopPropagation(); - setShowEmojiPicker(!showEmojiPicker); - } + // @ts-expect-error + const topOffset = ref.current?.getBoundingClientRect().top; + // @ts-expect-error + const leftOffset = ref.current?.getBoundingClientRect().left; - return ( - <> -
Please login to write messages!
)} diff --git a/src/element/write-message.tsx b/src/element/write-message.tsx index f29d84c..9d3d763 100644 --- a/src/element/write-message.tsx +++ b/src/element/write-message.tsx @@ -1,6 +1,5 @@ import { NostrLink, EventPublisher, EventKind } from "@snort/system"; import { useRef, useState, useMemo, ChangeEvent } from "react"; -import uniqBy from "lodash.uniqby"; import { LIVE_STREAM_CHAT } from "../const"; import useEmoji, { packId } from "../hooks/emoji"; @@ -10,27 +9,23 @@ import AsyncButton from "./async-button"; import { Icon } from "./icon"; import { Textarea } from "./textarea"; import { EmojiPicker } from "./emoji-picker"; +import type { EmojiPack, Emoji } from "../hooks/emoji"; -interface Emoji { - id: string; - native?: string; -} - -export function WriteMessage({ link }: { link: NostrLink }) { +export function WriteMessage({ + link, + emojiPacks, +}: { + link: NostrLink; + emojiPacks: EmojiPack[]; +}) { const ref = useRef(null); const emojiRef = useRef(null); const [chat, setChat] = useState(""); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const login = useLogin(); - const userEmojiPacks = useEmoji(login!.pubkey); - const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat(); - const channelEmojiPacks = useEmoji(link.author!); - const channelEmojis = channelEmojiPacks.map((pack) => pack.emojis).flat(); - const emojis = userEmojis.concat(channelEmojis); + const emojis = emojiPacks.map((pack) => pack.emojis).flat(); const names = emojis.map((t) => t.at(1)); - const allEmojiPacks = useMemo(() => { - return uniqBy(channelEmojiPacks.concat(userEmojiPacks), packId); - }, [userEmojiPacks, channelEmojiPacks]); + // @ts-expect-error const topOffset = ref.current?.getBoundingClientRect().top; // @ts-expect-error @@ -112,7 +107,7 @@ export function WriteMessage({ link }: { link: NostrLink }) {