snort/src/Element/NoteFooter.tsx

286 lines
8.6 KiB
TypeScript
Raw Normal View History

2023-01-08 00:29:59 +00:00
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
2023-01-31 11:52:55 +00:00
import { faTrash, faRepeat, faShareNodes, faCopy, faCommentSlash, faBan, faLanguage } from "@fortawesome/free-solid-svg-icons";
2023-01-08 00:29:59 +00:00
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2023-01-20 22:59:26 +00:00
import { Menu, MenuItem } from '@szhsin/react-menu';
2023-01-08 00:29:59 +00:00
2023-01-25 18:08:53 +00:00
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";
2023-01-20 11:11:50 +00:00
import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
2023-01-20 22:59:26 +00:00
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
2023-01-20 11:11:50 +00:00
import { NoteCreator } from "Element/NoteCreator";
2023-02-07 13:32:32 +00:00
import SendSats from "Element/SendSats";
2023-02-03 21:38:14 +00:00
import { parseZap, ZapsSummary } from "Element/Zap";
import { useUserProfile } from "Feed/ProfileFeed";
2023-01-20 11:11:50 +00:00
import { default as NEvent } from "Nostr/Event";
import { RootState } from "State/Store";
import { HexKey, TaggedRawEvent } from "Nostr";
import EventKind from "Nostr/EventKind";
2023-01-20 17:07:14 +00:00
import { UserPreferences } from "State/Login";
2023-01-26 11:34:18 +00:00
import useModeration from "Hooks/useModeration";
2023-01-31 11:52:55 +00:00
import { TranslateHost } from "Const";
export interface Translation {
text: string,
fromLanguage: string,
confidence: number
}
2023-01-08 00:29:59 +00:00
2023-01-16 17:48:25 +00:00
export interface NoteFooterProps {
2023-01-20 17:07:14 +00:00
related: TaggedRawEvent[],
2023-01-31 11:52:55 +00:00
ev: NEvent,
onTranslated?: (content: Translation) => void
2023-01-16 17:48:25 +00:00
}
export default function NoteFooter(props: NoteFooterProps) {
2023-01-20 17:07:14 +00:00
const { related, ev } = props;
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const { mute, block } = useModeration();
2023-01-20 17:07:14 +00:00
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const author = useUserProfile(ev.RootPubKey);
2023-01-20 17:07:14 +00:00
const publisher = useEventPublisher();
const [reply, setReply] = useState(false);
const [tip, setTip] = useState(false);
const isMine = ev.RootPubKey === login;
2023-01-31 11:52:55 +00:00
const lang = window.navigator.language;
const langNames = new Intl.DisplayNames([...window.navigator.languages], { type: "language" });
2023-01-25 13:54:45 +00:00
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related, ev]);
const zaps = useMemo(() =>
getReactions(related, ev.Id, EventKind.ZapReceipt).map(parseZap).filter(z => z.valid && z.zapper !== ev.PubKey),
[related]
);
2023-02-03 21:38:14 +00:00
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0)
const didZap = zaps.some(a => a.zapper === login);
2023-01-20 17:07:14 +00:00
const groupReactions = useMemo(() => {
return reactions?.reduce((acc, { content }) => {
let r = normalizeReaction(content);
const amount = acc[r] || 0
return { ...acc, [r]: amount + 1 }
}, {
[Reaction.Positive]: 0,
[Reaction.Negative]: 0
});
}, [reactions]);
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
function hasReacted(emoji: string) {
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login)
}
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
function hasReposted() {
return reposts.some(a => a.pubkey === login);
}
2023-01-08 00:29:59 +00:00
2023-01-20 17:07:14 +00:00
async function react(content: string) {
if (!hasReacted(content)) {
let evLike = await publisher.react(ev, content);
publisher.broadcast(evLike);
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 deleteEvent() {
if (window.confirm(`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`)) {
let evDelete = await publisher.delete(ev.Id);
publisher.broadcast(evDelete);
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-01-20 17:07:14 +00:00
async function repost() {
if (!hasReposted()) {
if (!prefs.confirmReposts || window.confirm(`Are you sure you want to repost: ${ev.Id}`)) {
let evRepost = await publisher.repost(ev);
publisher.broadcast(evRepost);
}
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-01-20 17:07:14 +00:00
function tipButton() {
let service = author?.lud16 || author?.lud06;
if (service) {
return (
2023-01-20 17:07:14 +00:00
<>
2023-02-03 21:38:14 +00:00
<div className={`reaction-pill ${didZap ? 'reacted' : ''}`} onClick={() => setTip(true)}>
<div className="reaction-pill-icon">
2023-01-25 18:08:53 +00:00
<Zap />
</div>
2023-02-03 21:38:14 +00:00
{zapTotal > 0 && (<div className="reaction-pill-number">{formatShort(zapTotal)}</div>)}
</div>
2023-01-20 17:07:14 +00:00
</>
)
}
2023-01-20 17:07:14 +00:00
return null;
}
2023-01-20 17:07:14 +00:00
function repostIcon() {
return (
<div className={`reaction-pill ${hasReposted() ? 'reacted' : ''}`} onClick={() => repost()}>
<div className="reaction-pill-icon">
<FontAwesomeIcon icon={faRepeat} />
</div>
{reposts.length > 0 && (
<div className="reaction-pill-number">
{formatShort(reposts.length)}
</div>
)}
</div>
)
}
2023-01-20 17:07:14 +00:00
function reactionIcons() {
if (!prefs.enableReactions) {
return null;
}
2023-01-08 00:29:59 +00:00
return (
2023-01-20 17:07:14 +00:00
<>
2023-01-20 22:59:26 +00:00
<div className={`reaction-pill ${hasReacted('+') ? 'reacted' : ''} `} onClick={() => react("+")}>
2023-01-20 17:07:14 +00:00
<div className="reaction-pill-icon">
2023-01-25 18:08:53 +00:00
<Heart />
2023-01-20 17:07:14 +00:00
</div>
<div className="reaction-pill-number">
{formatShort(groupReactions[Reaction.Positive])}
</div>
</div>
{repostIcon()}
</>
2023-01-08 00:29:59 +00:00
)
2023-01-20 17:07:14 +00:00
}
2023-01-20 22:59:26 +00:00
async function share() {
2023-01-21 11:59:19 +00:00
const url = `${window.location.protocol}//${window.location.host}/e/${hexToBech32("note", ev.Id)}`;
2023-01-20 22:59:26 +00:00
if ("share" in window.navigator) {
await window.navigator.share({
title: "Snort",
url: url
});
} else {
await navigator.clipboard.writeText(url);
}
}
2023-01-31 11:52:55 +00:00
async function translate() {
const res = await fetch(`${TranslateHost}/translate`, {
method: "POST",
body: JSON.stringify({
q: ev.Content,
source: "auto",
2023-02-06 22:06:47 +00:00
target: lang.split("-")[0],
2023-01-31 11:52:55 +00:00
}),
headers: { "Content-Type": "application/json" }
});
if (res.ok) {
let 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);
}
}
}
2023-01-21 11:59:19 +00:00
async function copyId() {
await navigator.clipboard.writeText(hexToBech32("note", ev.Id));
}
2023-01-21 17:00:09 +00:00
async function copyEvent() {
await navigator.clipboard.writeText(JSON.stringify(ev.Original, undefined, ' '));
}
2023-01-20 22:59:26 +00:00
function menuItems() {
return (
<>
2023-01-25 18:08:53 +00:00
{prefs.enableReactions && (
<MenuItem onClick={() => react("-")}>
<Dislike />
2023-01-21 11:59:19 +00:00
{formatShort(groupReactions[Reaction.Negative])}
2023-01-25 18:08:53 +00:00
&nbsp;
Dislike
</MenuItem>
)}
2023-01-20 22:59:26 +00:00
<MenuItem onClick={() => share()}>
2023-01-21 11:59:19 +00:00
<FontAwesomeIcon icon={faShareNodes} />
2023-01-20 22:59:26 +00:00
Share
</MenuItem>
2023-01-21 11:59:19 +00:00
<MenuItem onClick={() => copyId()}>
<FontAwesomeIcon icon={faCopy} />
Copy ID
</MenuItem>
2023-01-26 11:34:18 +00:00
<MenuItem onClick={() => mute(ev.PubKey)}>
<FontAwesomeIcon icon={faCommentSlash} />
Mute
</MenuItem>
<MenuItem onClick={() => block(ev.PubKey)}>
<FontAwesomeIcon icon={faBan} />
Block
2023-01-26 11:34:18 +00:00
</MenuItem>
2023-01-31 11:52:55 +00:00
<MenuItem onClick={() => translate()}>
<FontAwesomeIcon icon={faLanguage} />
Translate to {langNames.of(lang.split("-")[0])}
</MenuItem>
2023-01-21 17:00:09 +00:00
{prefs.showDebugMenus && (
<MenuItem onClick={() => copyEvent()}>
<FontAwesomeIcon icon={faCopy} />
Copy Event JSON
</MenuItem>
)}
2023-01-20 22:59:26 +00:00
{isMine && (
<MenuItem onClick={() => deleteEvent()}>
2023-01-21 11:59:19 +00:00
<FontAwesomeIcon icon={faTrash} className="red" />
2023-01-20 22:59:26 +00:00
Delete
</MenuItem>
)}
</>
)
}
2023-01-21 11:59:19 +00:00
2023-01-20 17:07:14 +00:00
return (
2023-02-03 21:55:03 +00:00
<>
2023-01-26 06:51:03 +00:00
<div className="footer">
<div className="footer-reactions">
{tipButton()}
{reactionIcons()}
2023-01-25 18:08:53 +00:00
<div className={`reaction-pill ${reply ? 'reacted' : ''}`} onClick={(e) => setReply(s => !s)}>
<div className="reaction-pill-icon">
<Reply />
</div>
</div>
2023-01-26 06:51:03 +00:00
<Menu menuButton={<div className="reaction-pill">
2023-01-31 11:52:55 +00:00
<div className="reaction-pill-icon">
<Dots />
</div>
</div>}
2023-01-26 06:51:03 +00:00
menuClassName="ctx-menu"
>
{menuItems()}
</Menu>
2023-01-20 17:07:14 +00:00
</div>
<NoteCreator
autoFocus={true}
replyTo={ev}
onSend={() => setReply(false)}
show={reply}
2023-01-25 18:08:53 +00:00
setShow={setReply}
2023-01-20 17:07:14 +00:00
/>
2023-02-07 13:32:32 +00:00
<SendSats
svc={author?.lud16 || author?.lud06}
onClose={() => setTip(false)}
show={tip}
author={author?.pubkey}
target={author?.display_name || author?.name}
note={ev.Id}
/>
2023-01-26 06:51:03 +00:00
</div>
2023-02-03 21:55:03 +00:00
<div className="zaps-container">
<ZapsSummary zaps={zaps} />
</div>
</>
2023-01-20 17:07:14 +00:00
)
}