2023-01-08 00:29:59 +00:00
|
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { useSelector } from "react-redux";
|
2023-02-07 20:04:50 +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-02-07 20:04:50 +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";
|
2023-01-27 21:38:41 +00:00
|
|
|
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 {
|
2023-02-07 20:04:50 +00:00
|
|
|
text: string;
|
|
|
|
fromLanguage: string;
|
|
|
|
confidence: number;
|
2023-01-31 11:52:55 +00:00
|
|
|
}
|
2023-01-08 00:29:59 +00:00
|
|
|
|
2023-01-16 17:48:25 +00:00
|
|
|
export interface NoteFooterProps {
|
2023-02-07 20:04:50 +00:00
|
|
|
related: TaggedRawEvent[];
|
|
|
|
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-02-07 20:04:50 +00:00
|
|
|
const login = useSelector<RootState, HexKey | undefined>(
|
|
|
|
(s) => s.login.publicKey
|
|
|
|
);
|
2023-01-27 21:10:14 +00:00
|
|
|
const { mute, block } = useModeration();
|
2023-02-07 20:04:50 +00:00
|
|
|
const prefs = useSelector<RootState, UserPreferences>(
|
|
|
|
(s) => s.login.preferences
|
|
|
|
);
|
2023-01-27 21:38:41 +00:00
|
|
|
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;
|
2023-02-07 20:04:50 +00:00
|
|
|
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
|
|
|
type: "language",
|
|
|
|
});
|
|
|
|
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),
|
2023-02-05 11:04:18 +00:00
|
|
|
[related]
|
|
|
|
);
|
2023-02-07 20:04:50 +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(() => {
|
2023-02-07 20:04:50 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
);
|
2023-01-20 17:07:14 +00:00
|
|
|
}, [reactions]);
|
2023-01-08 00:29:59 +00:00
|
|
|
|
2023-01-20 17:07:14 +00:00
|
|
|
function hasReacted(emoji: string) {
|
2023-02-07 20:04:50 +00:00
|
|
|
return reactions?.some(
|
|
|
|
({ pubkey, content }) =>
|
|
|
|
normalizeReaction(content) === emoji && pubkey === login
|
|
|
|
);
|
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-02-07 20:04:50 +00:00
|
|
|
return reposts.some((a) => a.pubkey === login);
|
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) {
|
|
|
|
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() {
|
2023-02-07 20:04:50 +00:00
|
|
|
if (
|
|
|
|
window.confirm(
|
|
|
|
`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`
|
|
|
|
)
|
|
|
|
) {
|
2023-01-20 17:07:14 +00:00
|
|
|
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()) {
|
2023-02-07 20:04:50 +00:00
|
|
|
if (
|
|
|
|
!prefs.confirmReposts ||
|
|
|
|
window.confirm(`Are you sure you want to repost: ${ev.Id}`)
|
|
|
|
) {
|
2023-01-20 17:07:14 +00:00
|
|
|
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) {
|
2023-01-19 17:56:39 +00:00
|
|
|
return (
|
2023-01-20 17:07:14 +00:00
|
|
|
<>
|
2023-02-07 20:04:50 +00:00
|
|
|
<div
|
|
|
|
className={`reaction-pill ${didZap ? "reacted" : ""}`}
|
|
|
|
onClick={() => setTip(true)}
|
|
|
|
>
|
2023-01-19 17:56:39 +00:00
|
|
|
<div className="reaction-pill-icon">
|
2023-01-25 18:08:53 +00:00
|
|
|
<Zap />
|
2023-01-19 17:56:39 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
{zapTotal > 0 && (
|
|
|
|
<div className="reaction-pill-number">
|
|
|
|
{formatShort(zapTotal)}
|
|
|
|
</div>
|
|
|
|
)}
|
2023-01-19 17:56:39 +00:00
|
|
|
</div>
|
2023-01-20 17:07:14 +00:00
|
|
|
</>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-19 17:56:39 +00:00
|
|
|
}
|
2023-01-20 17:07:14 +00:00
|
|
|
return null;
|
|
|
|
}
|
2023-01-19 17:56:39 +00:00
|
|
|
|
2023-01-20 17:07:14 +00:00
|
|
|
function repostIcon() {
|
|
|
|
return (
|
2023-02-07 20:04:50 +00:00
|
|
|
<div
|
|
|
|
className={`reaction-pill ${hasReposted() ? "reacted" : ""}`}
|
|
|
|
onClick={() => repost()}
|
|
|
|
>
|
2023-01-20 17:07:14 +00:00
|
|
|
<div className="reaction-pill-icon">
|
|
|
|
<FontAwesomeIcon icon={faRepeat} />
|
|
|
|
</div>
|
|
|
|
{reposts.length > 0 && (
|
|
|
|
<div className="reaction-pill-number">
|
|
|
|
{formatShort(reposts.length)}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-20 17:07:14 +00:00
|
|
|
}
|
2023-01-19 17:56:39 +00:00
|
|
|
|
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-02-07 20:04:50 +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-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-20 17:07:14 +00:00
|
|
|
}
|
|
|
|
|
2023-01-20 22:59:26 +00:00
|
|
|
async function share() {
|
2023-02-07 20:04:50 +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",
|
2023-02-07 20:04:50 +00:00
|
|
|
url: url,
|
2023-01-20 22:59:26 +00:00
|
|
|
});
|
|
|
|
} 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
|
|
|
}),
|
2023-02-07 20:04:50 +00:00
|
|
|
headers: { "Content-Type": "application/json" },
|
2023-01-31 11:52:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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),
|
2023-02-07 20:04:50 +00:00
|
|
|
confidence: result.detectedLanguage.confidence,
|
2023-01-31 11:52:55 +00:00
|
|
|
} 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() {
|
2023-02-07 20:04:50 +00:00
|
|
|
await navigator.clipboard.writeText(
|
|
|
|
JSON.stringify(ev.Original, undefined, " ")
|
|
|
|
);
|
2023-01-21 17:00:09 +00:00
|
|
|
}
|
|
|
|
|
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-02-07 20:04:50 +00:00
|
|
|
Dislike
|
2023-01-25 18:08:53 +00:00
|
|
|
</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} />
|
2023-01-27 21:10:14 +00:00
|
|
|
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-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-20 22:59:26 +00:00
|
|
|
}
|
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-02-07 20:04:50 +00:00
|
|
|
<div className="footer">
|
|
|
|
<div className="footer-reactions">
|
|
|
|
{tipButton()}
|
|
|
|
{reactionIcons()}
|
|
|
|
<div
|
|
|
|
className={`reaction-pill ${reply ? "reacted" : ""}`}
|
|
|
|
onClick={(e) => setReply((s) => !s)}
|
|
|
|
>
|
|
|
|
<div className="reaction-pill-icon">
|
|
|
|
<Reply />
|
|
|
|
</div>
|
2023-01-25 18:08:53 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
<Menu
|
|
|
|
menuButton={
|
|
|
|
<div className="reaction-pill">
|
|
|
|
<div className="reaction-pill-icon">
|
|
|
|
<Dots />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
menuClassName="ctx-menu"
|
|
|
|
>
|
|
|
|
{menuItems()}
|
|
|
|
</Menu>
|
2023-01-25 18:08:53 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
<NoteCreator
|
|
|
|
autoFocus={true}
|
|
|
|
replyTo={ev}
|
|
|
|
onSend={() => setReply(false)}
|
|
|
|
show={reply}
|
|
|
|
setShow={setReply}
|
|
|
|
/>
|
|
|
|
<SendSats
|
|
|
|
svc={author?.lud16 || author?.lud06}
|
|
|
|
onClose={() => setTip(false)}
|
|
|
|
show={tip}
|
|
|
|
author={author?.pubkey}
|
|
|
|
target={author?.display_name || author?.name}
|
|
|
|
note={ev.Id}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="zaps-container">
|
|
|
|
<ZapsSummary zaps={zaps} />
|
2023-01-20 17:07:14 +00:00
|
|
|
</div>
|
2023-02-03 21:55:03 +00:00
|
|
|
</>
|
2023-02-07 20:04:50 +00:00
|
|
|
);
|
2023-01-15 23:44:50 +00:00
|
|
|
}
|