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",
|
"@thisbeyond/solid-select": "^0.13.0",
|
||||||
"@types/dompurify": "3.0.2",
|
"@types/dompurify": "3.0.2",
|
||||||
"dompurify": "3.0.5",
|
"dompurify": "3.0.5",
|
||||||
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"medium-zoom": "1.0.8",
|
"medium-zoom": "1.0.8",
|
||||||
"nostr-tools": "1.15.0",
|
"nostr-tools": "1.15.0",
|
||||||
"photoswipe": "5.4.3",
|
"photoswipe": "5.4.3",
|
||||||
@ -1734,6 +1735,25 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"@thisbeyond/solid-select": "^0.13.0",
|
"@thisbeyond/solid-select": "^0.13.0",
|
||||||
"@types/dompurify": "3.0.2",
|
"@types/dompurify": "3.0.2",
|
||||||
"dompurify": "3.0.5",
|
"dompurify": "3.0.5",
|
||||||
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"medium-zoom": "1.0.8",
|
"medium-zoom": "1.0.8",
|
||||||
"nostr-tools": "1.15.0",
|
"nostr-tools": "1.15.0",
|
||||||
"photoswipe": "5.4.3",
|
"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 { useAppContext } from '../../contexts/AppContext';
|
||||||
import CustomZap from '../CustomZap/CustomZap';
|
import CustomZap from '../CustomZap/CustomZap';
|
||||||
import NoteContextMenu from '../Note/NoteContextMenu';
|
import NoteContextMenu from '../Note/NoteContextMenu';
|
||||||
|
import LnQrCodeModal from '../LnQrCodeModal/LnQrCodeModal';
|
||||||
|
import ConfirmModal from '../ConfirmModal/ConfirmModal';
|
||||||
|
|
||||||
export const [isHome, setIsHome] = createSignal(false);
|
export const [isHome, setIsHome] = createSignal(false);
|
||||||
|
|
||||||
@ -166,6 +168,23 @@ const Layout: Component = () => {
|
|||||||
onFail={app?.customZap?.onFail}
|
onFail={app?.customZap?.onFail}
|
||||||
onCancel={app?.customZap?.onCancel}
|
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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</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 { truncateNumber } from '../../../lib/notifications';
|
||||||
import { canUserReceiveZaps, zapNote } from '../../../lib/zap';
|
import { canUserReceiveZaps, zapNote } from '../../../lib/zap';
|
||||||
import CustomZap from '../../CustomZap/CustomZap';
|
|
||||||
import { useSettingsContext } from '../../../contexts/SettingsContext';
|
import { useSettingsContext } from '../../../contexts/SettingsContext';
|
||||||
|
|
||||||
import zapMD from '../../../assets/lottie/zap_md.json';
|
import zapMD from '../../../assets/lottie/zap_md.json';
|
||||||
import { toast as t } from '../../../translations';
|
import { toast as t } from '../../../translations';
|
||||||
import PrimalMenu from '../../PrimalMenu/PrimalMenu';
|
import PrimalMenu from '../../PrimalMenu/PrimalMenu';
|
||||||
import { hookForDev } from '../../../lib/devTools';
|
import { hookForDev } from '../../../lib/devTools';
|
||||||
import NoteContextMenu from '../NoteContextMenu';
|
|
||||||
import { getScreenCordinates } from '../../../utils';
|
import { getScreenCordinates } from '../../../utils';
|
||||||
import ZapAnimation from '../../ZapAnimation/ZapAnimation';
|
import ZapAnimation from '../../ZapAnimation/ZapAnimation';
|
||||||
import ReactionsModal from '../../ReactionsModal/ReactionsModal';
|
|
||||||
import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext';
|
import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext';
|
||||||
|
|
||||||
const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> = (props) => {
|
const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> = (props) => {
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
isHashtag,
|
isHashtag,
|
||||||
isImage,
|
isImage,
|
||||||
isInterpunction,
|
isInterpunction,
|
||||||
|
isLnbc,
|
||||||
isMixCloud,
|
isMixCloud,
|
||||||
isMp4Video,
|
isMp4Video,
|
||||||
isNoteMention,
|
isNoteMention,
|
||||||
@ -45,6 +46,7 @@ import { useIntl } from '@cookbook/solid-intl';
|
|||||||
import { actions } from '../../translations';
|
import { actions } from '../../translations';
|
||||||
|
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
|
import Lnbc from '../Lnbc/Lnbc';
|
||||||
|
|
||||||
const groupGridLimit = 7;
|
const groupGridLimit = 7;
|
||||||
|
|
||||||
@ -396,6 +398,12 @@ const ParsedNote: Component<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLnbc(token)) {
|
||||||
|
lastSignificantContent = 'lnbc';
|
||||||
|
updateContent(content, 'lnbc', token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lastSignificantContent = 'text';
|
lastSignificantContent = 'text';
|
||||||
updateContent(content, 'text', token);
|
updateContent(content, 'text', token);
|
||||||
return;
|
return;
|
||||||
@ -1061,6 +1069,18 @@ const ParsedNote: Component<{
|
|||||||
</For>
|
</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 renderContent = (item: NoteContent, index: number) => {
|
||||||
|
|
||||||
const renderers: Record<string, (item: NoteContent, index?: number) => JSXElement> = {
|
const renderers: Record<string, (item: NoteContent, index?: number) => JSXElement> = {
|
||||||
@ -1081,6 +1101,7 @@ const ParsedNote: Component<{
|
|||||||
tagmention: renderTagMention,
|
tagmention: renderTagMention,
|
||||||
hashtag: renderHashtag,
|
hashtag: renderHashtag,
|
||||||
emoji: renderEmoji,
|
emoji: renderEmoji,
|
||||||
|
lnbc: renderLnbc,
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderers[item.type] ?
|
return renderers[item.type] ?
|
||||||
|
@ -380,9 +380,9 @@ const ProfileTabs: Component<{
|
|||||||
<Show
|
<Show
|
||||||
when={!profile?.isFetchingFollowers}
|
when={!profile?.isFetchingFollowers}
|
||||||
fallback={
|
fallback={
|
||||||
<div style="margin-top: 40px;">
|
<div style="margin-top: 40px;">
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<For
|
<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 logoFire from './assets/icons/logo_fire.svg';
|
||||||
import logoIce from './assets/icons/logo_ice.svg';
|
import logoIce from './assets/icons/logo_ice.svg';
|
||||||
|
|
||||||
@ -137,6 +137,8 @@ export enum Kind {
|
|||||||
SuggestedUsersByCategory = 10_000_134,
|
SuggestedUsersByCategory = 10_000_134,
|
||||||
UploadChunk = 10_000_135,
|
UploadChunk = 10_000_135,
|
||||||
UserRelays=10_000_139,
|
UserRelays=10_000_139,
|
||||||
|
|
||||||
|
WALLET_OPERATION = 10_000_300,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const relayConnectingTimeout = 1000;
|
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 urlExtractRegex = /https?:\/\/\S+\.[^()]+(?:\([^)]*\))*/;
|
||||||
export const interpunctionRegex = /^(\.|,|;|\?|\!)$/;
|
export const interpunctionRegex = /^(\.|,|;|\?|\!)$/;
|
||||||
export const emojiRegex = /(?:\s|^)\:\w+\:/;
|
export const emojiRegex = /(?:\s|^)\:\w+\:/;
|
||||||
|
export const lnRegex = /lnbc[a-zA-Z0-9]*/;
|
||||||
|
|
||||||
export const hashtagRegex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/i;
|
export const hashtagRegex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/i;
|
||||||
export const linebreakRegex = /(\r\n|\r|\n)/ig;
|
export const linebreakRegex = /(\r\n|\r|\n)/ig;
|
||||||
@ -386,3 +389,10 @@ export const uploadLimit = {
|
|||||||
regular: 100,
|
regular: 100,
|
||||||
premium: 1024,
|
premium: 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const emptyInvoice: LnbcInvoice = {
|
||||||
|
paymentRequest: '',
|
||||||
|
sections: [],
|
||||||
|
expiry: 0,
|
||||||
|
route_hints: [],
|
||||||
|
};
|
||||||
|
@ -32,6 +32,21 @@ export type NoteContextMenuInfo = {
|
|||||||
openReactions?: () => void,
|
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 = {
|
export type AppContextStore = {
|
||||||
isInactive: boolean,
|
isInactive: boolean,
|
||||||
appState: 'sleep' | 'waking' | 'woke',
|
appState: 'sleep' | 'waking' | 'woke',
|
||||||
@ -41,6 +56,10 @@ export type AppContextStore = {
|
|||||||
customZap: CustomZapInfo | undefined,
|
customZap: CustomZapInfo | undefined,
|
||||||
showNoteContextMenu: boolean,
|
showNoteContextMenu: boolean,
|
||||||
noteContextMenuInfo: NoteContextMenuInfo | undefined,
|
noteContextMenuInfo: NoteContextMenuInfo | undefined,
|
||||||
|
showLnInvoiceModal: boolean,
|
||||||
|
lnbc: LnbcInfo | undefined,
|
||||||
|
showConfirmModal: boolean,
|
||||||
|
confirmInfo: ConfirmInfo | undefined,
|
||||||
actions: {
|
actions: {
|
||||||
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
||||||
closeReactionModal: () => void,
|
closeReactionModal: () => void,
|
||||||
@ -48,6 +67,10 @@ export type AppContextStore = {
|
|||||||
closeCustomZapModal: () => void,
|
closeCustomZapModal: () => void,
|
||||||
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
|
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
|
||||||
closeContextMenu: () => 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,
|
customZap: undefined,
|
||||||
showNoteContextMenu: false,
|
showNoteContextMenu: false,
|
||||||
noteContextMenuInfo: undefined,
|
noteContextMenuInfo: undefined,
|
||||||
|
showLnInvoiceModal: false,
|
||||||
|
lnbc: undefined,
|
||||||
|
showConfirmModal: false,
|
||||||
|
confirmInfo: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppContext = createContext<AppContextStore>();
|
export const AppContext = createContext<AppContextStore>();
|
||||||
@ -124,6 +151,30 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
|||||||
updateStore('showNoteContextMenu', () => true);
|
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 = () => {
|
const closeContextMenu = () => {
|
||||||
updateStore('showNoteContextMenu', () => false);
|
updateStore('showNoteContextMenu', () => false);
|
||||||
};
|
};
|
||||||
@ -172,6 +223,10 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
|||||||
closeCustomZapModal,
|
closeCustomZapModal,
|
||||||
openContextMenu,
|
openContextMenu,
|
||||||
closeContextMenu,
|
closeContextMenu,
|
||||||
|
openLnbcModal,
|
||||||
|
closeLnbcModal,
|
||||||
|
openConfirmModal,
|
||||||
|
closeConfirmModal,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -65,3 +65,51 @@ export const date = (postTimestamp: number, style: Intl.RelativeTimeFormatStyle
|
|||||||
|
|
||||||
return { date, label: `${diff}s` };
|
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 { Relay } from "nostr-tools";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
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 { sendMessage, subscribeTo } from "../sockets";
|
||||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
|
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
|
||||||
import { logError, logInfo, logWarning } from "./logger";
|
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 isUserMention = (url: string) => profileRegex.test(url);
|
||||||
export const isInterpunction = (url: string) => interpunctionRegex.test(url);
|
export const isInterpunction = (url: string) => interpunctionRegex.test(url);
|
||||||
export const isCustomEmoji = (url: string) => emojiRegex.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 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));
|
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',
|
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,
|
used_storage?: number,
|
||||||
expires_on?: 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