split notefooter into smaller components, CONFIG.showPowIcon
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Martti Malmi 2024-01-11 23:04:36 +02:00
parent edca8a9636
commit 649bab228b
10 changed files with 218 additions and 164 deletions

View File

@ -33,6 +33,7 @@
"noteCreatorToast": false,
"hideFromNavbar": ["/graph"],
"deckSubKind": 1,
"showPowIcon": true,
"eventLinkPrefix": "nevent",
"profileLinkPrefix": "nprofile",
"defaultRelays": {

View File

@ -34,6 +34,7 @@
"hideFromNavbar": [],
"eventLinkPrefix": "note",
"profileLinkPrefix": "npub",
"showPowIcon": false,
"defaultRelays": {
"ws://localhost:7777": { "read": true, "write": true },
"wss://relay.snort.social/": { "read": true, "write": true },

View File

@ -89,6 +89,7 @@ declare const CONFIG: {
eventLinkPrefix: NostrPrefix;
profileLinkPrefix: NostrPrefix;
defaultRelays: Record<string, RelaySettings>;
showPowIcon: boolean;
// Alby wallet oAuth config
alby?: {

View File

@ -57,7 +57,7 @@ export function Note(props: NoteProps) {
{ev.kind === EventKind.Polls && <Poll ev={ev} />}
{optionsMerged.showFooter && (
<div className="mt-4">
<NoteFooter ev={ev} replies={props.threadChains?.get(chainKey(ev))?.length} />
<NoteFooter ev={ev} replyCount={props.threadChains?.get(chainKey(ev))?.length} />
</div>
)}
</div>

View File

@ -1,5 +1,4 @@
import classNames from "classnames";
import React from "react";
import { AsyncIcon, AsyncIconProps } from "@/Components/Button/AsyncIcon";
import { formatShort } from "@/Utils/Number";

View File

@ -0,0 +1,52 @@
import { normalizeReaction } from "@snort/shared";
import { TaggedNostrEvent } from "@snort/system";
import classNames from "classnames";
import { useIntl } from "react-intl";
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { useInteractionCache } from "@/Hooks/useInteractionCache";
import useLogin from "@/Hooks/useLogin";
export const LikeButton = ({
ev,
positiveReactions,
}: {
ev: TaggedNostrEvent;
positiveReactions: TaggedNostrEvent[];
}) => {
const { formatMessage } = useIntl();
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
const interactionCache = useInteractionCache(publicKey, ev.id);
const { publisher, system } = useEventPublisher();
const hasReacted = (emoji: string) => {
return (
interactionCache.data.reacted ||
positiveReactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
);
};
const react = async (content: string) => {
if (!hasReacted(content) && publisher) {
const evLike = await publisher.react(ev, content);
system.BroadcastEvent(evLike);
interactionCache.react();
}
};
const reacted = hasReacted("+");
return (
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
reacted ? "reacted text-nostr-red" : "hover:text-nostr-red",
)}
iconName={reacted ? "heart-solid" : "heart"}
title={formatMessage({ defaultMessage: "Like", id: "qtWLmt" })}
value={positiveReactions.length}
onClick={() => react("+")}
/>
);
};

View File

@ -1,24 +1,16 @@
import { normalizeReaction } from "@snort/shared";
import { countLeadingZeros, NostrLink, TaggedNostrEvent } from "@snort/system";
import { 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 { useMemo } from "react";
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 { LikeButton } from "@/Components/Event/Note/NoteFooter/LikeButton";
import { PowIcon } from "@/Components/Event/Note/NoteFooter/PowIcon";
import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
import useLogin from "@/Hooks/useLogin";
import { useNoteCreator } from "@/State/NoteCreator";
import { findTag } from "@/Utils";
import messages from "../../../messages";
export interface NoteFooterProps {
replies?: number;
replyCount?: number;
ev: TaggedNostrEvent;
}
@ -31,156 +23,18 @@ export default function NoteFooter(props: NoteFooterProps) {
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 (
<AsyncFooterIcon
className={"hidden md:flex flex-none min-w-[50px] md:min-w-[80px]"}
title={formatMessage({ defaultMessage: "Proof of Work", id: "grQ+mI" })}
iconName="diamond"
value={pow}
/>
);
}
}
function repostIcon() {
if (readonly) return;
return (
<Menu
menuButton={
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
hasReposted() ? "reacted text-nostr-blue" : "hover:text-nostr-blue",
)}
iconName="repeat"
title={formatMessage({ defaultMessage: "Repost", id: "JeoS4y" })}
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" />
</MenuItem>
<MenuItem
onClick={() =>
note.update(n => {
n.reset();
n.quote = ev;
n.show = true;
})
}>
<Icon name="edit" />
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
</MenuItem>
</Menu>
);
}
function reactionIcon() {
if (!prefs.enableReactions) {
return null;
}
const reacted = hasReacted("+");
return (
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
reacted ? "reacted text-nostr-red" : "hover:text-nostr-red",
)}
iconName={reacted ? "heart-solid" : "heart"}
title={formatMessage({ defaultMessage: "Like", id: "qtWLmt" })}
value={positive.length}
onClick={async () => {
if (readonly) return;
await react(prefs.reactionEmoji);
}}
/>
);
}
function replyIcon() {
if (readonly) return;
return (
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
note.show ? "reacted text-nostr-purple" : "hover:text-nostr-purple",
)}
iconName="reply"
title={formatMessage({ defaultMessage: "Reply", id: "9HU8vw" })}
value={props.replies ?? 0}
onClick={async () => handleReplyButtonClick()}
/>
);
}
const handleReplyButtonClick = () => {
note.update(v => {
if (v.replyTo?.id !== ev.id) {
v.reset();
}
v.show = true;
v.replyTo = ev;
});
};
const { preferences: prefs, readonly } = useLogin(s => ({
preferences: s.appData.item.preferences,
publicKey: s.publicKey,
readonly: s.readonly,
}));
return (
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
{replyIcon()}
{repostIcon()}
{reactionIcon()}
{powIcon()}
<ReplyButton ev={ev} replyCount={props.replyCount} readonly={readonly} />
{!readonly && <RepostButton ev={ev} reposts={reposts} />}
{prefs.enableReactions && <LikeButton ev={ev} positiveReactions={positive} />}
{CONFIG.showPowIcon && <PowIcon ev={ev} />}
<FooterZapButton ev={ev} zaps={zaps} />
</div>
);

