import { normalizeReaction } from "@snort/shared"; import { countLeadingZeros, NostrLink, TaggedNostrEvent } from "@snort/system"; import { useEventReactions, useReactions } from "@snort/system-react"; import { Menu, MenuItem } from "@szhsin/react-menu"; import classNames from "classnames"; import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon"; import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton"; import Icon from "@/Components/Icons/Icon"; import useEventPublisher from "@/Hooks/useEventPublisher"; import { useInteractionCache } from "@/Hooks/useInteractionCache"; import useLogin from "@/Hooks/useLogin"; import { useNoteCreator } from "@/State/NoteCreator"; import { findTag } from "@/Utils"; import messages from "../../../messages"; export interface NoteFooterProps { replies?: number; ev: TaggedNostrEvent; } export default function NoteFooter(props: NoteFooterProps) { const { ev } = props; const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]); const ids = useMemo(() => [link], [link]); const related = useReactions("note:reactions", ids, undefined, false); const { reactions, zaps, reposts } = useEventReactions(link, related); const { positive } = reactions; const { formatMessage } = useIntl(); const { publicKey, preferences: prefs, readonly, } = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, readonly: s.readonly })); const interactionCache = useInteractionCache(publicKey, ev.id); const { publisher, system } = useEventPublisher(); const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote })); function hasReacted(emoji: string) { return ( interactionCache.data.reacted || positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey) ); } function hasReposted() { return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey); } async function react(content: string) { if (!hasReacted(content) && publisher) { const evLike = await publisher.react(ev, content); system.BroadcastEvent(evLike); interactionCache.react(); } } async function repost() { if (!hasReposted() && publisher) { if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) { const evRepost = await publisher.repost(ev); system.BroadcastEvent(evRepost); await interactionCache.repost(); } } } function powIcon() { const pow = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined; if (pow) { return ( ); } } function repostIcon() { if (readonly) return; return ( } menuClassName="ctx-menu" align="start">
{/* This menu item serves as a "close menu" button; it allows the user to click anywhere nearby the menu to close it. */}
repost()} disabled={hasReposted()}> note.update(n => { n.reset(); n.quote = ev; n.show = true; }) }>
); } function reactionIcon() { if (!prefs.enableReactions) { return null; } const reacted = hasReacted("+"); return ( { if (readonly) return; await react(prefs.reactionEmoji); }} /> ); } function replyIcon() { if (readonly) return; return ( handleReplyButtonClick()} /> ); } const handleReplyButtonClick = () => { note.update(v => { if (v.replyTo?.id !== ev.id) { v.reset(); } v.show = true; v.replyTo = ev; }); }; return (
{replyIcon()} {repostIcon()} {reactionIcon()} {powIcon()}
); }