split notefooter into smaller components, CONFIG.showPowIcon
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
edca8a9636
commit
649bab228b
@ -33,6 +33,7 @@
|
|||||||
"noteCreatorToast": false,
|
"noteCreatorToast": false,
|
||||||
"hideFromNavbar": ["/graph"],
|
"hideFromNavbar": ["/graph"],
|
||||||
"deckSubKind": 1,
|
"deckSubKind": 1,
|
||||||
|
"showPowIcon": true,
|
||||||
"eventLinkPrefix": "nevent",
|
"eventLinkPrefix": "nevent",
|
||||||
"profileLinkPrefix": "nprofile",
|
"profileLinkPrefix": "nprofile",
|
||||||
"defaultRelays": {
|
"defaultRelays": {
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"hideFromNavbar": [],
|
"hideFromNavbar": [],
|
||||||
"eventLinkPrefix": "note",
|
"eventLinkPrefix": "note",
|
||||||
"profileLinkPrefix": "npub",
|
"profileLinkPrefix": "npub",
|
||||||
|
"showPowIcon": false,
|
||||||
"defaultRelays": {
|
"defaultRelays": {
|
||||||
"ws://localhost:7777": { "read": true, "write": true },
|
"ws://localhost:7777": { "read": true, "write": true },
|
||||||
"wss://relay.snort.social/": { "read": true, "write": true },
|
"wss://relay.snort.social/": { "read": true, "write": true },
|
||||||
|
1
packages/app/custom.d.ts
vendored
1
packages/app/custom.d.ts
vendored
@ -89,6 +89,7 @@ declare const CONFIG: {
|
|||||||
eventLinkPrefix: NostrPrefix;
|
eventLinkPrefix: NostrPrefix;
|
||||||
profileLinkPrefix: NostrPrefix;
|
profileLinkPrefix: NostrPrefix;
|
||||||
defaultRelays: Record<string, RelaySettings>;
|
defaultRelays: Record<string, RelaySettings>;
|
||||||
|
showPowIcon: boolean;
|
||||||
|
|
||||||
// Alby wallet oAuth config
|
// Alby wallet oAuth config
|
||||||
alby?: {
|
alby?: {
|
||||||
|
@ -57,7 +57,7 @@ export function Note(props: NoteProps) {
|
|||||||
{ev.kind === EventKind.Polls && <Poll ev={ev} />}
|
{ev.kind === EventKind.Polls && <Poll ev={ev} />}
|
||||||
{optionsMerged.showFooter && (
|
{optionsMerged.showFooter && (
|
||||||
<div className="mt-4">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { AsyncIcon, AsyncIconProps } from "@/Components/Button/AsyncIcon";
|
import { AsyncIcon, AsyncIconProps } from "@/Components/Button/AsyncIcon";
|
||||||
import { formatShort } from "@/Utils/Number";
|
import { formatShort } from "@/Utils/Number";
|
||||||
|
@ -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("+")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,24 +1,16 @@
|
|||||||
import { normalizeReaction } from "@snort/shared";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { countLeadingZeros, NostrLink, TaggedNostrEvent } from "@snort/system";
|
|
||||||
import { useEventReactions, useReactions } from "@snort/system-react";
|
import { useEventReactions, useReactions } from "@snort/system-react";
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { useMemo } from "react";
|
||||||
import classNames from "classnames";
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
|
|
||||||
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
|
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import { LikeButton } from "@/Components/Event/Note/NoteFooter/LikeButton";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import { PowIcon } from "@/Components/Event/Note/NoteFooter/PowIcon";
|
||||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
||||||
|
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { useNoteCreator } from "@/State/NoteCreator";
|
|
||||||
import { findTag } from "@/Utils";
|
|
||||||
|
|
||||||
import messages from "../../../messages";
|
|
||||||
|
|
||||||
export interface NoteFooterProps {
|
export interface NoteFooterProps {
|
||||||
replies?: number;
|
replyCount?: number;
|
||||||
ev: TaggedNostrEvent;
|
ev: TaggedNostrEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,156 +23,18 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
||||||
const { positive } = reactions;
|
const { positive } = reactions;
|
||||||
|
|
||||||
const { formatMessage } = useIntl();
|
const { preferences: prefs, readonly } = useLogin(s => ({
|
||||||
const {
|
preferences: s.appData.item.preferences,
|
||||||
publicKey,
|
publicKey: s.publicKey,
|
||||||
preferences: prefs,
|
readonly: s.readonly,
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
|
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
|
||||||
{replyIcon()}
|
<ReplyButton ev={ev} replyCount={props.replyCount} readonly={readonly} />
|
||||||
{repostIcon()}
|
{!readonly && <RepostButton ev={ev} reposts={reposts} />}
|
||||||
{reactionIcon()}
|
{prefs.enableReactions && <LikeButton ev={ev} positiveReactions={positive} />}
|
||||||
{powIcon()}
|
{CONFIG.showPowIcon && <PowIcon ev={ev} />}
|
||||||
<FooterZapButton ev={ev} zaps={zaps} />
|
<FooterZapButton ev={ev} zaps={zaps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user