snort/src/Element/Zap.tsx

141 lines
3.8 KiB
TypeScript
Raw Normal View History

2023-02-03 21:38:14 +00:00
import "./Zap.css";
2023-02-04 10:28:13 +00:00
import { useMemo } from "react";
2023-02-03 23:45:04 +00:00
import { useSelector } from "react-redux";
2023-02-03 21:38:14 +00:00
// @ts-expect-error
import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bytesToHex } from "@noble/hashes/utils";
2023-02-04 13:30:05 +00:00
import { sha256 } from "Util";
2023-02-03 21:38:14 +00:00
2023-02-04 10:16:08 +00:00
//import { sha256 } from "Util";
2023-02-03 21:38:14 +00:00
import { formatShort } from "Number";
import { HexKey, TaggedRawEvent } from "Nostr";
import Event from "Nostr/Event";
import Text from "Element/Text";
import ProfileImage from "Element/ProfileImage";
2023-02-03 23:45:04 +00:00
import { RootState } from "State/Store";
2023-02-03 21:38:14 +00:00
function findTag(e: TaggedRawEvent, tag: string) {
const maybeTag = e.tags.find((evTag) => {
return evTag[0] === tag
})
return maybeTag && maybeTag[1]
}
function getInvoice(zap: TaggedRawEvent) {
const bolt11 = findTag(zap, 'bolt11')
const decoded = invoiceDecode(bolt11)
const amount = decoded.sections.find((section: any) => section.name === 'amount')?.value
const hash = decoded.sections.find((section: any) => section.name === 'description_hash')?.value;
return { amount, hash: hash ? bytesToHex(hash) : undefined };
}
2023-02-04 13:30:05 +00:00
interface Zapper {
pubkey?: HexKey,
isValid: boolean
}
function getZapper(zap: TaggedRawEvent, dhash: string): Zapper {
2023-02-03 21:38:14 +00:00
const zapRequest = findTag(zap, 'description')
if (zapRequest) {
const rawEvent: TaggedRawEvent = JSON.parse(zapRequest);
if (Array.isArray(rawEvent)) {
// old format, ignored
2023-02-04 13:30:05 +00:00
return { isValid: false };
2023-02-03 21:38:14 +00:00
}
2023-02-04 13:30:05 +00:00
const metaHash = sha256(zapRequest);
2023-02-03 21:38:14 +00:00
const ev = new Event(rawEvent)
2023-02-04 13:30:05 +00:00
return { pubkey: ev.PubKey, isValid: dhash === metaHash };
2023-02-03 21:38:14 +00:00
}
2023-02-04 13:30:05 +00:00
return { isValid: false }
2023-02-03 21:38:14 +00:00
}
interface ParsedZap {
id: HexKey
e?: HexKey
p: HexKey
amount: number
content: string
zapper?: HexKey
valid: boolean
}
export function parseZap(zap: TaggedRawEvent): ParsedZap {
const { amount, hash } = getInvoice(zap)
2023-02-04 13:30:05 +00:00
const zapper = hash ? getZapper(zap, hash) : { isValid: false };
2023-02-03 21:38:14 +00:00
const e = findTag(zap, 'e')
const p = findTag(zap, 'p')!
return {
id: zap.id,
e,
p,
amount: Number(amount) / 1000,
2023-02-04 13:30:05 +00:00
zapper: zapper.pubkey,
2023-02-03 21:38:14 +00:00
content: zap.content,
2023-02-04 13:30:05 +00:00
valid: zapper.isValid,
2023-02-03 21:38:14 +00:00
}
}
2023-02-04 10:16:08 +00:00
const Zap = ({ zap, showZapped = true }: { zap: ParsedZap, showZapped?: boolean }) => {
2023-02-03 23:35:57 +00:00
const { amount, content, zapper, valid, p } = zap
2023-02-03 23:45:04 +00:00
const pubKey = useSelector((s: RootState) => s.login.publicKey)
2023-02-03 21:38:14 +00:00
return valid ? (
2023-02-03 23:35:57 +00:00
<div className="zap note card">
<div className="header">
2023-02-04 10:28:13 +00:00
{zapper ? <ProfileImage pubkey={zapper} /> : <div>Anon&nbsp;</div>}
2023-02-04 10:16:08 +00:00
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
2023-02-03 21:38:14 +00:00
<div className="amount">
<span className="amount-number">{formatShort(amount)}</span> sats
</div>
</div>
<div className="body">
2023-02-04 13:30:05 +00:00
<Text
creator={zapper || ""}
content={content}
tags={[]}
users={new Map()}
/>
2023-02-03 21:38:14 +00:00
</div>
</div>
) : null
}
interface ZapsSummaryProps { zaps: ParsedZap[] }
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
2023-02-04 10:28:13 +00:00
const sortedZaps = useMemo(() => {
2023-02-04 13:30:05 +00:00
const pub = [...zaps.filter(z => z.zapper)]
const priv = [...zaps.filter(z => !z.zapper)]
2023-02-04 10:28:13 +00:00
pub.sort((a, b) => b.amount - a.amount)
return pub.concat(priv)
}, [zaps])
if (zaps.length === 0) {
return null
}
const [topZap, ...restZaps] = sortedZaps
2023-02-03 21:38:14 +00:00
const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0)
2023-02-04 10:28:13 +00:00
const { zapper, amount, content, valid } = topZap
2023-02-03 21:38:14 +00:00
return (
<div className="zaps-summary">
2023-02-04 10:28:13 +00:00
{amount && (
2023-02-03 21:38:14 +00:00
<div className={`top-zap`}>
<div className="summary">
2023-02-04 15:25:37 +00:00
{zapper && <ProfileImage pubkey={zapper} />}
{restZaps.length > 0 && (
<span>and {restZaps.length} other{restZaps.length > 1 ? 's' : ''}</span>
)}
<span>&nbsp;zapped</span>
2023-02-03 21:38:14 +00:00
</div>
</div>
)}
</div>
)
}
export default Zap