diff --git a/CHANGELOG.md b/CHANGELOG.md index 591a14df..d9545ec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.2.15 + +- [x] Add zaps + ## 0.2.14 - [x] Improve paste support diff --git a/ROADMAP.md b/ROADMAP.md index b9fa1410..e9ce8f18 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,6 @@ # Current +- [ ] Try adding boxes/separation on feeds based on user feedback - [ ] Strip zero width spaces from compose - [ ] Fix iOS/safari/firefox - [ ] Make the note relays button modal make sense, one relay with no explanation is not good @@ -13,13 +14,6 @@ - https://github.com/seaweedfs/seaweedfs - https://github.com/cubefs/cubefs -# Lightning - -- [ ] Linkify invoices -- [ ] Linkify bech32 entities w/ NIP 21 https://github.com/nostr-protocol/nips/blob/master/21.md -- [ ] Support invoices, tips, zaps https://twitter.com/jb55/status/1604131336247476224 - - nevent1qqsd0x0xzfwtppu0n52ngw0zhynlwv0sjsr77aflcpufms2wrl3v8mspr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqs7amnwvaz7tmwdaehgu3wd4hk6d7ewgp - # Custom views - [ ] Add customize icon and route with editable custom view cards using "lists" nip @@ -28,6 +22,15 @@ # More +- [ ] Linkify invoices +- [ ] Linkify bech32 entities w/ NIP 21 https://github.com/nostr-protocol/nips/blob/master/21.md +- [ ] Person zaps +- [ ] Add dynamic title tag +- [ ] Collapsible thread view +- [ ] Split inbox into replies + everything else +- [ ] Show more link on long notes +- [ ] Show popover on delayed hover rather than click (on mobile, keep it click) +- [ ] Light mode - [ ] Mute threads http://localhost:5173/nevent1qqsyz8x6r0cu7l6vwlcjhf8qhxyjtdykvuervkc3t3mfggse4qtwt0gpyfmhxue69uhkummnw3ezumrfvfjhyarpwdc8y6tddaexg6t4d5hxxmmdnhxvea - [ ] Add webtorrent support - https://coracle.social/nevent1qqsxgxcsq5vevy4wdty5z5v88nhwp2fc5qgl0ws5rmamn6z72hwv3qcpyfmhxue69uhkummnw3ez6an9wf5kv6t9vsh8wetvd3hhyer9wghxuet5qk6c9q diff --git a/package-lock.json b/package-lock.json index 8a0558ff..0b9816c3 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index e2dca72c..87e1eb47 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,14 @@ "@fortawesome/fontawesome-free": "^6.2.1", "@noble/secp256k1": "^1.7.0", "@tsconfig/svelte": "^3.0.0", + "bech32": "^2.0.0", + "bolt11": "^1.4.0", "classnames": "^2.3.2", "compressorjs": "^1.1.1", "fuse.js": "^6.6.2", "hurdak": "github:ConsignCloud/hurdak", "husky": "^8.0.3", + "js-lnurl": "^0.5.1", "localforage": "^1.10.0", "localforage-memoryStorageDriver": "^0.9.2", "nostr-tools": "^1.4.1", diff --git a/src/agent/cmd.ts b/src/agent/cmd.ts index 9fac9dcd..0971a02f 100644 --- a/src/agent/cmd.ts +++ b/src/agent/cmd.ts @@ -93,6 +93,16 @@ const createReply = (note, content, mentions = [], topics = []) => { return new PublishableEvent(1, {content, tags}) } +const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => { + const tags = [["relays", ...relays], ["amount", amount], ["lnurl", lnurl], ["p", pubkey]] + + if (eventId) { + tags.push(["e", eventId]) + } + + return new PublishableEvent(9734, {content, tags}) +} + const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id])}) @@ -119,5 +129,5 @@ class PublishableEvent { export default { updateUser, setRelays, setPetnames, muffle, createRoom, updateRoom, createChatMessage, createDirectMessage, createNote, createReaction, - createReply, deleteEvent, + createReply, requestZap, deleteEvent, PublishableEvent, } diff --git a/src/agent/network.ts b/src/agent/network.ts index ae79e34d..d1cc43e6 100644 --- a/src/agent/network.ts +++ b/src/agent/network.ts @@ -1,5 +1,5 @@ import type {MyEvent} from 'src/util/types' -import {partition, assoc, uniq, uniqBy, prop, propEq, reject, groupBy, pluck} from 'ramda' +import {assoc, uniq, uniqBy, prop, propEq, reject, groupBy, pluck} from 'ramda' import {personKinds, findReplyId} from 'src/util/nostr' import {log} from 'src/util/logger' import {chunk} from 'hurdak/lib/hurdak' @@ -148,7 +148,7 @@ const streamContext = ({notes, onChunk, depth = 0}) => while (events.length > 0 && depth > 0) { const chunk = events.splice(0) const authors = getStalePubkeys(pluck('pubkey', chunk)) - const filter = [{kinds: [1, 7], '#e': pluck('id', chunk)}] as Array + const filter = [{kinds: [1, 7, 9735], '#e': pluck('id', chunk)}] as Array const relays = sampleRelays(aggregateScores(chunk.map(getRelaysForEventChildren))) // Load authors and reactions in one subscription @@ -171,22 +171,26 @@ const streamContext = ({notes, onChunk, depth = 0}) => ) const applyContext = (notes, context) => { - const [replies, reactions] = partition( - propEq('kind', 1), - context.map(assoc('isContext', true)) - ) + context = context.map(assoc('isContext', true)) + + const replies = context.filter(propEq('kind', 1)) + const reactions = context.filter(propEq('kind', 7)) + const zaps = context.filter(propEq('kind', 9735)) const repliesByParentId = groupBy(findReplyId, replies) const reactionsByParentId = groupBy(findReplyId, reactions) + const zapsByParentId = groupBy(findReplyId, zaps) - const annotate = ({replies = [], reactions = [], ...note}) => { + const annotate = ({replies = [], reactions = [], zaps = [], ...note}) => { const combinedReplies = replies.concat(repliesByParentId[note.id] || []) const combinedReactions = reactions.concat(reactionsByParentId[note.id] || []) + const combinedZaps = zaps.concat(zapsByParentId[note.id] || []) return { ...note, replies: uniqBy(prop('id'), combinedReplies).map(annotate), reactions: uniqBy(prop('id'), combinedReactions), + zaps: uniqBy(prop('id'), combinedZaps), } } diff --git a/src/agent/sync.ts b/src/agent/sync.ts index 53346967..a551e867 100644 --- a/src/agent/sync.ts +++ b/src/agent/sync.ts @@ -1,8 +1,9 @@ import {uniq, pick, identity, isEmpty} from 'ramda' import {nip05} from 'nostr-tools' +import {getParams} from 'js-lnurl' import {noop, createMap, ensurePlural, chunk, switcherFn} from 'hurdak/lib/hurdak' import {log} from 'src/util/logger' -import {now, sleep, tryJson, timedelta, shuffle, hash} from 'src/util/misc' +import {hexToBech32, now, sleep, tryJson, timedelta, shuffle, hash} from 'src/util/misc' import {Tags, roomAttrs, personKinds, isRelay, isShareableRelay, normalizeRelayUrl} from 'src/util/nostr' import database from 'src/agent/database' @@ -45,8 +46,10 @@ const processProfileEvents = async events => { if (e.created_at > (person.kind0_updated_at || 0)) { if (kind0.nip05) { verifyNip05(e.pubkey, kind0.nip05) + } - kind0.nip05_updated_at = e.created_at + if (kind0.lud16 || kind0.lud06) { + verifyZapper(e.pubkey, kind0.lud16 || kind0.lud06) } return { @@ -304,4 +307,29 @@ const verifyNip05 = (pubkey, as) => } }, noop) +const verifyZapper = async (pubkey, address) => { + // Try to parse it as a lud06 LNURL + let zapper = await getParams(address) as any + let lnurl = address + + // If that failed, try to parse it as a lud16 address + if (zapper.status === 'ERROR' && address.includes('@')) { + const [name, domain] = address.split('@') + + if (!domain || !name) { + return + } + + const url = `https://${domain}/.well-known/lnurlp/${name}` + const res = await fetch(url) + + zapper = await res.json() + lnurl = hexToBech32('lnurl', url) + } + + if (zapper?.allowsNostr && zapper?.nostrPubkey) { + database.people.patch({pubkey, zapper, lnurl}) + } +} + export default {processEvents} diff --git a/src/agent/user.ts b/src/agent/user.ts index 945382b7..38566f22 100644 --- a/src/agent/user.ts +++ b/src/agent/user.ts @@ -25,6 +25,7 @@ const anonRelays = synced('agent/user/anonRelays', []) const settings = synced("agent/user/settings", { relayLimit: 20, + defaultZap: 21, showMedia: true, reportAnalytics: true, dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, @@ -91,6 +92,7 @@ const user = { canPublish, getProfile: () => profileCopy, getPubkey: () => profileCopy?.pubkey, + canZap: () => profileCopy?.zapper, muffle: events => { const muffle = user.getMuffle() diff --git a/src/partials/Input.svelte b/src/partials/Input.svelte index b09fb179..cf3be648 100644 --- a/src/partials/Input.svelte +++ b/src/partials/Input.svelte @@ -1,8 +1,9 @@ -
- + {#if $$slots.before}
{/if} {#if $$slots.after} -
+
{/if} diff --git a/src/util/misc.ts b/src/util/misc.ts index d94be990..f0f4e608 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -1,8 +1,10 @@ +import {bech32} from 'bech32' +import {Buffer} from 'buffer' import {debounce, throttle} from 'throttle-debounce' import {aperture, path as getPath, allPass, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda" import Fuse from "fuse.js/dist/fuse.min.js" import {writable} from 'svelte/store' -import {isObject} from 'hurdak/lib/hurdak' +import {isObject, round} from 'hurdak/lib/hurdak' import {warn} from 'src/util/logger' export const fuzzy = (data, opts = {}) => { @@ -327,3 +329,18 @@ export const uploadFile = (url, fileObj) => { return fetchJson(url, {method: 'POST', body}) } + +export const hexToBech32 = (prefix, hex) => + bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex'))) + +export const bech32ToHex = b32 => + Buffer.from(bech32.fromWords(bech32.decode(b32).words)).toString('hex') + +export const formatSats = sats => { + const formatter = new Intl.NumberFormat() + + if (sats < 1_000) return formatter.format(sats) + if (sats < 1_000_000) return formatter.format(round(1, sats / 1000)) + 'K' + if (sats < 100_000_000) return formatter.format(round(1, sats / 1_000_000)) + 'MM' + return formatter.format(round(2, sats / 100_000_000)) + 'BTC' +} diff --git a/src/util/nostr.ts b/src/util/nostr.ts index 001f61ba..023c743e 100644 --- a/src/util/nostr.ts +++ b/src/util/nostr.ts @@ -1,5 +1,5 @@ import type {DisplayEvent} from 'src/util/types' -import {last, identity, objOf, prop, flatten, uniq} from 'ramda' +import {fromPairs, last, identity, objOf, prop, flatten, uniq} from 'ramda' import {nip19} from 'nostr-tools' import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak' @@ -31,6 +31,9 @@ export class Tags { pubkeys() { return this.type("p").values().all() } + asMeta() { + return fromPairs(this.tags) + } values() { return new Tags(this.tags.map(t => t[1])) } @@ -118,7 +121,7 @@ export const normalizeRelayUrl = url => url.replace(/\/+$/, '').toLowerCase().tr export const roomAttrs = ['name', 'about', 'picture'] export const asDisplayEvent = event => - ({replies: [], reactions: [], ...event}) as DisplayEvent + ({replies: [], reactions: [], zaps: [], ...event}) as DisplayEvent export const toHex = (data: string): string | null => { try { diff --git a/src/util/types.ts b/src/util/types.ts index f77c7b04..371a574f 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -28,6 +28,7 @@ export type MyEvent = Event & { export type DisplayEvent = MyEvent & { replies: Array reactions: Array + zaps: Array } export type Room = { diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index 8ad60a4c..e67e1608 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -46,6 +46,15 @@ in any note.

+
+
+ Default zap amount + +
+

+ The default amount of sats to use when sending a lightning tip. +

+
Max relays per request diff --git a/src/views/notes/Note.svelte b/src/views/notes/Note.svelte index 4a2af146..9071a8e8 100644 --- a/src/views/notes/Note.svelte +++ b/src/views/notes/Note.svelte @@ -1,15 +1,20 @@ @@ -282,12 +395,20 @@
-
+
+
+