diff --git a/packages/app/src/Components/Event/LongFormText.tsx b/packages/app/src/Components/Event/LongFormText.tsx index afc7858a..52bfe23b 100644 --- a/packages/app/src/Components/Event/LongFormText.tsx +++ b/packages/app/src/Components/Event/LongFormText.tsx @@ -12,7 +12,7 @@ import useImgProxy from "@/Hooks/useImgProxy"; import { findTag } from "@/Utils"; import { Markdown } from "./Markdown"; -import NoteFooter from "./Note/NoteFooter"; +import NoteFooter from "./Note/NoteFooter/NoteFooter"; import NoteTime from "./Note/NoteTime"; interface LongFormTextProps { diff --git a/packages/app/src/Components/Event/Note/Note.tsx b/packages/app/src/Components/Event/Note/Note.tsx index 6e30cb6f..4f749819 100644 --- a/packages/app/src/Components/Event/Note/Note.tsx +++ b/packages/app/src/Components/Event/Note/Note.tsx @@ -18,7 +18,7 @@ import { NoteProps } from "../EventComponent"; import HiddenNote from "../HiddenNote"; import Poll from "../Poll"; import { NoteTranslation } from "./NoteContextMenu"; -import NoteFooter from "./NoteFooter"; +import NoteFooter from "./NoteFooter/NoteFooter"; const defaultOptions = { showHeader: true, diff --git a/packages/app/src/Components/Event/Note/AsyncFooterIcon.tsx b/packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx similarity index 100% rename from packages/app/src/Components/Event/Note/AsyncFooterIcon.tsx rename to packages/app/src/Components/Event/Note/NoteFooter/AsyncFooterIcon.tsx diff --git a/packages/app/src/Components/Event/Note/NoteFooter/FooterZapButton.tsx b/packages/app/src/Components/Event/Note/NoteFooter/FooterZapButton.tsx new file mode 100644 index 00000000..f6e698c7 --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/FooterZapButton.tsx @@ -0,0 +1,158 @@ +import { barrierQueue } from "@snort/shared"; +import { NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system"; +import { useUserProfile } from "@snort/system-react"; +import React, { useEffect, useState } from "react"; +import { useIntl } from "react-intl"; +import { useLongPress } from "use-long-press"; + +import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon"; +import { ZapperQueue } from "@/Components/Event/Note/NoteFooter/ZapperQueue"; +import { ZapsSummary } from "@/Components/Event/ZapsSummary"; +import SendSats from "@/Components/SendSats/SendSats"; +import useEventPublisher from "@/Hooks/useEventPublisher"; +import { useInteractionCache } from "@/Hooks/useInteractionCache"; +import useLogin from "@/Hooks/useLogin"; +import { getDisplayName } from "@/Utils"; +import { Zapper, ZapTarget } from "@/Utils/Zapper"; +import { ZapPoolController } from "@/Utils/ZapPoolController"; +import { useWallet } from "@/Wallet"; + +export interface ZapIconProps { + ev: TaggedNostrEvent; + zaps: Array; +} + +export const FooterZapButton = ({ ev, zaps }: ZapIconProps) => { + const { + publicKey, + readonly, + preferences: prefs, + } = useLogin(s => ({ + publicKey: s.publicKey, + readonly: s.readonly, + preferences: s.appData.item.preferences, + })); + const walletState = useWallet(); + const wallet = walletState.wallet; + const interactionCache = useInteractionCache(publicKey, ev.id); + const link = NostrLink.fromEvent(ev); + const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); + const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey); + const [tip, setTip] = useState(false); + const { formatMessage } = useIntl(); + const [zapping, setZapping] = useState(false); + const { publisher, system } = useEventPublisher(); + const author = useUserProfile(ev.pubkey); + const isMine = ev.pubkey === publicKey; + + const longPress = useLongPress(() => setTip(true), { captureEvent: true }); + + const getZapTarget = (): Array | undefined => { + if (ev.tags.some(v => v[0] === "zap")) { + return Zapper.fromEvent(ev); + } + + const authorTarget = author?.lud16 || author?.lud06; + if (authorTarget) { + return [ + { + type: "lnurl", + value: authorTarget, + weight: 1, + name: getDisplayName(author, ev.pubkey), + zap: { + pubkey: ev.pubkey, + event: link, + }, + } as ZapTarget, + ]; + } + }; + + const fastZap = async (e: React.MouseEvent) => { + if (zapping || e?.isPropagationStopped()) return; + + const lnurl = getZapTarget(); + if (canFastZap && lnurl) { + setZapping(true); + try { + await fastZapInner(lnurl, prefs.defaultZapAmount); + } catch (e) { + console.warn("Fast zap failed", e); + if (!(e instanceof Error) || e.message !== "User rejected") { + setTip(true); + } + } finally { + setZapping(false); + } + } else { + setTip(true); + } + }; + + async function fastZapInner(targets: Array, amount: number) { + if (wallet) { + // only allow 1 invoice req/payment at a time to avoid hitting rate limits + await barrierQueue(ZapperQueue, async () => { + const zapper = new Zapper(system, publisher); + const result = await zapper.send(wallet, targets, amount); + const totalSent = result.reduce((acc, v) => (acc += v.sent), 0); + if (totalSent > 0) { + if (CONFIG.features.zapPool) { + ZapPoolController?.allocate(totalSent); + } + await interactionCache.zap(); + } + }); + } + } + + const canFastZap = wallet?.isReady() && !readonly; + + const targets = getZapTarget(); + + useEffect(() => { + if (prefs.autoZap && !didZap && !isMine && !zapping) { + const lnurl = getZapTarget(); + if (wallet?.isReady() && lnurl) { + setZapping(true); + queueMicrotask(async () => { + try { + await fastZapInner(lnurl, prefs.defaultZapAmount); + } catch { + // ignored + } finally { + setZapping(false); + } + }); + } + } + }, [prefs.autoZap, author, zapping]); + + return ( + <> + {targets && ( + <> +
+ + +
+ setTip(false)} + show={tip} + note={ev.id} + allocatePool={true} + /> + + )} + + ); +}; diff --git a/packages/app/src/Components/Event/Note/NoteFooter.tsx b/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx similarity index 55% rename from packages/app/src/Components/Event/Note/NoteFooter.tsx rename to packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx index 8d4a0d5f..61ed7eca 100644 --- a/packages/app/src/Components/Event/Note/NoteFooter.tsx +++ b/packages/app/src/Components/Event/Note/NoteFooter/NoteFooter.tsx @@ -1,29 +1,21 @@ -import { barrierQueue, normalizeReaction, processWorkQueue, WorkQueueItem } from "@snort/shared"; +import { normalizeReaction } from "@snort/shared"; import { countLeadingZeros, NostrLink, TaggedNostrEvent } from "@snort/system"; -import { useEventReactions, useReactions, useUserProfile } from "@snort/system-react"; +import { useEventReactions, useReactions } from "@snort/system-react"; import { Menu, MenuItem } from "@szhsin/react-menu"; import classNames from "classnames"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { useLongPress } from "use-long-press"; -import { AsyncFooterIcon } from "@/Components/Event/Note/AsyncFooterIcon"; -import { ZapsSummary } from "@/Components/Event/ZapsSummary"; +import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon"; +import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton"; import Icon from "@/Components/Icons/Icon"; -import SendSats from "@/Components/SendSats/SendSats"; import useEventPublisher from "@/Hooks/useEventPublisher"; import { useInteractionCache } from "@/Hooks/useInteractionCache"; import useLogin from "@/Hooks/useLogin"; import { useNoteCreator } from "@/State/NoteCreator"; -import { findTag, getDisplayName } from "@/Utils"; -import { Zapper, ZapTarget } from "@/Utils/Zapper"; -import { ZapPoolController } from "@/Utils/ZapPoolController"; -import { useWallet } from "@/Wallet"; +import { findTag } from "@/Utils"; -import messages from "../../messages"; - -const ZapperQueue: Array = []; -processWorkQueue(ZapperQueue); +import messages from "../../../messages"; export interface NoteFooterProps { replies?: number; @@ -45,28 +37,9 @@ export default function NoteFooter(props: NoteFooterProps) { preferences: prefs, readonly, } = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, readonly: s.readonly })); - const author = useUserProfile(ev.pubkey); 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 })); - const [tip, setTip] = useState(false); - const [zapping, setZapping] = useState(false); - const walletState = useWallet(); - const wallet = walletState.wallet; - - const canFastZap = wallet?.isReady() && !readonly; - const isMine = ev.pubkey === publicKey; - const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); - const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey); - const longPress = useLongPress( - e => { - e.stopPropagation(); - setTip(true); - }, - { - captureEvent: true, - }, - ); function hasReacted(emoji: string) { return ( @@ -97,84 +70,6 @@ export default function NoteFooter(props: NoteFooterProps) { } } - function getZapTarget(): Array | undefined { - if (ev.tags.some(v => v[0] === "zap")) { - return Zapper.fromEvent(ev); - } - - const authorTarget = author?.lud16 || author?.lud06; - if (authorTarget) { - return [ - { - type: "lnurl", - value: authorTarget, - weight: 1, - name: getDisplayName(author, ev.pubkey), - zap: { - pubkey: ev.pubkey, - event: link, - }, - } as ZapTarget, - ]; - } - } - - async function fastZap(e?: React.MouseEvent) { - if (zapping || e?.isPropagationStopped()) return; - - const lnurl = getZapTarget(); - if (canFastZap && lnurl) { - setZapping(true); - try { - await fastZapInner(lnurl, prefs.defaultZapAmount); - } catch (e) { - console.warn("Fast zap failed", e); - if (!(e instanceof Error) || e.message !== "User rejected") { - setTip(true); - } - } finally { - setZapping(false); - } - } else { - setTip(true); - } - } - - async function fastZapInner(targets: Array, amount: number) { - if (wallet) { - // only allow 1 invoice req/payment at a time to avoid hitting rate limits - await barrierQueue(ZapperQueue, async () => { - const zapper = new Zapper(system, publisher); - const result = await zapper.send(wallet, targets, amount); - const totalSent = result.reduce((acc, v) => (acc += v.sent), 0); - if (totalSent > 0) { - if (CONFIG.features.zapPool) { - ZapPoolController?.allocate(totalSent); - } - await interactionCache.zap(); - } - }); - } - } - - useEffect(() => { - if (prefs.autoZap && !didZap && !isMine && !zapping) { - const lnurl = getZapTarget(); - if (wallet?.isReady() && lnurl) { - setZapping(true); - queueMicrotask(async () => { - try { - await fastZapInner(lnurl, prefs.defaultZapAmount); - } catch { - // ignored - } finally { - setZapping(false); - } - }); - } - } - }, [prefs.autoZap, author, zapping]); - function powIcon() { const pow = findTag(ev, "nonce") ? countLeadingZeros(ev.id) : undefined; if (pow) { @@ -189,26 +84,6 @@ export default function NoteFooter(props: NoteFooterProps) { } } - function tipButton() { - const targets = getZapTarget(); - if (targets) { - return ( -
- fastZap(e)} - /> - -
- ); - } - return
; - } - function repostIcon() { if (readonly) return; return ( @@ -306,8 +181,7 @@ export default function NoteFooter(props: NoteFooterProps) { {repostIcon()} {reactionIcon()} {powIcon()} - {tipButton()} - setTip(false)} show={tip} note={ev.id} allocatePool={true} /> + ); } diff --git a/packages/app/src/Components/Event/Note/NoteFooter/ZapperQueue.tsx b/packages/app/src/Components/Event/Note/NoteFooter/ZapperQueue.tsx new file mode 100644 index 00000000..b39f790e --- /dev/null +++ b/packages/app/src/Components/Event/Note/NoteFooter/ZapperQueue.tsx @@ -0,0 +1,5 @@ +import { processWorkQueue, WorkQueueItem } from "@snort/shared"; + +export const ZapperQueue: Array = []; + +processWorkQueue(ZapperQueue); diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index bfba0e94..61ebdb32 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -321,6 +321,9 @@ "9WRlF4": { "defaultMessage": "Send" }, + "9kO0VQ": { + "defaultMessage": "Hide muted notes" + }, "9kSari": { "defaultMessage": "Retry publishing" }, @@ -1582,6 +1585,9 @@ "sZQzjQ": { "defaultMessage": "Failed to parse zap split: {input}" }, + "sfL/O+": { + "defaultMessage": "Muted notes will not be shown" + }, "tGXF0Q": { "defaultMessage": "Relay Lists" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 732b89cb..24bc45c9 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -106,6 +106,7 @@ "9HU8vw": "Reply", "9SvQep": "Follows {n}", "9WRlF4": "Send", + "9kO0VQ": "Hide muted notes", "9kSari": "Retry publishing", "9pMqYs": "Nostr Address", "9wO4wJ": "Lightning Invoice", @@ -522,6 +523,7 @@ "sKDn4e": "Show Badges", "sUNhQE": "user", "sZQzjQ": "Failed to parse zap split: {input}", + "sfL/O+": "Muted notes will not be shown", "tGXF0Q": "Relay Lists", "tOdNiY": "Dark", "th5lxp": "Send note to a subset of your write relays",