diff --git a/packages/app/config/default.json b/packages/app/config/default.json index c8c105b6..706423ab 100644 --- a/packages/app/config/default.json +++ b/packages/app/config/default.json @@ -33,6 +33,7 @@ "noteCreatorToast": false, "hideFromNavbar": ["/graph"], "deckSubKind": 1, + "showPowIcon": true, "eventLinkPrefix": "nevent", "profileLinkPrefix": "nprofile", "defaultRelays": { diff --git a/packages/app/config/iris.json b/packages/app/config/iris.json index 571cc3dd..98f2c863 100644 --- a/packages/app/config/iris.json +++ b/packages/app/config/iris.json @@ -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 }, diff --git a/packages/app/custom.d.ts b/packages/app/custom.d.ts index 786701ae..a62b5af9 100644 --- a/packages/app/custom.d.ts +++ b/packages/app/custom.d.ts @@ -89,6 +89,7 @@ declare const CONFIG: { eventLinkPrefix: NostrPrefix; profileLinkPrefix: NostrPrefix; defaultRelays: Record; + showPowIcon: boolean; // Alby wallet oAuth config alby?: { diff --git a/packages/app/src/Components/Event/Note/Note.tsx b/packages/app/src/Components/Event/Note/Note.tsx index 4f749819..f28fa09a 100644 --- a/packages/app/src/Components/Event/Note/Note.tsx +++ b/packages/app/src/Components/Event/Note/Note.tsx @@ -57,7 +57,7 @@ export function Note(props: NoteProps) { {ev.kind === EventKind.Polls && } {optionsMerged.showFooter && (
- +
)} diff --git a/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx b/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx index 65dc3b29..ee76f62e 100644 --- a/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx +++ b/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx @@ -1,5 +1,4 @@ import classNames from "classnames"; -import React from "react"; import { AsyncIcon, AsyncIconProps } from "@/Components/Button/AsyncIcon"; import { formatShort } from "@/Utils/Number"; diff --git a/packages/app/src/Components/Event/Note/NoteFooter/LikeButton.tsx b/packages/app/src/Components/Event/Note/NoteFooter/LikeButton.tsx new file mode 100644 index 00000000..5bf65324 --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/LikeButton.tsx @@ -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 ( + react("+")} + /> + ); +}; diff --git a/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx b/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx index 61ed7eca..daf8f4c2 100644 --- a/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx +++ b/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx @@ -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 ( - - ); - } - } - - function repostIcon() { - if (readonly) return; - return ( - - } - menuClassName="ctx-menu" - align="start"> -
- {/* This menu item serves as a "close menu" button; - it allows the user to click anywhere nearby the menu to close it. */} - -
- -
- repost()} disabled={hasReposted()}> - - - - - note.update(n => { - n.reset(); - n.quote = ev; - n.show = true; - }) - }> - - - -
- ); - } - - function reactionIcon() { - if (!prefs.enableReactions) { - return null; - } - const reacted = hasReacted("+"); - return ( - { - if (readonly) return; - await react(prefs.reactionEmoji); - }} - /> - ); - } - - function replyIcon() { - if (readonly) return; - return ( - 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 (
- {replyIcon()} - {repostIcon()} - {reactionIcon()} - {powIcon()} + + {!readonly && } + {prefs.enableReactions && } + {CONFIG.showPowIcon && }
); diff --git a/packages/app/src/Components/Event/Note/NoteFooter/PowIcon.tsx b/packages/app/src/Components/Event/Note/NoteFooter/PowIcon.tsx new file mode 100644 index 00000000..247c7417 --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/PowIcon.tsx @@ -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 ( + + ); +}; diff --git a/packages/app/src/Components/Event/Note/NoteFooter/ReplyButton.tsx b/packages/app/src/Components/Event/Note/NoteFooter/ReplyButton.tsx new file mode 100644 index 00000000..74285202 --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/ReplyButton.tsx @@ -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 ( + + ); +}; diff --git a/packages/app/src/Components/Event/Note/NoteFooter/RepostButton.tsx b/packages/app/src/Components/Event/Note/NoteFooter/RepostButton.tsx new file mode 100644 index 00000000..b9f58987 --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/RepostButton.tsx @@ -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 ( + + } + menuClassName="ctx-menu" + align="start"> +
+ +
+ +
+ + + + + + note.update(n => { + n.reset(); + n.quote = ev; + n.show = true; + }) + }> + + + +
+ ); +};