View File

@ -0,0 +1,21 @@
import { countLeadingZeros, TaggedNostrEvent } from "@snort/system";
import { useIntl } from "react-intl";
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
import { findTag } from "@/Utils";
export const PowIcon = ({ ev }: { ev: TaggedNostrEvent }) => {
const { formatMessage } = useIntl();
const powValue = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined;
if (!powValue) return null;
return (
<AsyncFooterIcon
className="hidden md:flex flex-none min-w-[50px] md:min-w-[80px]"
title={formatMessage({ defaultMessage: "Proof of Work", id: "grQ+mI" })}
iconName="diamond"
value={powValue}
/>
);
};

View File

@ -0,0 +1,50 @@
import { TaggedNostrEvent } from "@snort/system";
import classNames from "classnames";
import { useIntl } from "react-intl";
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
import { useNoteCreator } from "@/State/NoteCreator";
export const ReplyButton = ({
ev,
replyCount,
readonly,
}: {
ev: TaggedNostrEvent;
replyCount?: number;
readonly: boolean;
}) => {
const { formatMessage } = useIntl();
const note = useNoteCreator(n => ({
show: n.show,
replyTo: n.replyTo,
update: n.update,
quote: n.quote,
}));
const handleReplyButtonClick = () => {
if (readonly) {
return;
}
note.update(v => {
if (v.replyTo?.id !== ev.id) {
v.reset();
}
v.show = true;
v.replyTo = ev;
});
};
return (
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
note.show ? "reacted text-nostr-purple" : "hover:text-nostr-purple",
)}
iconName="reply"
title={formatMessage({ defaultMessage: "Reply", id: "9HU8vw" })}
value={replyCount ?? 0}
onClick={handleReplyButtonClick}
/>
);
};

View File

@ -0,0 +1,75 @@
import { TaggedNostrEvent } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu";
import classNames from "classnames";
import { FormattedMessage, useIntl } from "react-intl";
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
import Icon from "@/Components/Icons/Icon";
import messages from "@/Components/messages";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { useInteractionCache } from "@/Hooks/useInteractionCache";
import useLogin from "@/Hooks/useLogin";
import { useNoteCreator } from "@/State/NoteCreator";
export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: TaggedNostrEvent[] }) => {
const { formatMessage } = useIntl();
const { publisher, system } = useEventPublisher();
const { publicKey, preferences: prefs } = useLogin(s => ({
preferences: s.appData.item.preferences,
publicKey: s.publicKey,
}));
const interactionCache = useInteractionCache(publicKey, ev.id);
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
const hasReposted = () => {
return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey);
};
const repost = async () => {
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();
}
}
};
return (
<Menu
menuButton={
<AsyncFooterIcon
className={classNames(
"flex-none min-w-[50px] md:min-w-[80px]",
hasReposted() ? "reacted text-nostr-blue" : "hover:text-nostr-blue",
)}
iconName="repeat"
title={formatMessage({ defaultMessage: "Repost", id: "JeoS4y" })}
value={reposts.length}
/>
}
menuClassName="ctx-menu"
align="start">
<div className="close-menu-container">
<MenuItem>
<div className="close-menu" />
</MenuItem>
</div>
<MenuItem onClick={repost} disabled={hasReposted()}>
<Icon name="repeat" />
<FormattedMessage defaultMessage="Repost" id="JeoS4y" />
</MenuItem>
<MenuItem
onClick={() =>
note.update(n => {
n.reset();
n.quote = ev;
n.show = true;
})
}>
<Icon name="edit" />
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
</MenuItem>
</Menu>
);
};