mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
feat: send zaps to user
This commit is contained in:
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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()}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user