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:
ekzyis 2023-08-08 20:19:31 +02:00 committed by GitHub
parent 4094adfa4f
commit 67a0de3ea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 92 additions and 4 deletions

View File

@ -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
}

View File

@ -22,6 +22,7 @@ export default gql`
cancelled: Boolean!
confirmedAt: Date
satsReceived: Int
nostr: JSONObject
}
type Withdrawl {

View File

@ -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>
</>
)
}

View File

@ -1,5 +1,5 @@
.login {
justify-content: start;
justify-content: flex-start;
align-items: center;
display: flex;
flex-direction: column;

View File

@ -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'>

View File

@ -80,6 +80,7 @@ export const NOTIFICATIONS = gql`
earnedSats
invoice {
id
nostr
}
}
}

View File

@ -10,6 +10,7 @@ export const INVOICE = gql`
cancelled
confirmedAt
expiresAt
nostr
}
}`

View File

@ -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 }
}

2
package-lock.json generated
View File

@ -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",

View File

@ -608,6 +608,10 @@ div[contenteditable]:focus,
fill: var(--theme-grey);
}
.fill-nostr {
fill: var(--bs-nostr);
}
.fill-lgrey {
fill: #a5a5a5;
}