import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { useIntl, FormattedMessage } from "react-intl"; import { faTrash, faHeart, faRepeat, faShareNodes, faCopy, faCommentSlash, faBan, faLanguage, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Menu, MenuItem } from "@szhsin/react-menu"; import Dislike from "Icons/Dislike"; import Heart from "Icons/Heart"; import Dots from "Icons/Dots"; import Zap from "Icons/Zap"; import Reply from "Icons/Reply"; import { formatShort } from "Number"; import useEventPublisher from "Feed/EventPublisher"; import { getReactions, dedupeByPubkey, hexToBech32, normalizeReaction, Reaction, } from "Util"; import { NoteCreator } from "Element/NoteCreator"; import Reactions from "Element/Reactions"; import SendSats from "Element/SendSats"; import { parseZap, ZapsSummary } from "Element/Zap"; import { useUserProfile } from "Feed/ProfileFeed"; import { default as NEvent } from "Nostr/Event"; import { RootState } from "State/Store"; import { HexKey, TaggedRawEvent } from "Nostr"; import EventKind from "Nostr/EventKind"; import { UserPreferences } from "State/Login"; import useModeration from "Hooks/useModeration"; import { TranslateHost } from "Const"; import messages from "./messages"; export interface Translation { text: string; fromLanguage: string; confidence: number; } export interface NoteFooterProps { related: TaggedRawEvent[]; ev: NEvent; onTranslated?: (content: Translation) => void; } export default function NoteFooter(props: NoteFooterProps) { const { related, ev } = props; const { formatMessage } = useIntl(); const login = useSelector( (s) => s.login.publicKey ); const { mute, block } = useModeration(); const prefs = useSelector( (s) => s.login.preferences ); const author = useUserProfile(ev.RootPubKey); const publisher = useEventPublisher(); const [reply, setReply] = useState(false); const [showReactions, setShowReactions] = useState(false); const [tip, setTip] = useState(false); const isMine = ev.RootPubKey === login; const lang = window.navigator.language; const langNames = new Intl.DisplayNames([...window.navigator.languages], { type: "language", }); const reactions = useMemo( () => getReactions(related, ev.Id, EventKind.Reaction), [related, ev] ); const reposts = useMemo( () => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)), [related, ev] ); const zaps = useMemo(() => { const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt) .map(parseZap) .filter((z) => z.valid && z.zapper !== ev.PubKey); sortedZaps.sort((a, b) => b.amount - a.amount); return sortedZaps; }, [related]); const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); const didZap = zaps.some((a) => a.zapper === login); const groupReactions = useMemo(() => { const result = reactions?.reduce( (acc, reaction) => { const kind = normalizeReaction(reaction.content); const rs = acc[kind] || []; if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) { return acc; } return { ...acc, [kind]: [...rs, reaction] }; }, { [Reaction.Positive]: [] as TaggedRawEvent[], [Reaction.Negative]: [] as TaggedRawEvent[], } ); return { [Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]), [Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]), }; }, [reactions]); const positive = groupReactions[Reaction.Positive]; const negative = groupReactions[Reaction.Negative]; function hasReacted(emoji: string) { return reactions?.some( ({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login ); } function hasReposted() { return reposts.some((a) => a.pubkey === login); } async function react(content: string) { if (!hasReacted(content)) { const evLike = await publisher.react(ev, content); publisher.broadcast(evLike); } } async function deleteEvent() { if ( window.confirm( formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) }) ) ) { const evDelete = await publisher.delete(ev.Id); publisher.broadcast(evDelete); } } async function repost() { if (!hasReposted()) { if ( !prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id })) ) { const evRepost = await publisher.repost(ev); publisher.broadcast(evRepost); } } } function tipButton() { const service = author?.lud16 || author?.lud06; if (service) { return ( <>
setTip(true)} >
{zapTotal > 0 && (
{formatShort(zapTotal)}
)}
); } return null; } function repostIcon() { return (
repost()} >
{reposts.length > 0 && (
{formatShort(reposts.length)}
)}
); } function reactionIcons() { if (!prefs.enableReactions) { return null; } return ( <>
react("+")} >
{formatShort(positive.length)}
{repostIcon()} ); } async function share() { const url = `${window.location.protocol}//${ window.location.host }/e/${hexToBech32("note", ev.Id)}`; if ("share" in window.navigator) { await window.navigator.share({ title: "Snort", url: url, }); } else { await navigator.clipboard.writeText(url); } } async function translate() { const res = await fetch(`${TranslateHost}/translate`, { method: "POST", body: JSON.stringify({ q: ev.Content, source: "auto", target: lang.split("-")[0], }), headers: { "Content-Type": "application/json" }, }); if (res.ok) { const result = await res.json(); if (typeof props.onTranslated === "function" && result) { props.onTranslated({ text: result.translatedText, fromLanguage: langNames.of(result.detectedLanguage.language), confidence: result.detectedLanguage.confidence, } as Translation); } } } async function copyId() { await navigator.clipboard.writeText(hexToBech32("note", ev.Id)); } async function copyEvent() { await navigator.clipboard.writeText( JSON.stringify(ev.Original, undefined, " ") ); } function menuItems() { return ( <> {prefs.enableReactions && ( setShowReactions(true)}> )} share()}> copyId()}> mute(ev.PubKey)}> {prefs.enableReactions && ( react("-")}> )} block(ev.PubKey)}> translate()}> {prefs.showDebugMenus && ( copyEvent()}> )} {isMine && ( deleteEvent()}> )} ); } return ( <>
{tipButton()} {reactionIcons()}
setReply((s) => !s)} >
} menuClassName="ctx-menu" > {menuItems()}
setReply(false)} show={reply} setShow={setReply} /> setTip(false)} show={tip} author={author?.pubkey} target={author?.display_name || author?.name} note={ev.Id} />
); }