Notifications with nostr info (#368)
* Show zap message and pubkey in notifications + show zap request event in invoice view * enhance ui --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
4094adfa4f
commit
67a0de3ea5
|
@ -26,6 +26,11 @@ export async function getInvoice (parent, { id }, { me, models }) {
|
|||
throw new GraphQLError('not ur invoice', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
try {
|
||||
inv.nostr = JSON.parse(inv.desc)
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
return inv
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ export default gql`
|
|||
cancelled: Boolean!
|
||||
confirmedAt: Date
|
||||
satsReceived: Int
|
||||
nostr: JSONObject
|
||||
}
|
||||
|
||||
type Withdrawl {
|
||||
|
|
|
@ -1,18 +1,43 @@
|
|||
import AccordianItem from './accordian-item'
|
||||
import Qr from './qr'
|
||||
|
||||
export function Invoice ({ invoice }) {
|
||||
let variant = 'default'
|
||||
let status = 'waiting for you'
|
||||
let webLn = true
|
||||
if (invoice.confirmedAt) {
|
||||
variant = 'confirmed'
|
||||
status = `${invoice.satsReceived} sats deposited`
|
||||
webLn = false
|
||||
} else if (invoice.cancelled) {
|
||||
variant = 'failed'
|
||||
status = 'cancelled'
|
||||
webLn = false
|
||||
} else if (invoice.expiresAt <= new Date()) {
|
||||
variant = 'failed'
|
||||
status = 'expired'
|
||||
webLn = false
|
||||
}
|
||||
|
||||
return <Qr webLn value={invoice.bolt11} statusVariant={variant} status={status} />
|
||||
const { nostr } = invoice
|
||||
|
||||
return (
|
||||
<>
|
||||
<Qr webLn={webLn} value={invoice.bolt11} statusVariant={variant} status={status} />
|
||||
<div className='w-100'>
|
||||
{nostr
|
||||
? <AccordianItem
|
||||
header='Nostr Zap Request'
|
||||
body={
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(nostr, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.login {
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -21,6 +21,9 @@ import { useServiceWorker } from './serviceworker'
|
|||
import { Checkbox, Form } from './form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useData } from './use-data'
|
||||
import { nostrZapDetails } from '../lib/nostr'
|
||||
import Text from './text'
|
||||
import NostrIcon from '../svgs/nostr.svg'
|
||||
|
||||
function Notification ({ n, fresh }) {
|
||||
const type = n.__typename
|
||||
|
@ -30,7 +33,7 @@ function Notification ({ n, fresh }) {
|
|||
{
|
||||
(type === 'Earn' && <EarnNotification n={n} />) ||
|
||||
(type === 'Invitification' && <Invitification n={n} />) ||
|
||||
(type === 'InvoicePaid' && <InvoicePaid n={n} />) ||
|
||||
(type === 'InvoicePaid' && (n.invoice.nostr ? <NostrZap n={n} /> : <InvoicePaid n={n} />)) ||
|
||||
(type === 'Referral' && <Referral n={n} />) ||
|
||||
(type === 'Streak' && <Streak n={n} />) ||
|
||||
(type === 'Votification' && <Votification n={n} />) ||
|
||||
|
@ -187,6 +190,30 @@ function Invitification ({ n }) {
|
|||
)
|
||||
}
|
||||
|
||||
function NostrZap ({ n }) {
|
||||
const { nostr } = n.invoice
|
||||
const { npub, content, note } = nostrZapDetails(nostr)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='fw-bold text-nostr ms-2 py-1'>
|
||||
<NostrIcon width={24} height={24} className='fill-nostr me-1' />{n.earnedSats} sats zap from
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/p/${npub}`} rel='noreferrer'>
|
||||
{npub.slice(0, 10)}...
|
||||
</Link>
|
||||
on {note
|
||||
? (
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/e/${note}`} rel='noreferrer'>
|
||||
{note.slice(0, 12)}...
|
||||
</Link>)
|
||||
: 'nostr'}
|
||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||
{content && <small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'><Text>{content}</Text></small>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InvoicePaid ({ n }) {
|
||||
return (
|
||||
<div className='fw-bold text-info ms-2 py-1'>
|
||||
|
|
|
@ -80,6 +80,7 @@ export const NOTIFICATIONS = gql`
|
|||
earnedSats
|
||||
invoice {
|
||||
id
|
||||
nostr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export const INVOICE = gql`
|
|||
cancelled
|
||||
confirmedAt
|
||||
expiresAt
|
||||
nostr
|
||||
}
|
||||
}`
|
||||
|
||||
|
|
24
lib/nostr.js
24
lib/nostr.js
|
@ -1,3 +1,27 @@
|
|||
import { bech32 } from 'bech32'
|
||||
|
||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||
export const NOSTR_MAX_RELAY_NUM = 20
|
||||
export const NOSTR_ZAPPLE_PAY_NPUB = 'npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan'
|
||||
|
||||
export function hexToBech32 (hex, prefix = 'npub') {
|
||||
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
||||
}
|
||||
|
||||
export function nostrZapDetails (zap) {
|
||||
let { pubkey, content, tags } = zap
|
||||
let npub = hexToBech32(pubkey)
|
||||
if (npub === NOSTR_ZAPPLE_PAY_NPUB) {
|
||||
const znpub = content.match(/^From: nostr:(npub1[02-9ac-hj-np-z]+)$/)?.[1]
|
||||
if (znpub) {
|
||||
npub = znpub
|
||||
// zapple pay does not support user content
|
||||
content = null
|
||||
}
|
||||
}
|
||||
const event = tags.filter(t => t?.length >= 2 && t[0] === 'e')?.[0]?.[1]
|
||||
const note = event ? hexToBech32(event, 'note') : null
|
||||
|
||||
return { npub, content, note }
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@
|
|||
"remark-gfm": "^3.0.1",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"sass": "^1.64.1",
|
||||
"tldts": "^6.0.13",
|
||||
"serviceworker-storage": "^0.1.0",
|
||||
"tldts": "^6.0.13",
|
||||
"typescript": "^5.1.6",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"url-unshort": "^6.1.0",
|
||||
|
|
|
@ -608,6 +608,10 @@ div[contenteditable]:focus,
|
|||
fill: var(--theme-grey);
|
||||
}
|
||||
|
||||
.fill-nostr {
|
||||
fill: var(--bs-nostr);
|
||||
}
|
||||
|
||||
.fill-lgrey {
|
||||
fill: #a5a5a5;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue