feat: send zaps to user

This commit is contained in:
Shusui MOYATANI
2024-06-28 09:02:18 +09:00
parent f6afec631b
commit 7f30181840
3 changed files with 73 additions and 22 deletions

View File

@@ -460,7 +460,7 @@ const Actions: Component<ActionProps> = (props) => {
<RepostsModal event={props.event} onClose={closeModal} /> <RepostsModal event={props.event} onClose={closeModal} />
</Match> </Match>
<Match when={modal() === 'ZapRequest'}> <Match when={modal() === 'ZapRequest'}>
<ZapRequestModal event={props.event} onClose={closeModal} /> <ZapRequestModal zapTo={{ event: props.event }} onClose={closeModal} />
</Match> </Match>
</Switch> </Switch>
</> </>

View File

@@ -2,6 +2,7 @@ import { Component, createSignal, createMemo, Show, Switch, Match, createEffect
import { createMutation } from '@tanstack/solid-query'; import { createMutation } from '@tanstack/solid-query';
import ArrowPath from 'heroicons/24/outline/arrow-path.svg'; import ArrowPath from 'heroicons/24/outline/arrow-path.svg';
import Bolt from 'heroicons/24/outline/bolt.svg';
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg'; import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg';
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg'; import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
import CheckCircle from 'heroicons/24/solid/check-circle.svg'; import CheckCircle from 'heroicons/24/solid/check-circle.svg';
@@ -12,6 +13,7 @@ import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentD
import BasicModal from '@/components/modal/BasicModal'; import BasicModal from '@/components/modal/BasicModal';
import EventDebugModal from '@/components/modal/EventDebugModal'; import EventDebugModal from '@/components/modal/EventDebugModal';
import UserList from '@/components/modal/UserList'; import UserList from '@/components/modal/UserList';
import ZapRequestModal from '@/components/modal/ZapRequestModal';
import Timeline from '@/components/timeline/Timeline'; import Timeline from '@/components/timeline/Timeline';
import SafeLink from '@/components/utils/SafeLink'; import SafeLink from '@/components/utils/SafeLink';
import useContextMenu from '@/components/utils/useContextMenu'; import useContextMenu from '@/components/utils/useContextMenu';
@@ -34,6 +36,7 @@ import ensureNonNull from '@/utils/ensureNonNull';
import npubEncodeFallback from '@/utils/npubEncodeFallback'; import npubEncodeFallback from '@/utils/npubEncodeFallback';
import timeout from '@/utils/timeout'; import timeout from '@/utils/timeout';
export type ProfileDisplayProps = { export type ProfileDisplayProps = {
pubkey: string; pubkey: string;
onClose?: () => void; onClose?: () => void;
@@ -60,7 +63,9 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
const [updatingContacts, setUpdatingContacts] = createSignal(false); const [updatingContacts, setUpdatingContacts] = createSignal(false);
const [hoverFollowButton, setHoverFollowButton] = createSignal(false); const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
const [showFollowers, setShowFollowers] = createSignal(false); const [showFollowers, setShowFollowers] = createSignal(false);
const [modal, setModal] = createSignal<'Following' | 'EventDebugModal' | null>(null); const [modal, setModal] = createSignal<'Following' | 'EventDebugModal' | 'ZapRequest' | null>(
null,
);
const closeModal = () => setModal(null); const closeModal = () => setModal(null);
const { const {
@@ -354,6 +359,12 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</button> </button>
</Match> </Match>
</Switch> </Switch>
<button
class="w-10 rounded-full border border-primary p-2 text-primary hover:border-primary-hover hover:text-primary-hover"
onClick={() => setModal('ZapRequest')}
>
<Bolt />
</button>
<button <button
ref={otherActionsPopup.targetRef} ref={otherActionsPopup.targetRef}
type="button" type="button"
@@ -482,6 +493,9 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
/> />
)} )}
</Match> </Match>
<Match when={modal() === 'ZapRequest'}>
<ZapRequestModal zapTo={{ pubkey: props.pubkey }} onClose={closeModal} />
</Match>
</Switch> </Switch>
<ul class="border-t border-border p-1"> <ul class="border-t border-border p-1">
<LoadMore loadMore={loadMore} eose={eose()}> <LoadMore loadMore={loadMore} eose={eose()}>

View File

@@ -21,6 +21,7 @@ import { requestProvider, type WebLNProvider } from 'webln';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import EventDisplay from '@/components/event/EventDisplay'; import EventDisplay from '@/components/event/EventDisplay';
import BasicModal from '@/components/modal/BasicModal'; import BasicModal from '@/components/modal/BasicModal';
import UserNameDisplay from '@/components/UserDisplayName';
import Copy from '@/components/utils/Copy'; import Copy from '@/components/utils/Copy';
import useConfig from '@/core/useConfig'; import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation'; import { useTranslation } from '@/i18n/useTranslation';
@@ -39,17 +40,33 @@ import verifyInvoice from '@/nostr/zap/verifyInvoice';
import ensureNonNull from '@/utils/ensureNonNull'; import ensureNonNull from '@/utils/ensureNonNull';
import epoch from '@/utils/epoch'; import epoch from '@/utils/epoch';
type ZapTarget = { event: NostrEvent } | { pubkey: string };
export type ZapRequestModalProps = { export type ZapRequestModalProps = {
event: NostrEvent; zapTo: ZapTarget;
// TODO zap to profile // TODO zap to profile
onClose: () => void; onClose: () => void;
}; };
type ZapDialogProps = { type ZapDialogProps = {
zapTo: ZapTarget;
lnurlPayUrl?: string | null; lnurlPayUrl?: string | null;
event: NostrEvent;
}; };
const getPubkey = (zapTo: ZapTarget): string => {
if ('event' in zapTo) {
return zapTo.event.pubkey;
}
if ('pubkey' in zapTo) {
return zapTo.pubkey;
}
throw new Error('unexpected ZapTarget');
};
const getEvent = (zapTo: ZapTarget): NostrEvent | undefined =>
'event' in zapTo ? zapTo.event : undefined;
const useWebLN = () => { const useWebLN = () => {
const [provider, setProvider] = createSignal<WebLNProvider | undefined>(); const [provider, setProvider] = createSignal<WebLNProvider | undefined>();
const [status, setStatus] = createSignal<'available' | 'unavailable' | 'checking'>('checking'); const [status, setStatus] = createSignal<'available' | 'unavailable' | 'checking'>('checking');
@@ -96,9 +113,11 @@ const QRCodeDisplay: Component<{ text: string }> = (props) => {
return <canvas width="256" height="256" ref={canvasRef} />; return <canvas width="256" height="256" ref={canvasRef} />;
}; };
const InvoiceDisplay: Component<{ invoice: string; event: NostrEvent; nostrPubkey?: string }> = ( const InvoiceDisplay: Component<{
props, invoice: string;
) => { recipientPubkey: string;
lnurlPubkey?: string;
}> = (props) => {
const i18n = useTranslation(); const i18n = useTranslation();
const { config } = useConfig(); const { config } = useConfig();
const webln = useWebLN(); const webln = useWebLN();
@@ -106,14 +125,13 @@ const InvoiceDisplay: Component<{ invoice: string; event: NostrEvent; nostrPubke
const lightingInvoiceWithSchema = () => `lightning:${props.invoice}`; const lightingInvoiceWithSchema = () => `lightning:${props.invoice}`;
const { events } = useSubscription(() => const { events } = useSubscription(() =>
ensureNonNull([props.nostrPubkey] as const)(([nostrPubkey]) => ({ ensureNonNull([props.lnurlPubkey] as const)(([lnurlPubkey]) => ({
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
filters: [ filters: [
{ {
kinds: [Kind.Zap], kinds: [Kind.Zap],
authors: nostrPubkey != null ? [nostrPubkey] : undefined, authors: lnurlPubkey != null ? [lnurlPubkey] : undefined,
'#p': [props.event.pubkey], '#p': [props.recipientPubkey],
'#e': [props.event.id],
since: epoch(), since: epoch(),
}, },
], ],
@@ -197,8 +215,17 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
})), })),
); );
const event = () => genericEvent(props.event); const event = () => {
const hasZapTag = () => event().findTagsByName('zap').length > 0; const rawEvent = getEvent(props.zapTo);
if (rawEvent == null) return undefined;
return genericEvent(rawEvent);
};
const hasZapTag = () => {
const ev = event();
if (ev == null) return false;
return ev.findTagsByName('zap').length > 0;
};
const lnurlPayUrlDomain = () => { const lnurlPayUrlDomain = () => {
if (props.lnurlPayUrl == null) return null; if (props.lnurlPayUrl == null) return null;
@@ -240,8 +267,8 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
amountMilliSats, amountMilliSats,
content: comment(), content: comment(),
pubkey: p, pubkey: p,
recipientPubkey: props.event.pubkey, recipientPubkey: getPubkey(props.zapTo),
eventId: props.event.id, eventId: event()?.id,
relays: config().relayUrls, relays: config().relayUrls,
lnurlPayUrl: props.lnurlPayUrl, lnurlPayUrl: props.lnurlPayUrl,
}); });
@@ -263,7 +290,7 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
}; };
const getInvoiceMutation = createMutation(() => ({ const getInvoiceMutation = createMutation(() => ({
mutationKey: ['getInvoiceMutation', props.event.id], mutationKey: ['getInvoiceMutation', props.zapTo],
mutationFn: () => getInvoice(), mutationFn: () => getInvoice(),
})); }));
@@ -293,8 +320,8 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
{(invoice) => ( {(invoice) => (
<InvoiceDisplay <InvoiceDisplay
invoice={invoice} invoice={invoice}
event={props.event} recipientPubkey={getPubkey(props.zapTo)}
nostrPubkey={endpoint()?.nostrPubkey} lnurlPubkey={endpoint()?.nostrPubkey}
/> />
)} )}
</Match> </Match>
@@ -321,7 +348,14 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
<div>{endpoint()?.decodedMetadata?.textLongDesc}</div> <div>{endpoint()?.decodedMetadata?.textLongDesc}</div>
</div> </div>
<div class="w-96 rounded-lg border border-border p-2"> <div class="w-96 rounded-lg border border-border p-2">
<EventDisplay event={props.event} actions={false} embedding={false} /> <Switch>
<Match when={'event' in props.zapTo && props.zapTo} keyed>
{(zapTo) => <EventDisplay event={zapTo.event} actions={false} embedding={false} />}
</Match>
<Match when={'pubkey' in props.zapTo && props.zapTo} keyed>
{(zapTo) => <UserNameDisplay pubkey={zapTo.pubkey} />}
</Match>
</Switch>
</div> </div>
<form class="mt-4 flex w-64 flex-col items-center gap-1" onSubmit={handleSubmit}> <form class="mt-4 flex w-64 flex-col items-center gap-1" onSubmit={handleSubmit}>
<label class="flex w-full items-center gap-2"> <label class="flex w-full items-center gap-2">
@@ -366,8 +400,11 @@ const ZapDialog: Component<ZapDialogProps> = (props) => {
const ZapRequestModal: Component<ZapRequestModalProps> = (props) => { const ZapRequestModal: Component<ZapRequestModalProps> = (props) => {
const i18n = useTranslation(); const i18n = useTranslation();
const recipientPubkey = () => getPubkey(props.zapTo);
const { lud06, lud16, isZapConfigured } = useProfile(() => ({ const { lud06, lud16, isZapConfigured } = useProfile(() => ({
pubkey: props.event.pubkey, pubkey: recipientPubkey(),
})); }));
const [lnurlSource, setLnurlSource] = createSignal<'lud06' | 'lud16' | undefined>(); const [lnurlSource, setLnurlSource] = createSignal<'lud06' | 'lud16' | undefined>();
@@ -412,10 +449,10 @@ const ZapRequestModal: Component<ZapRequestModalProps> = (props) => {
</div> </div>
</Show> </Show>
<Show when={lnurlSource() === 'lud06' && lud06()} keyed> <Show when={lnurlSource() === 'lud06' && lud06()} keyed>
{(value) => <ZapDialog lnurlPayUrl={lud06ToLnurlPayUrl(value)} event={props.event} />} {(value) => <ZapDialog lnurlPayUrl={lud06ToLnurlPayUrl(value)} zapTo={props.zapTo} />}
</Show> </Show>
<Show when={lnurlSource() === 'lud16' && lud16()} keyed> <Show when={lnurlSource() === 'lud16' && lud16()} keyed>
{(value) => <ZapDialog lnurlPayUrl={lud16ToLnurlPayUrl(value)} event={props.event} />} {(value) => <ZapDialog lnurlPayUrl={lud16ToLnurlPayUrl(value)} zapTo={props.zapTo} />}
</Show> </Show>
</Show> </Show>
</div> </div>