mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Preliminary lightning invoice rendering
This commit is contained in:
parent
371b55aa44
commit
4e15d19b40
20
package-lock.json
generated
20
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"@thisbeyond/solid-select": "^0.13.0",
|
||||
"@types/dompurify": "3.0.2",
|
||||
"dompurify": "3.0.5",
|
||||
"light-bolt11-decoder": "^3.1.1",
|
||||
"medium-zoom": "1.0.8",
|
||||
"nostr-tools": "1.15.0",
|
||||
"photoswipe": "5.4.3",
|
||||
@ -1734,6 +1735,25 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/light-bolt11-decoder": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.1.1.tgz",
|
||||
"integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==",
|
||||
"dependencies": {
|
||||
"@scure/base": "1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/light-bolt11-decoder/node_modules/@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@thisbeyond/solid-select": "^0.13.0",
|
||||
"@types/dompurify": "3.0.2",
|
||||
"dompurify": "3.0.5",
|
||||
"light-bolt11-decoder": "^3.1.1",
|
||||
"medium-zoom": "1.0.8",
|
||||
"nostr-tools": "1.15.0",
|
||||
"photoswipe": "5.4.3",
|
||||
|
11
src/assets/icons/copy_border.svg
Normal file
11
src/assets/icons/copy_border.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6181_960)">
|
||||
<path d="M2 1.5H9C9.27614 1.5 9.5 1.72386 9.5 2V2.25C9.5 2.66421 9.83579 3 10.25 3C10.6642 3 11 2.66421 11 2.25V2C11 0.895431 10.1046 0 9 0H2C0.895431 0 0 0.89543 0 2V9C0 10.1046 0.895431 11 2 11H2.25C2.66421 11 3 10.6642 3 10.25C3 9.83579 2.66421 9.5 2.25 9.5H2C1.72386 9.5 1.5 9.27614 1.5 9V2C1.5 1.72386 1.72386 1.5 2 1.5Z" fill="#AAAAAA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 7C5 5.89543 5.89543 5 7 5H14C15.1046 5 16 5.89543 16 7V14C16 15.1046 15.1046 16 14 16H7C5.89543 16 5 15.1046 5 14V7ZM7 6.5H14C14.2761 6.5 14.5 6.72386 14.5 7V14C14.5 14.2761 14.2761 14.5 14 14.5H7C6.72386 14.5 6.5 14.2761 6.5 14V7C6.5 6.72386 6.72386 6.5 7 6.5Z" fill="#AAAAAA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6181_960">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 923 B |
10
src/assets/icons/lightning.svg
Normal file
10
src/assets/icons/lightning.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="14" height="20" viewBox="0 0 14 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9211 7.71733C14.1194 7.45376 13.921 7.0876 13.5799 7.0876H8.66235L9.6442 0.953049C9.81526 0.125008 8.6907 -0.353895 8.15325 0.318065L0.0861641 11.5986C-0.123564 11.8608 0.07342 12.2377 0.420148 12.2377H5.30747L4.28387 19.0603C4.12666 19.8963 5.27231 20.3544 5.79207 19.6633L13.9211 7.71733Z" fill="url(#paint0_linear_6181_952)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9211 7.71733C14.1194 7.45376 13.921 7.0876 13.5799 7.0876H8.66235L9.6442 0.953049C9.81526 0.125008 8.6907 -0.353895 8.15325 0.318065L0.0861641 11.5986C-0.123564 11.8608 0.07342 12.2377 0.420148 12.2377H5.30747L4.28387 19.0603C4.12666 19.8963 5.27231 20.3544 5.79207 19.6633L13.9211 7.71733Z" fill="#FA9011"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6181_952" x1="4.55" y1="1.37931" x2="10.0011" y2="18.3238" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.078125" stop-color="#FFD12F"/>
|
||||
<stop offset="0.860784" stop-color="#FF9F2F"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -20,6 +20,8 @@ import ReactionsModal from '../ReactionsModal/ReactionsModal';
|
||||
import { useAppContext } from '../../contexts/AppContext';
|
||||
import CustomZap from '../CustomZap/CustomZap';
|
||||
import NoteContextMenu from '../Note/NoteContextMenu';
|
||||
import LnQrCodeModal from '../LnQrCodeModal/LnQrCodeModal';
|
||||
import ConfirmModal from '../ConfirmModal/ConfirmModal';
|
||||
|
||||
export const [isHome, setIsHome] = createSignal(false);
|
||||
|
||||
@ -166,6 +168,23 @@ const Layout: Component = () => {
|
||||
onFail={app?.customZap?.onFail}
|
||||
onCancel={app?.customZap?.onCancel}
|
||||
/>
|
||||
|
||||
<LnQrCodeModal
|
||||
open={app?.showLnInvoiceModal}
|
||||
lnbc={app?.lnbc?.invoice || ''}
|
||||
onPay={app?.lnbc?.onPay}
|
||||
onClose={app?.lnbc?.onCancel}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
open={app?.showConfirmModal}
|
||||
title={app?.confirmInfo?.title}
|
||||
description={app?.confirmInfo?.description}
|
||||
confirmLabel={app?.confirmInfo?.confirmLabel}
|
||||
abortLabel={app?.confirmInfo?.abortLabel}
|
||||
onConfirm={app?.confirmInfo?.onConfirm}
|
||||
onAbort={app?.confirmInfo?.onAbort}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
128
src/components/LnQrCodeModal/LnQrCodeModal.module.scss
Normal file
128
src/components/LnQrCodeModal/LnQrCodeModal.module.scss
Normal file
@ -0,0 +1,128 @@
|
||||
.LnQrCodeModal {
|
||||
position: fixed;
|
||||
min-width: 472px;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-input);
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 24px 28px 24px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin: 0px 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.description {
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: 1px solid var(--subtile-devider);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
|
||||
.expiryDate {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
.expiredDate {
|
||||
color: var(--text-tertiary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.payAction {
|
||||
height: 36px;
|
||||
min-width: 120px;
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zapIcon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-block;
|
||||
margin-right: 9px;
|
||||
background: var(--sidebar-section-icon-gradient);
|
||||
-webkit-mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
|
||||
mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
|
||||
}
|
88
src/components/LnQrCodeModal/LnQrCodeModal.tsx
Normal file
88
src/components/LnQrCodeModal/LnQrCodeModal.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
// @ts-ignore
|
||||
import { decode } from 'light-bolt11-decoder';
|
||||
import { Component, createEffect } from 'solid-js';
|
||||
import { createStore, reconcile } from 'solid-js/store';
|
||||
import { emptyInvoice } from '../../constants';
|
||||
import { date, dateFuture } from '../../lib/dates';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { humanizeNumber } from '../../lib/stats';
|
||||
import { lnInvoice } from '../../translations';
|
||||
import { LnbcInvoice } from '../../types/primal';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import Modal from '../Modal/Modal';
|
||||
import QrCode from '../QrCode/QrCode';
|
||||
|
||||
import styles from './LnQrCodeModal.module.scss';
|
||||
|
||||
const LnQrCodeModal: Component<{
|
||||
id?: string,
|
||||
open?: boolean,
|
||||
lnbc: string | undefined,
|
||||
onPay?: () => void,
|
||||
onClose?: () => void,
|
||||
}> = (props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [invoice, setInvoice] = createStore<LnbcInvoice>(emptyInvoice);
|
||||
|
||||
createEffect(() => {
|
||||
if (props.lnbc) {
|
||||
const dec: LnbcInvoice = decode(props.lnbc);
|
||||
setInvoice(reconcile(dec));
|
||||
} else {
|
||||
setInvoice(reconcile(emptyInvoice));
|
||||
}
|
||||
});
|
||||
|
||||
const expiryDate = () => {
|
||||
const expiry = invoice.sections.find(s => s.name === 'expiry')?.value as number;
|
||||
const created = invoice.sections.find(s => s.name === 'timestamp')?.value as number;
|
||||
|
||||
return expiry + created;
|
||||
}
|
||||
|
||||
const amount = () =>
|
||||
`${humanizeNumber(parseInt(invoice.sections.find(s => s.name === 'amount')?.value ||'0') / 1_000)} sats`;
|
||||
|
||||
const description = () =>
|
||||
invoice.sections.find(s => s.name === 'description')?.value;
|
||||
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<div id={props.id} class={styles.LnQrCodeModal}>
|
||||
<div class={styles.header}>
|
||||
<div class={styles.title}>
|
||||
{intl.formatMessage(lnInvoice.title)}
|
||||
</div>
|
||||
<button class={styles.close} onClick={props.onClose}>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={styles.body}>
|
||||
<div class={styles.qrCode}>
|
||||
<QrCode data={props.lnbc || ''} />
|
||||
</div>
|
||||
|
||||
<div class={styles.description}>{description()}</div>
|
||||
<div class={styles.amount}>{amount()}</div>
|
||||
|
||||
<div class={styles.separator}></div>
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
<div class={styles.expiryDate}>
|
||||
{intl.formatMessage(lnInvoice.expires, { date: dateFuture(expiryDate(), 'long').label })}
|
||||
</div>
|
||||
<div class={styles.payAction}>
|
||||
<ButtonPrimary onClick={props.onPay}>
|
||||
{intl.formatMessage(lnInvoice.pay)}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(LnQrCodeModal);
|
161
src/components/Lnbc/Lnbc.module.scss
Normal file
161
src/components/Lnbc/Lnbc.module.scss
Normal file
@ -0,0 +1,161 @@
|
||||
.lnbc {
|
||||
width: 100%;
|
||||
min-height: 158px;
|
||||
background-color: var(--background-header-input);
|
||||
border-radius: var(--border-radius-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
|
||||
.paymentOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-site);
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
button {
|
||||
.qrIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px;
|
||||
mask: url(../../assets/icons/qr_code.svg) no-repeat 0px / 18px;
|
||||
}
|
||||
.copyIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px;
|
||||
mask: url(../../assets/icons/copy_border.svg) no-repeat 0px / 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.qrIcon, .copyIcon {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyDone {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.checkIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
background-color: var(--success-bright);
|
||||
-webkit-mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px;
|
||||
mask: url(../../assets/icons/check.svg) no-repeat 0px / 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
|
||||
.description {
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
.expiryDate {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
.expiredDate {
|
||||
color: var(--text-tertiary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.payAction {
|
||||
height: 36px;
|
||||
min-width: 120px;
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lnIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url('../../assets/icons/lightning.svg');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
349
src/components/Lnbc/Lnbc.tsx
Normal file
349
src/components/Lnbc/Lnbc.tsx
Normal file
@ -0,0 +1,349 @@
|
||||
import { Component, createEffect, createSignal, onMount, Show } from 'solid-js';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
// @ts-ignore
|
||||
import { decode } from 'light-bolt11-decoder';
|
||||
|
||||
import styles from './Lnbc.module.scss';
|
||||
import { createStore, reconcile } from 'solid-js/store';
|
||||
import { humanizeNumber } from '../../lib/stats';
|
||||
import { date, dateFuture } from '../../lib/dates';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import ButtonGhost from '../Buttons/ButtonGhost';
|
||||
import { LnbcInvoice } from '../../types/primal';
|
||||
import { emptyInvoice, Kind } from '../../constants';
|
||||
import { useAppContext } from '../../contexts/AppContext';
|
||||
import { sendMessage, subTo } from '../../lib/sockets';
|
||||
import { APP_ID } from '../../App';
|
||||
import { signEvent } from '../../lib/nostrAPI';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { logError, logInfo } from '../../lib/logger';
|
||||
import { useToastContext } from '../Toaster/Toaster';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { lnInvoice } from '../../translations';
|
||||
|
||||
|
||||
const Lnbc: Component< { id?: string, lnbc: string } > = (props) => {
|
||||
|
||||
const app = useAppContext();
|
||||
const toast = useToastContext();
|
||||
const intl = useIntl();
|
||||
|
||||
const [invoice, setInvoice] = createStore<LnbcInvoice>({ ...emptyInvoice });
|
||||
|
||||
const [invoiceCopied, setInvoiceCopied] = createSignal(false);
|
||||
|
||||
const [paymentInProgress, setPaymentInProgress] = createSignal(false);
|
||||
|
||||
createEffect(() => {
|
||||
const dec: LnbcInvoice = decode(props.lnbc);
|
||||
setInvoice(reconcile({...emptyInvoice}))
|
||||
setInvoice(() => ({ ...dec }));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (invoiceCopied()) {
|
||||
setTimeout(() => {
|
||||
setInvoiceCopied(() => false);
|
||||
}, 1_000);
|
||||
}
|
||||
})
|
||||
|
||||
const isLightning = () => invoice.sections.find(s => s.name === 'lightning_network');
|
||||
|
||||
const expiryDate = () => {
|
||||
const expiry = invoice.sections.find(s => s.name === 'expiry')?.value as number;
|
||||
const created = invoice.sections.find(s => s.name === 'timestamp')?.value as number;
|
||||
|
||||
return expiry + created;
|
||||
}
|
||||
|
||||
const hasExpired = () => {
|
||||
const today = Math.floor((new Date()).getTime() / 1_000);
|
||||
|
||||
return today > expiryDate();
|
||||
}
|
||||
|
||||
const amount = () =>
|
||||
`${humanizeNumber(parseInt(invoice.sections.find(s => s.name === 'amount')?.value ||'0') / 1_000)} sats`;
|
||||
|
||||
const description = () =>
|
||||
invoice.sections.find(s => s.name === 'description')?.value;
|
||||
|
||||
const confirmPayment = () => app?.actions.openConfirmModal({
|
||||
title: intl.formatMessage(lnInvoice.confirm.title),
|
||||
description: intl.formatMessage(lnInvoice.confirm.description, { amount: amount() }),
|
||||
confirmLabel: intl.formatMessage(lnInvoice.confirm.confirmLabel),
|
||||
abortLabel: intl.formatMessage(lnInvoice.confirm.abortLabel),
|
||||
onAbort: app.actions.closeConfirmModal,
|
||||
onConfirm: () => {
|
||||
app.actions.closeConfirmModal();
|
||||
payInvoice();
|
||||
},
|
||||
});
|
||||
|
||||
const payInvoice = () => {
|
||||
setPaymentInProgress(() => true);
|
||||
const walletSocket = new WebSocket('wss://wallet.primal.net/v1');
|
||||
|
||||
walletSocket.addEventListener('close', () => {
|
||||
logInfo('PREMIUM SOCKET CLOSED');
|
||||
});
|
||||
|
||||
walletSocket.addEventListener('open', () => {
|
||||
logInfo('WALLET SOCKET OPENED');
|
||||
sendPayment(walletSocket, (success: boolean) => {
|
||||
if (!success) {
|
||||
toast?.sendWarning(`Failed to pay ${amount()}`);
|
||||
}
|
||||
walletSocket.close();
|
||||
setPaymentInProgress(() => false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const sendPayment = async (socket: WebSocket, then?: (success: boolean) => void) => {
|
||||
const subId = `sp_${APP_ID}`;
|
||||
|
||||
let success = true;
|
||||
|
||||
const unsub = subTo(socket, subId, (type, _, content) => {
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
then && then(success);
|
||||
}
|
||||
|
||||
if (type === 'NOTICE') {
|
||||
success = false;
|
||||
}
|
||||
});
|
||||
|
||||
const content = JSON.stringify(
|
||||
["withdraw", {
|
||||
subwallet: 1,
|
||||
lnInvoice: invoice.paymentRequest,
|
||||
target_lud16: '',
|
||||
note_for_recipient: invoice.sections.find(s => s.name === 'description')?.value || '',
|
||||
note_for_self: '',
|
||||
}],
|
||||
);
|
||||
|
||||
const event = {
|
||||
content,
|
||||
kind: Kind.WALLET_OPERATION,
|
||||
created_at: Math.ceil((new Date()).getTime() / 1000),
|
||||
tags: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const signedEvent = await signEvent(event);
|
||||
|
||||
sendMessage(socket, JSON.stringify([
|
||||
"REQ",
|
||||
subId,
|
||||
{cache: ["wallet", { operation_event: signedEvent }]},
|
||||
]));
|
||||
} catch (reason) {
|
||||
logError('failed to sign due to: ', reason);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={props.id} class={styles.lnbc}>
|
||||
<Show when={paymentInProgress()}>
|
||||
<div class={styles.paymentOverlay}>
|
||||
<Loader />
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.header}>
|
||||
<Show when={isLightning()}>
|
||||
<div class={styles.title}>
|
||||
<div class={styles.lnIcon}></div>
|
||||
<div>{intl.formatMessage(lnInvoice.title)}</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.headerActions}>
|
||||
<Show
|
||||
when={!hasExpired()}
|
||||
>
|
||||
<ButtonGhost
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
app?.actions.openLnbcModal(props.lnbc, () => {
|
||||
app.actions.closeLnbcModal();
|
||||
confirmPayment();
|
||||
});
|
||||
}}
|
||||
shrink={true}
|
||||
>
|
||||
<div class={styles.qrIcon}></div>
|
||||
</ButtonGhost>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={!invoiceCopied()}
|
||||
fallback={<div class={styles.copyDone}><div class={styles.checkIcon}></div></div>}
|
||||
>
|
||||
<ButtonGhost
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
navigator.clipboard.writeText(props.lnbc);
|
||||
setInvoiceCopied(() => true);
|
||||
}}
|
||||
shrink={true}
|
||||
>
|
||||
<div class={styles.copyIcon}></div>
|
||||
</ButtonGhost>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.body}>
|
||||
<div class={styles.description}>{description()}</div>
|
||||
<div class={styles.amount}>{amount()}</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
<Show
|
||||
when={!hasExpired()}
|
||||
fallback={
|
||||
<div class={styles.expiredDate}>
|
||||
{intl.formatMessage(lnInvoice.expired, { date: date(expiryDate(), 'long').label })}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class={styles.expiryDate}>
|
||||
{intl.formatMessage(lnInvoice.expires, { date: dateFuture(expiryDate(), 'long').label })}
|
||||
</div>
|
||||
<div class={styles.payAction}>
|
||||
<ButtonPrimary onClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
confirmPayment();
|
||||
}}>
|
||||
{intl.formatMessage(lnInvoice.pay)}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(Lnbc);
|
||||
|
||||
// sections = [
|
||||
// {
|
||||
// "name": "lightning_network",
|
||||
// "letters": "ln"
|
||||
// },
|
||||
// {
|
||||
// "name": "coin_network",
|
||||
// "letters": "bc",
|
||||
// "value": {
|
||||
// "bech32": "bc",
|
||||
// "pubKeyHash": 0,
|
||||
// "scriptHash": 5,
|
||||
// "validWitnessVersions": [
|
||||
// 0
|
||||
// ]
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "name": "amount",
|
||||
// "letters": "100u",
|
||||
// "value": "10000000"
|
||||
// },
|
||||
// {
|
||||
// "name": "separator",
|
||||
// "letters": "1"
|
||||
// },
|
||||
// {
|
||||
// "name": "timestamp",
|
||||
// "letters": "pjatlyx",
|
||||
// "value": 1708522630
|
||||
// },
|
||||
// {
|
||||
// "name": "payment_secret",
|
||||
// "tag": "s",
|
||||
// "letters": "sp5938h8ewswdm7smn9yfge6wvfeletzxrujz2kt6yjxl77at09zlys",
|
||||
// "value": "2c4f73e5d07377e86e6522519d3989cff2b1187c909565e89237fdeeade517c9"
|
||||
// },
|
||||
// {
|
||||
// "name": "payment_hash",
|
||||
// "tag": "p",
|
||||
// "letters": "pp5edcuua748en26zlyjga47gpcga3htnw06xmqw4zrkwwz37s45cxq",
|
||||
// "value": "cb71ce77d53e66ad0be4923b5f2038476375cdcfd1b6075443b39c28fa15a60c"
|
||||
// },
|
||||
// {
|
||||
// "name": "description",
|
||||
// "tag": "d",
|
||||
// "letters": "dqgv4h82arn",
|
||||
// "value": "enuts"
|
||||
// },
|
||||
// {
|
||||
// "name": "expiry",
|
||||
// "tag": "x",
|
||||
// "letters": "xqzjc",
|
||||
// "value": 600
|
||||
// },
|
||||
// {
|
||||
// "name": "min_final_cltv_expiry",
|
||||
// "tag": "c",
|
||||
// "letters": "cqpj",
|
||||
// "value": 18
|
||||
// },
|
||||
// {
|
||||
// "name": "route_hint",
|
||||
// "tag": "r",
|
||||
// "letters": "rzjqgfffll4jmjf0tffqtx47xt886gzp9fajp3966xz96gm2xj9cqedxrrld5qq0tgqqqqqqqqqqqqqrssqyg",
|
||||
// "value": [
|
||||
// {
|
||||
// "pubkey": "021294fff596e497ad2902cd5f19673e9020953d90625d68c22e91b51a45c032d3",
|
||||
// "short_channel_id": "0c7f6d0007ad0000",
|
||||
// "fee_base_msat": 0,
|
||||
// "fee_proportional_millionths": 450,
|
||||
// "cltv_expiry_delta": 34
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "name": "feature_bits",
|
||||
// "tag": "9",
|
||||
// "letters": "9qxpqysgq",
|
||||
// "value": {
|
||||
// "option_data_loss_protect": "unsupported",
|
||||
// "initial_routing_sync": "unsupported",
|
||||
// "option_upfront_shutdown_script": "unsupported",
|
||||
// "gossip_queries": "unsupported",
|
||||
// "var_onion_optin": "required",
|
||||
// "gossip_queries_ex": "unsupported",
|
||||
// "option_static_remotekey": "unsupported",
|
||||
// "payment_secret": "required",
|
||||
// "basic_mpp": "supported",
|
||||
// "option_support_large_channel": "unsupported",
|
||||
// "extra_bits": {
|
||||
// "start_bit": 20,
|
||||
// "bits": [
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// false,
|
||||
// false,
|
||||
// false
|
||||
// ],
|
||||
// "has_required": false
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "name": "signature",
|
||||
// "letters": "ml5za767e9scmd52l8mh8zl0g93n74jq0asr98ezvq0gpw8cmsrknehucng4utdjm3cx5mpzkc3psty5yp3ftddkhhrp2hsvy3q08ucq",
|
||||
// "value": "dfe82efb5ec9618db68af9f7738bef41633f56407f60329f22601e80b8f8dc0769e6fcc4d15e2db2dc706a6c22b622182c94206295b5b6bdc6155e0c2440f3f300"
|
||||
// },
|
||||
// {
|
||||
// "name": "checksum",
|
||||
// "letters": "ef6k3v"
|
||||
// }
|
||||
// ],
|
@ -9,17 +9,14 @@ import { useIntl } from '@cookbook/solid-intl';
|
||||
|
||||
import { truncateNumber } from '../../../lib/notifications';
|
||||
import { canUserReceiveZaps, zapNote } from '../../../lib/zap';
|
||||
import CustomZap from '../../CustomZap/CustomZap';
|
||||
import { useSettingsContext } from '../../../contexts/SettingsContext';
|
||||
|
||||
import zapMD from '../../../assets/lottie/zap_md.json';
|
||||
import { toast as t } from '../../../translations';
|
||||
import PrimalMenu from '../../PrimalMenu/PrimalMenu';
|
||||
import { hookForDev } from '../../../lib/devTools';
|
||||
import NoteContextMenu from '../NoteContextMenu';
|
||||
import { getScreenCordinates } from '../../../utils';
|
||||
import ZapAnimation from '../../ZapAnimation/ZapAnimation';
|
||||
import ReactionsModal from '../../ReactionsModal/ReactionsModal';
|
||||
import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext';
|
||||
|
||||
const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> = (props) => {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
isHashtag,
|
||||
isImage,
|
||||
isInterpunction,
|
||||
isLnbc,
|
||||
isMixCloud,
|
||||
isMp4Video,
|
||||
isNoteMention,
|
||||
@ -45,6 +46,7 @@ import { useIntl } from '@cookbook/solid-intl';
|
||||
import { actions } from '../../translations';
|
||||
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import Lnbc from '../Lnbc/Lnbc';
|
||||
|
||||
const groupGridLimit = 7;
|
||||
|
||||
@ -396,6 +398,12 @@ const ParsedNote: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLnbc(token)) {
|
||||
lastSignificantContent = 'lnbc';
|
||||
updateContent(content, 'lnbc', token);
|
||||
return;
|
||||
}
|
||||
|
||||
lastSignificantContent = 'text';
|
||||
updateContent(content, 'text', token);
|
||||
return;
|
||||
@ -1061,6 +1069,18 @@ const ParsedNote: Component<{
|
||||
</For>
|
||||
};
|
||||
|
||||
const renderLnbc = (item: NoteContent) => {
|
||||
return <For each={item.tokens}>
|
||||
{(token) => {
|
||||
if (isNoteTooLong()) return;
|
||||
|
||||
setWordsDisplayed(w => w + 100);
|
||||
|
||||
return <Lnbc lnbc={token} />
|
||||
}}
|
||||
</For>
|
||||
}
|
||||
|
||||
const renderContent = (item: NoteContent, index: number) => {
|
||||
|
||||
const renderers: Record<string, (item: NoteContent, index?: number) => JSXElement> = {
|
||||
@ -1081,6 +1101,7 @@ const ParsedNote: Component<{
|
||||
tagmention: renderTagMention,
|
||||
hashtag: renderHashtag,
|
||||
emoji: renderEmoji,
|
||||
lnbc: renderLnbc,
|
||||
}
|
||||
|
||||
return renderers[item.type] ?
|
||||
|
@ -380,9 +380,9 @@ const ProfileTabs: Component<{
|
||||
<Show
|
||||
when={!profile?.isFetchingFollowers}
|
||||
fallback={
|
||||
<div style="margin-top: 40px;">
|
||||
<Loader />
|
||||
</div>
|
||||
<div style="margin-top: 40px;">
|
||||
<Loader />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ContentModeration, FeedPage, } from "./types/primal";
|
||||
import { ContentModeration, FeedPage, LnbcInvoice, } from "./types/primal";
|
||||
import logoFire from './assets/icons/logo_fire.svg';
|
||||
import logoIce from './assets/icons/logo_ice.svg';
|
||||
|
||||
@ -137,6 +137,8 @@ export enum Kind {
|
||||
SuggestedUsersByCategory = 10_000_134,
|
||||
UploadChunk = 10_000_135,
|
||||
UserRelays=10_000_139,
|
||||
|
||||
WALLET_OPERATION = 10_000_300,
|
||||
}
|
||||
|
||||
export const relayConnectingTimeout = 1000;
|
||||
@ -253,6 +255,7 @@ export const urlRegexG = /https?:\/\/(www\.)?[-a-zA-Z0-9\u00F0-\u02AF@:%._\+~#=]
|
||||
export const urlExtractRegex = /https?:\/\/\S+\.[^()]+(?:\([^)]*\))*/;
|
||||
export const interpunctionRegex = /^(\.|,|;|\?|\!)$/;
|
||||
export const emojiRegex = /(?:\s|^)\:\w+\:/;
|
||||
export const lnRegex = /lnbc[a-zA-Z0-9]*/;
|
||||
|
||||
export const hashtagRegex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/i;
|
||||
export const linebreakRegex = /(\r\n|\r|\n)/ig;
|
||||
@ -386,3 +389,10 @@ export const uploadLimit = {
|
||||
regular: 100,
|
||||
premium: 1024,
|
||||
}
|
||||
|
||||
export const emptyInvoice: LnbcInvoice = {
|
||||
paymentRequest: '',
|
||||
sections: [],
|
||||
expiry: 0,
|
||||
route_hints: [],
|
||||
};
|
||||
|
@ -32,6 +32,21 @@ export type NoteContextMenuInfo = {
|
||||
openReactions?: () => void,
|
||||
};
|
||||
|
||||
export type ConfirmInfo = {
|
||||
title: string,
|
||||
description: string,
|
||||
confirmLabel?: string,
|
||||
abortLabel?: string,
|
||||
onConfirm?: () => void,
|
||||
onAbort?: () => void,
|
||||
};
|
||||
|
||||
export type LnbcInfo = {
|
||||
invoice: string,
|
||||
onPay?: () => void,
|
||||
onCancel?: () => void,
|
||||
};
|
||||
|
||||
export type AppContextStore = {
|
||||
isInactive: boolean,
|
||||
appState: 'sleep' | 'waking' | 'woke',
|
||||
@ -41,6 +56,10 @@ export type AppContextStore = {
|
||||
customZap: CustomZapInfo | undefined,
|
||||
showNoteContextMenu: boolean,
|
||||
noteContextMenuInfo: NoteContextMenuInfo | undefined,
|
||||
showLnInvoiceModal: boolean,
|
||||
lnbc: LnbcInfo | undefined,
|
||||
showConfirmModal: boolean,
|
||||
confirmInfo: ConfirmInfo | undefined,
|
||||
actions: {
|
||||
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
||||
closeReactionModal: () => void,
|
||||
@ -48,6 +67,10 @@ export type AppContextStore = {
|
||||
closeCustomZapModal: () => void,
|
||||
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
|
||||
closeContextMenu: () => void,
|
||||
openLnbcModal: (lnbc: string, onPay: () => void) => void,
|
||||
closeLnbcModal: () => void,
|
||||
openConfirmModal: (confirmInfo: ConfirmInfo) => void,
|
||||
closeConfirmModal: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
@ -65,6 +88,10 @@ const initialData: Omit<AppContextStore, 'actions'> = {
|
||||
customZap: undefined,
|
||||
showNoteContextMenu: false,
|
||||
noteContextMenuInfo: undefined,
|
||||
showLnInvoiceModal: false,
|
||||
lnbc: undefined,
|
||||
showConfirmModal: false,
|
||||
confirmInfo: undefined,
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextStore>();
|
||||
@ -124,6 +151,30 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
updateStore('showNoteContextMenu', () => true);
|
||||
};
|
||||
|
||||
const openLnbcModal = (lnbc: string, onPay: () => void) => {
|
||||
updateStore('showLnInvoiceModal', () => true);
|
||||
updateStore('lnbc', () => ({
|
||||
invoice: lnbc,
|
||||
onPay,
|
||||
onCancel: () => updateStore('showLnInvoiceModal', () => false),
|
||||
}))
|
||||
};
|
||||
|
||||
const closeLnbcModal = () => {
|
||||
updateStore('showLnInvoiceModal', () => false);
|
||||
updateStore('lnbc', () => undefined);
|
||||
};
|
||||
|
||||
const openConfirmModal = (confirmInfo: ConfirmInfo) => {
|
||||
updateStore('showConfirmModal', () => true);
|
||||
updateStore('confirmInfo', () => ({...confirmInfo }));
|
||||
};
|
||||
|
||||
const closeConfirmModal = () => {
|
||||
updateStore('showConfirmModal', () => false);
|
||||
updateStore('confirmInfo', () => undefined);
|
||||
};
|
||||
|
||||
const closeContextMenu = () => {
|
||||
updateStore('showNoteContextMenu', () => false);
|
||||
};
|
||||
@ -172,6 +223,10 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
closeCustomZapModal,
|
||||
openContextMenu,
|
||||
closeContextMenu,
|
||||
openLnbcModal,
|
||||
closeLnbcModal,
|
||||
openConfirmModal,
|
||||
closeConfirmModal,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -65,3 +65,51 @@ export const date = (postTimestamp: number, style: Intl.RelativeTimeFormatStyle
|
||||
|
||||
return { date, label: `${diff}s` };
|
||||
};
|
||||
|
||||
export const dateFuture = (postTimestamp: number, style: Intl.RelativeTimeFormatStyle = 'short', since?: number) => {
|
||||
const today = since ?? Math.floor((new Date()).getTime() / 1000);
|
||||
const date = new Date(postTimestamp * 1000);
|
||||
|
||||
const minute = 60;
|
||||
const hour = minute * 60;
|
||||
const day = hour * 24;
|
||||
const week = day * 7;
|
||||
const month = day * 30;
|
||||
const year = month * 12;
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { style });
|
||||
|
||||
const diff = postTimestamp - today;
|
||||
|
||||
if ( diff > year) {
|
||||
const years = Math.floor(diff / year);
|
||||
return { date, label: rtf.format(-years, 'years').replace(' ago', '') };
|
||||
}
|
||||
|
||||
if (diff > month) {
|
||||
const months = Math.floor(diff / month);
|
||||
return { date, label: rtf.format(-months, 'months').replace(' ago', '') };
|
||||
}
|
||||
|
||||
if (diff > week) {
|
||||
const weeks = Math.floor(diff / week);
|
||||
return { date, label: rtf.format(-weeks, 'weeks').replace(' ago', '') };
|
||||
}
|
||||
|
||||
if (diff > day) {
|
||||
const days = Math.floor(diff / day);
|
||||
return { date, label: rtf.format(-days, 'days').replace(' ago', '') };
|
||||
}
|
||||
|
||||
if (diff > hour) {
|
||||
const hours = Math.floor(diff / hour);
|
||||
return { date, label: rtf.format(-hours, 'hours').replace(' ago', '') };
|
||||
}
|
||||
|
||||
if (diff > minute) {
|
||||
const minutes = Math.floor(diff / minute);
|
||||
return { date, label: rtf.format(-minutes, 'minutes').replace(' ago', '') };
|
||||
}
|
||||
|
||||
return { date, label: `${diff}s` };
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { A } from "@solidjs/router";
|
||||
import { Relay } from "nostr-tools";
|
||||
import { createStore } from "solid-js/store";
|
||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
||||
import { appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants";
|
||||
import { appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, lnRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants";
|
||||
import { sendMessage, subscribeTo } from "../sockets";
|
||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { logError, logInfo, logWarning } from "./logger";
|
||||
@ -60,6 +60,7 @@ export const isNoteMention = (url: string) => noteRegexLocal.test(url);
|
||||
export const isUserMention = (url: string) => profileRegex.test(url);
|
||||
export const isInterpunction = (url: string) => interpunctionRegex.test(url);
|
||||
export const isCustomEmoji = (url: string) => emojiRegex.test(url);
|
||||
export const isLnbc = (url: string) => lnRegex.test(url);
|
||||
|
||||
export const isImage = (url: string) => ['.jpg', '.jpeg', '.webp', '.png', '.gif', '.format=png'].some(x => url.includes(x));
|
||||
export const isMp4Video = (url: string) => ['.mp4', '.mov'].some(x => url.includes(x));
|
||||
|
23
src/lib/sockets.ts
Normal file
23
src/lib/sockets.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NostrEventType, NostrEventContent, NostrEvent, NostrEOSE } from "../types/primal";
|
||||
|
||||
export const subTo = (socket: WebSocket, subId: string, cb: (type: NostrEventType, subId: string, content?: NostrEventContent) => void ) => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
const message: NostrEvent | NostrEOSE = JSON.parse(event.data);
|
||||
const [type, subscriptionId, content] = message;
|
||||
|
||||
if (subId === subscriptionId) {
|
||||
cb(type, subscriptionId, content);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
socket.addEventListener('message', listener);
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', listener);
|
||||
};
|
||||
};
|
||||
|
||||
export const sendMessage = (socket: WebSocket, message: string) => {
|
||||
socket.readyState === WebSocket.OPEN && socket.send(message);
|
||||
}
|
@ -2083,3 +2083,49 @@ export const followWarning = {
|
||||
description: 'Abort forgot pin action',
|
||||
},
|
||||
};
|
||||
|
||||
export const lnInvoice = {
|
||||
pay: {
|
||||
id: 'lnInvoice.pay',
|
||||
defaultMessage: 'Pay',
|
||||
description: 'Pay invoice action',
|
||||
},
|
||||
title: {
|
||||
id: 'lnInvoice.title',
|
||||
defaultMessage: 'Lightning Invoice',
|
||||
description: 'Lightning Invoice title',
|
||||
},
|
||||
expired: {
|
||||
id: 'lnInvoice.expired',
|
||||
defaultMessage: 'Expired: {date} ago',
|
||||
description: 'Expired time',
|
||||
},
|
||||
expires: {
|
||||
id: 'lnInvoice.expires',
|
||||
defaultMessage: 'Expires: in {date}',
|
||||
description: 'Expires time',
|
||||
},
|
||||
confirm: {
|
||||
title: {
|
||||
id: 'lnInvoice.confirm.title',
|
||||
defaultMessage: 'Are you sure?',
|
||||
description: 'Lightning invoice pay confirmation',
|
||||
},
|
||||
description: {
|
||||
id: 'lnInvoice.confirm.description',
|
||||
defaultMessage: 'Pay {amount}',
|
||||
description: 'Lightning Invoice confirm description',
|
||||
},
|
||||
confirmLabel: {
|
||||
id: 'lnInvoice.confirm.confirmLabel',
|
||||
defaultMessage: 'Yes, pay',
|
||||
description: 'Lightning Invoice confirm button label',
|
||||
},
|
||||
abortLabel: {
|
||||
id: 'lnInvoice.confirm.abortLabel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Lightning Invoice confirm button label',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
27
src/types/primal.d.ts
vendored
27
src/types/primal.d.ts
vendored
@ -676,3 +676,30 @@ export type MembershipStatus = {
|
||||
used_storage?: number,
|
||||
expires_on?: number,
|
||||
};
|
||||
|
||||
export type LncbSectionNetwork = {
|
||||
name: 'lightning_network',
|
||||
letters: 'ln',
|
||||
};
|
||||
|
||||
export type LnbcSection = {
|
||||
name: string,
|
||||
letters: string,
|
||||
tag?: string,
|
||||
value?: any
|
||||
};
|
||||
|
||||
export type LnbcRouteHint = {
|
||||
pubkey: string,
|
||||
short_channel_id: string,
|
||||
fee_base_msat: number,
|
||||
fee_proportional_millionths: number,
|
||||
cltv_expiry_delta: number,
|
||||
}
|
||||
|
||||
export type LnbcInvoice = {
|
||||
paymentRequest: string,
|
||||
sections: LnbcSection[],
|
||||
expiry: number,
|
||||
route_hints: LnbcRouteHint[],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user