snort/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx

188 lines
5.8 KiB
TypeScript
Raw Normal View History

import { normalizeReaction } from "@snort/shared";
2024-01-08 09:24:14 +00:00
import { countLeadingZeros, NostrLink, TaggedNostrEvent } from "@snort/system";
import { useEventReactions, useReactions } from "@snort/system-react";
2023-10-13 11:40:39 +00:00
import { Menu, MenuItem } from "@szhsin/react-menu";
2023-10-16 15:54:55 +00:00
import classNames from "classnames";
import React, { useMemo } from "react";
2024-01-04 17:01:18 +00:00
import { FormattedMessage, useIntl } from "react-intl";
2023-01-08 00:29:59 +00:00
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
2024-01-04 17:01:18 +00:00
import Icon from "@/Components/Icons/Icon";
import useEventPublisher from "@/Hooks/useEventPublisher";
2023-11-17 11:52:10 +00:00
import { useInteractionCache } from "@/Hooks/useInteractionCache";
2024-01-04 17:01:18 +00:00
import useLogin from "@/Hooks/useLogin";
2023-11-17 11:52:10 +00:00
import { useNoteCreator } from "@/State/NoteCreator";
import { findTag } from "@/Utils";
2023-01-31 11:52:55 +00:00
import messages from "../../../messages";
2023-03-11 17:03:42 +00:00
2023-01-16 17:48:25 +00:00
export interface NoteFooterProps {
2023-10-13 10:22:58 +00:00
replies?: number;
2023-08-17 18:54:14 +00:00
ev: TaggedNostrEvent;
2023-01-16 17:48:25 +00:00
}
export default function NoteFooter(props: NoteFooterProps) {
2024-01-08 09:01:26 +00:00
const { ev } = props;
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
const ids = useMemo(() => [link], [link]);
2024-01-08 09:01:26 +00:00
const related = useReactions("note:reactions", ids, undefined, false);
const { reactions, zaps, reposts } = useEventReactions(link, related);
2024-01-08 09:01:26 +00:00
const { positive } = reactions;
2023-02-08 21:10:26 +00:00
const { formatMessage } = useIntl();
2023-09-23 21:21:37 +00:00
const {
publicKey,
preferences: prefs,
readonly,
2023-11-13 16:51:29 +00:00
} = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, readonly: s.readonly }));
2023-04-25 11:57:09 +00:00
const interactionCache = useInteractionCache(publicKey, ev.id);
2023-10-13 15:34:31 +00:00
const { publisher, system } = useEventPublisher();
2023-10-13 11:40:39 +00:00
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
function hasReacted(emoji: string) {
2023-04-25 11:57:09 +00:00
return (
interactionCache.data.reacted ||
positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
);
2023-01-20 17:07:14 +00:00
}
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
function hasReposted() {
2023-04-25 11:57:09 +00:00
return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey);
2023-01-20 17:07:14 +00:00
}
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
async function react(content: string) {
2023-04-14 15:02:15 +00:00
if (!hasReacted(content) && publisher) {
2023-02-07 19:47:57 +00:00
const evLike = await publisher.react(ev, content);
2023-10-13 15:34:31 +00:00
system.BroadcastEvent(evLike);
2023-12-18 11:17:06 +00:00
interactionCache.react();
2023-01-08 17:33:54 +00:00
}
2023-01-20 17:07:14 +00:00
}
2023-01-08 17:33:54 +00:00
2023-01-20 17:07:14 +00:00
async function repost() {
2023-04-14 15:02:15 +00:00
if (!hasReposted() && publisher) {
2023-03-28 14:34:01 +00:00
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
2023-02-07 19:47:57 +00:00
const evRepost = await publisher.repost(ev);
2023-10-13 15:34:31 +00:00
system.BroadcastEvent(evRepost);
2023-04-25 11:57:09 +00:00
await interactionCache.repost();
2023-01-20 17:07:14 +00:00
}
2023-01-08 00:29:59 +00:00
}
2023-01-20 17:07:14 +00:00
}
2023-01-08 00:29:59 +00:00
2023-08-18 18:01:34 +00:00
function powIcon() {
const pow = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined;
if (pow) {
return (
2023-11-20 19:16:47 +00:00
<AsyncFooterIcon
2024-01-11 13:37:50 +00:00
className={"hidden md:flex flex-none min-w-[50px] md:min-w-[80px]"}
2023-11-20 19:16:47 +00:00
title={formatMessage({ defaultMessage: "Proof of Work", id: "grQ+mI" })}
iconName="diamond"
value={pow}
/>
2023-08-18 18:01:34 +00:00
);
}
}
2023-01-20 17:07:14 +00:00
function repostIcon() {
2023-09-23 21:21:37 +00:00
if (readonly) return;
2023-01-20 17:07:14 +00:00
return (
2023-10-13 11:40:39 +00:00
<Menu
menuButton={
<AsyncFooterIcon
2024-01-11 13:37:50 +00:00
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
hasReposted() ? "reacted text-nostr-blue" : "hover:text-nostr-blue",
)}
2023-10-13 11:40:39 +00:00
iconName="repeat"
2023-11-20 19:16:47 +00:00
title={formatMessage({ defaultMessage: "Repost", id: "JeoS4y" })}
2023-10-13 11:40:39 +00:00
value={reposts.length}
/>
}
menuClassName="ctx-menu"
align="start">
<div className="close-menu-container">
{/* This menu item serves as a "close menu" button;
it allows the user to click anywhere nearby the menu to close it. */}
<MenuItem>
<div className="close-menu" />
</MenuItem>
</div>
<MenuItem onClick={() => repost()} disabled={hasReposted()}>
<Icon name="repeat" />
<FormattedMessage defaultMessage="Repost" id="JeoS4y" />
2023-10-13 11:40:39 +00:00
</MenuItem>
<MenuItem
onClick={() =>
note.update(n => {
n.reset();
n.quote = ev;
n.show = true;
})
}>
<Icon name="edit" />
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
2023-10-13 11:40:39 +00:00
</MenuItem>
</Menu>
);
2023-01-20 17:07:14 +00:00
}
2023-08-18 18:11:05 +00:00
function reactionIcon() {
2023-04-25 09:15:15 +00:00
if (!prefs.enableReactions) {
2023-01-20 17:07:14 +00:00
return null;
}
2023-07-24 14:30:21 +00:00
const reacted = hasReacted("+");
2023-01-08 00:29:59 +00:00
return (
2023-08-18 18:01:34 +00:00
<AsyncFooterIcon
2024-01-11 13:37:50 +00:00
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
reacted ? "reacted text-nostr-red" : "hover:text-nostr-red",
)}
2023-08-18 18:01:34 +00:00
iconName={reacted ? "heart-solid" : "heart"}
2023-11-20 19:16:47 +00:00
title={formatMessage({ defaultMessage: "Like", id: "qtWLmt" })}
2023-08-18 18:01:34 +00:00
value={positive.length}
2023-09-23 21:21:37 +00:00
onClick={async () => {
if (readonly) return;
await react(prefs.reactionEmoji);
}}
2023-08-18 18:01:34 +00:00
/>
);
2023-01-20 17:07:14 +00:00
}
2023-08-18 18:11:05 +00:00
function replyIcon() {
2023-09-23 21:21:37 +00:00
if (readonly) return;
2023-08-18 18:11:05 +00:00
return (
<AsyncFooterIcon
2024-01-11 13:37:50 +00:00
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
note.show ? "reacted text-nostr-purple" : "hover:text-nostr-purple",
)}
2023-08-18 18:11:05 +00:00
iconName="reply"
2023-11-20 19:16:47 +00:00
title={formatMessage({ defaultMessage: "Reply", id: "9HU8vw" })}
2023-10-13 10:22:58 +00:00
value={props.replies ?? 0}
2023-08-18 18:11:05 +00:00
onClick={async () => handleReplyButtonClick()}
/>
);
}
const handleReplyButtonClick = () => {
2023-09-21 20:02:59 +00:00
note.update(v => {
if (v.replyTo?.id !== ev.id) {
v.reset();
}
v.show = true;
v.replyTo = ev;
});
};
2023-01-20 17:07:14 +00:00
return (
2024-01-11 13:37:50 +00:00
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
{replyIcon()}
{repostIcon()}
{reactionIcon()}
{powIcon()}
<FooterZapButton ev={ev} zaps={zaps} />
</div>
);
}