diff --git a/src/App.svelte b/src/App.svelte index e8d343ac..7b14c711 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -12,13 +12,13 @@ import {hasParent} from 'src/util/html' import {timedelta} from 'src/util/misc' import {store as toast} from "src/state/toast" - import {channels, epoch} from "src/state/nostr" + import {channels} from "src/state/nostr" import {modal, logout, alerts} from "src/state/app" import {user} from 'src/state/user' import Anchor from 'src/partials/Anchor.svelte' import NoteDetail from "src/partials/NoteDetail.svelte" import NotFound from "src/routes/NotFound.svelte" - import Search from "src/routes/Search.svelte" + // import Search from "src/routes/Search.svelte" import Alerts from "src/routes/Alerts.svelte" import Notes from "src/routes/Notes.svelte" import Login from "src/routes/Login.svelte" @@ -27,12 +27,12 @@ import Keys from "src/routes/Keys.svelte" import RelayList from "src/routes/RelayList.svelte" import AddRelay from "src/routes/AddRelay.svelte" - import UserDetail from "src/routes/UserDetail.svelte" - import UserAdvanced from "src/routes/UserAdvanced.svelte" + // import UserDetail from "src/routes/UserDetail.svelte" + // import UserAdvanced from "src/routes/UserAdvanced.svelte" import NoteCreate from "src/routes/NoteCreate.svelte" - import Chat from "src/routes/Chat.svelte" - import ChatRoom from "src/routes/ChatRoom.svelte" - import ChatEdit from "src/routes/ChatEdit.svelte" + // import Chat from "src/routes/Chat.svelte" + // import ChatRoom from "src/routes/ChatRoom.svelte" + // import ChatEdit from "src/routes/ChatEdit.svelte" const menuIsOpen = writable(false) const toggleMenu = () => menuIsOpen.update(x => !x) @@ -43,7 +43,7 @@ let menuIcon let scrollY let suspendedSubs = [] - let mostRecentAlert = epoch + let mostRecentAlert = 0 export let url = "" @@ -96,13 +96,16 @@
+ + diff --git a/src/nostr/tags.js b/src/nostr/tags.js deleted file mode 100644 index 4ece7b3b..00000000 --- a/src/nostr/tags.js +++ /dev/null @@ -1,27 +0,0 @@ -import {last} from 'ramda' -import {ensurePlural, first} from 'hurdak/lib/hurdak' - -export const filterTags = (where, events) => - ensurePlural(events) - .flatMap( - e => e.tags.filter(t => { - if (where.tag && where.tag !== t[0]) { - return false - } - - if (where.type && where.type !== last(t)) { - return false - } - - return true - }).map(t => t[1]) - ) - -export const findTag = (where, events) => first(filterTags(where, events)) - -// Support the deprecated version where tags are not marked as replies -export const findReply = e => - findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e) - -export const findRoot = e => - findTag({tag: "e", type: "root"}, e) diff --git a/src/partials/Note.svelte b/src/partials/Note.svelte index 00698040..06106da3 100644 --- a/src/partials/Note.svelte +++ b/src/partials/Note.svelte @@ -4,13 +4,12 @@ import {slide} from 'svelte/transition' import {navigate} from 'svelte-routing' import {hasParent, findLink} from 'src/util/html' - import {renderNote} from 'src/util/notes' + import {findReply} from "src/util/nostr" import Preview from 'src/partials/Preview.svelte' import Anchor from 'src/partials/Anchor.svelte' import relay from 'src/relay' import {dispatch} from "src/state/dispatch" - import {findReply} from "src/state/nostr" - import {accounts, settings, modal} from "src/state/app" + import {settings, modal} from "src/state/app" import {user} from "src/state/user" import {formatTimestamp} from 'src/util/misc' import UserBadge from "src/partials/UserBadge.svelte" @@ -32,6 +31,7 @@ const likes = liveQuery(() => relay.countReactions(note.id, {content: "+"})) const flags = liveQuery(() => relay.countReactions(note.id, {content: "-"})) const replies = liveQuery(() => relay.filterReplies(note.id)) + const account = liveQuery(() => relay.db.users.get(note.pubkey)) relay.ensureContext(note) @@ -98,7 +98,7 @@
- +

{formatTimestamp(note.created_at)}

@@ -114,7 +114,10 @@

{:else}

- {@html renderNote(note, {showEntire})} + {#await relay.renderNote(note, {showEntire})} + {:then content} + {@html content} + {/await} {#if link}

e.stopPropagation()}> diff --git a/src/partials/Notes.svelte b/src/partials/Notes.svelte index 9c4caa9f..8718f253 100644 --- a/src/partials/Notes.svelte +++ b/src/partials/Notes.svelte @@ -1,8 +1,7 @@ - -
    {#each ($notes || []) as n (n.id)}
  • - {:else} -
  • No notes found.
  • {/each}
+ +{#if $notes?.length === 0} +
+
+ No notes found. +
+
+{/if} diff --git a/src/relay/db.js b/src/relay/db.js index 72255868..7f25c5a0 100644 --- a/src/relay/db.js +++ b/src/relay/db.js @@ -1,7 +1,5 @@ import Dexie from 'dexie' -import {defmulti} from 'hurdak/lib/hurdak' -import {filterTags} from 'src/nostr/tags' -import {worker} from 'src/relay/worker' +import {filterTags} from 'src/util/nostr' export const db = new Dexie('coracle/relay') @@ -36,13 +34,3 @@ db.events.hook('creating', (id, e, t) => { } }) }) - -// Listen to worker - -const withPayload = f => e => f(e.data.payload) - -worker.onmessage = defmulti('onmessage', e => e.data.topic) - -worker.onmessage.addMethod('events/put', withPayload(e => { - db.events.put(e) -})) diff --git a/src/relay/index.js b/src/relay/index.js index 909032af..6b0b3858 100644 --- a/src/relay/index.js +++ b/src/relay/index.js @@ -1,9 +1,11 @@ import {liveQuery} from 'dexie' import {pluck, isNil} from 'ramda' -import {ensurePlural, first} from 'hurdak/lib/hurdak' +import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak' import {now, timedelta} from 'src/util/misc' +import {escapeHtml} from 'src/util/html' +import {filterTags} from 'src/util/nostr' import {db} from 'src/relay/db' -import {worker} from 'src/relay/worker' +import pool from 'src/relay/pool' // Livequery appears to swallow errors const lq = f => liveQuery(async () => { @@ -20,11 +22,11 @@ const ensureContext = async e => { // Throttle updates for users if (!user || user.updated_at < now() - timedelta(1, 'hours')) { - worker.post('user/update', user || {pubkey: e.pubkey, updated_at: 0}) + await pool.updateUser(user || {pubkey: e.pubkey, updated_at: 0}) } // TODO optimize this like user above so we're not double-fetching - worker.post('event/fetchContext', e) + await pool.fetchContext(e) } const prefilterEvents = filter => { @@ -77,7 +79,31 @@ const findReaction = async (id, filter) => const countReactions = async (id, filter) => (await filterReactions(id, filter)).length -export default { - db, worker, lq, ensureContext, filterEvents, filterReactions, countReactions, - findReaction, filterReplies, +const renderNote = async (note, {showEntire = false}) => { + const shouldEllipsize = note.content.length > 500 && !showEntire + const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content + const accounts = await db.users.where('pubkey').anyOf(filterTags({tag: "p"}, note)).toArray() + const accountsByPubkey = createMap('pubkey', accounts) + + return escapeHtml(content) + .replace(/\n/g, '
') + .replace(/https?:\/\/([\w.-]+)[^ ]*/g, (url, domain) => { + return `${domain}` + }) + .replace(/#\[(\d+)\]/g, (tag, i) => { + if (!note.tags[parseInt(i)]) { + return tag + } + + const pubkey = note.tags[parseInt(i)][1] + const user = accountsByPubkey[pubkey] + const name = user?.name || pubkey.slice(0, 8) + + return `@${name}` + }) +} + +export default { + db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions, + findReaction, filterReplies, renderNote, } diff --git a/src/worker/index.js b/src/relay/pool.js similarity index 63% rename from src/worker/index.js rename to src/relay/pool.js index 9d7246f3..1ba8fe56 100644 --- a/src/worker/index.js +++ b/src/relay/pool.js @@ -1,7 +1,8 @@ -import {relayPool} from 'nostr-tools' -import {defmulti, noop, uuid} from 'hurdak/lib/hurdak' +import {relayPool, getPublicKey} from 'nostr-tools' +import {noop, switcherFn, uuid} from 'hurdak/lib/hurdak' import {now, timedelta} from "src/util/misc" -import {filterTags} from "src/nostr/tags" +import {filterTags} from "src/util/nostr" +import {db} from 'src/relay/db' // ============================================================================ // Utils/config @@ -49,24 +50,24 @@ req({ // ============================================================================ // Listen to messages posted from the main application -const withPayload = f => e => f(e.data.payload) +const getPubkey = () => { + return pool._pubkey || getPublicKey(pool._privkey) +} -onmessage = defmulti('self', e => e.data.topic) - -onmessage.addMethod('pool/addRelay', withPayload(url => { +const addRelay = url => { pool.addRelay(url) -})) +} -onmessage.addMethod('pool/removeRelay', withPayload(url => { +const removeRelay = url => { pool.removeRelay(url) -})) +} -onmessage.addMethod('pool/setPrivateKey', withPayload(privkey => { +const setPrivateKey = privkey => { pool.setPrivateKey(privkey) pool._privkey = privkey -})) +} -onmessage.addMethod('pool/setPublicKey', withPayload(pubkey => { +const setPublicKey = pubkey => { // TODO fix this, it ain't gonna work pool.registerSigningFunction(async event => { const {sig} = await window.nostr.signEvent(event) @@ -75,35 +76,41 @@ onmessage.addMethod('pool/setPublicKey', withPayload(pubkey => { }) pool._pubkey = pubkey -})) +} -onmessage.addMethod('event/publish', withPayload(event => { +const publishEvent = event => { pool.publish(event) - post('events/put', event) -})) + db.events.put(event) +} -onmessage.addMethod('user/update', withPayload(async user => { +const updateUser = async user => { if (!user.pubkey) throw new Error("Invalid user") + user = {muffle: [], petnames: [], ...user} + const sub = req({ - filter: {kinds: [0], authors: [user.pubkey], since: user.updated_at}, + filter: { + kinds: [0, 3, 12165], + authors: [user.pubkey], + since: user.updated_at, + }, onEvent: e => { - try { - Object.assign(user, JSON.parse(e.content)) - } catch (e) { - // pass - } + switcherFn(e.kind, { + 0: () => Object.assign(user, JSON.parse(e.content)), + 3: () => Object.assign(user, {petnames: e.tags}), + 12165: () => Object.assign(user, {muffle: e.tags}), + }) }, onEose: () => { sub.unsub() - post('users/put', {...user, updated_at: now()}) + db.users.put({...user, updated_at: now()}) }, }) -})) +} -onmessage.addMethod('event/fetchContext', withPayload(async event => { +const fetchContext = async event => { const sub = req({ filter: [ {kinds: [5, 7], '#e': [event.id]}, @@ -112,4 +119,9 @@ onmessage.addMethod('event/fetchContext', withPayload(async event => { onEvent: e => post('events/put', e), onEose: () => sub.unsub(), }) -})) +} + +export default { + getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey, + publishEvent, updateUser, fetchContext, +} diff --git a/src/relay/worker.js b/src/relay/worker.js deleted file mode 100644 index b47d5c4f..00000000 --- a/src/relay/worker.js +++ /dev/null @@ -1,7 +0,0 @@ -const url = new URL('src/worker/index.js', import.meta.url) - -export const worker = new Worker(url, {type: 'module'}) - -worker.post = (topic, payload) => worker.postMessage({topic, payload}) - -window.worker = worker diff --git a/src/routes/AddRelay.svelte b/src/routes/AddRelay.svelte index 7ed6dd13..671586d2 100644 --- a/src/routes/AddRelay.svelte +++ b/src/routes/AddRelay.svelte @@ -2,8 +2,7 @@ import {fly} from 'svelte/transition' import {registerRelay} from 'src/state/nostr' import toast from 'src/state/toast' - import {user} from 'src/state/user' - import {modal, ensureAccounts} from 'src/state/app' + import {modal} from 'src/state/app' import {dispatch} from 'src/state/dispatch' import Input from 'src/partials/Input.svelte' import Button from 'src/partials/Button.svelte' @@ -21,10 +20,6 @@ registerRelay(url) dispatch("relay/join", url) modal.set(null) - - if ($user) { - ensureAccounts([$user.pubkey], {force: true}) - } } diff --git a/src/routes/Alerts.svelte b/src/routes/Alerts.svelte index 18956469..3b66cf47 100644 --- a/src/routes/Alerts.svelte +++ b/src/routes/Alerts.svelte @@ -1,74 +1,35 @@ - -
    - {#each $events as e (e.id)} + {#each ($events || []) as e (e.id)} {#if e.kind === 7} - - {#if e.parent?.pubkey === $user.pubkey}
  • No recent activity found. diff --git a/src/state/app.js b/src/state/app.js index 802995ea..249649e5 100644 --- a/src/state/app.js +++ b/src/state/app.js @@ -1,11 +1,9 @@ -import {uniq} from 'ramda' -import {writable, get} from 'svelte/store' +import {writable} from 'svelte/store' import {navigate} from "svelte-routing" import {globalHistory} from "svelte-routing/src/history" -import {switcherFn} from 'hurdak/lib/hurdak' import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc" import {user} from 'src/state/user' -import {channels, relays} from 'src/state/nostr' +import {relays} from 'src/state/nostr' export const modal = { subscribe: cb => { @@ -55,60 +53,3 @@ export const logout = () => { navigate("/login") }, 200) } - -// Accounts - -export const accounts = writable(getLocalJson("coracle/accounts") || {}) - -accounts.subscribe($accounts => { - setLocalJson("coracle/accounts", $accounts) -}) - -user.subscribe($user => { - if ($user) { - accounts.update($accounts => ({...$accounts, [$user.pubkey]: $user})) - } -}) - -export const ensureAccounts = async (pubkeys, {force = false} = {}) => { - const $accounts = get(accounts) - - // Don't request accounts we recently updated - pubkeys = pubkeys.filter( - k => force || !$accounts[k] || $accounts[k].refreshed < now() - timedelta(10, 'minutes') - ) - - if (pubkeys.length) { - const events = await channels.getter.all({kinds: [0, 3, 12165], authors: uniq(pubkeys)}) - - await accounts.update($accounts => { - events.forEach(e => { - const values = { - muffle: [], - petnames: [], - ...$accounts[e.pubkey], - pubkey: e.pubkey, - refreshed: now(), - isUser: true, - } - - switcherFn(e.kind, { - 0: () => { - $accounts[e.pubkey] = {...values, ...JSON.parse(e.content)} - }, - 3: () => { - $accounts[e.pubkey] = {...values, petnames: e.tags} - }, - 12165: () => { - $accounts[e.pubkey] = {...values, muffle: e.tags} - }, - }) - }) - - return $accounts - }) - } - - // Keep our user in sync - user.update($user => $user ? {...$user, ...get(accounts)[$user.pubkey]} : null) -} diff --git a/src/state/dispatch.js b/src/state/dispatch.js index 6a7a2050..95f16233 100644 --- a/src/state/dispatch.js +++ b/src/state/dispatch.js @@ -3,8 +3,7 @@ import {get} from 'svelte/store' import {first, defmulti} from "hurdak/lib/hurdak" import {user} from "src/state/user" import relay from 'src/relay' -import {nostr, relays} from 'src/state/nostr' -import {ensureAccounts} from 'src/state/app' +import {relays} from 'src/state/nostr' // Commands are processed in two layers: // - App-oriented commands are created via dispatch @@ -35,7 +34,7 @@ dispatch.addMethod("account/update", async (topic, updates) => { user.set({...get(user), ...updates}) // Tell the network - await relay.worker.post('event/publish', nostr.event(0, JSON.stringify(updates))) + await relay.pool.publishEvent(makeEvent(0, JSON.stringify(updates))) }) dispatch.addMethod("account/petnames", async (topic, petnames) => { @@ -45,7 +44,7 @@ dispatch.addMethod("account/petnames", async (topic, petnames) => { user.set({...$user, petnames}) // Tell the network - await relay.worker.post('event/publish', nostr.event(3, '', petnames)) + await relay.pool.publishEvent(makeEvent(3, '', petnames)) }) dispatch.addMethod("account/muffle", async (topic, muffle) => { @@ -55,17 +54,13 @@ dispatch.addMethod("account/muffle", async (topic, muffle) => { user.set({...$user, muffle}) // Tell the network - await relay.worker.post('event/publish', nostr.event(12165, '', muffle)) + await relay.pool.publishEvent(makeEvent(12165, '', muffle)) }) dispatch.addMethod("relay/join", async (topic, url) => { const $user = get(user) relays.update(r => r.concat(url)) - - if ($user) { - await ensureAccounts([$user.pubkey], {force: true}) - } }) dispatch.addMethod("relay/leave", (topic, url) => { @@ -73,59 +68,59 @@ dispatch.addMethod("relay/leave", (topic, url) => { }) dispatch.addMethod("room/create", async (topic, room) => { - const event = nostr.event(40, JSON.stringify(room)) + const event = makeEvent(40, JSON.stringify(room)) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("room/update", async (topic, {id, ...room}) => { - const event = nostr.event(41, JSON.stringify(room), [t("e", id)]) + const event = makeEvent(41, JSON.stringify(room), [t("e", id)]) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("message/create", async (topic, roomId, content) => { - const event = nostr.event(42, content, [t("e", roomId, "root")]) + const event = makeEvent(42, content, [t("e", roomId, "root")]) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("note/create", async (topic, content, tags=[]) => { - const event = nostr.event(1, content, tags) + const event = makeEvent(1, content, tags) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("reaction/create", async (topic, content, e) => { const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')]) - const event = nostr.event(7, content, tags) + const event = makeEvent(7, content, tags) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("reply/create", async (topic, content, e) => { const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')]) - const event = nostr.event(1, content, tags) + const event = makeEvent(1, content, tags) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) dispatch.addMethod("event/delete", async (topic, ids) => { - const event = nostr.event(5, '', ids.map(id => t("e", id))) + const event = makeEvent(5, '', ids.map(id => t("e", id))) - await relay.worker.post('event/publish', event) + await relay.pool.publishEvent(event) return event }) @@ -149,3 +144,10 @@ export const t = (type, content, marker) => { return tag } + +export const makeEvent = (kind, content = '', tags = []) => { + const pubkey = relay.pool.getPubkey() + const createdAt = Math.round(new Date().valueOf() / 1000) + + return {kind, content, tags, pubkey, created_at: createdAt} +} diff --git a/src/state/nostr.js b/src/state/nostr.js index 41af4915..a97f0da7 100644 --- a/src/state/nostr.js +++ b/src/state/nostr.js @@ -1,56 +1,9 @@ import {writable, get} from 'svelte/store' -import {relayPool, getPublicKey} from 'nostr-tools' -import {assoc, last, find, intersection, uniqBy, prop} from 'ramda' -import {first, noop, ensurePlural} from 'hurdak/lib/hurdak' +import {assoc, uniqBy, prop} from 'ramda' +import {noop, ensurePlural} from 'hurdak/lib/hurdak' import relay from 'src/relay' import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc" -export const nostr = relayPool() - -export const epoch = 1633046400 - -export const filterTags = (where, events) => - ensurePlural(events) - .flatMap( - e => e.tags.filter(t => { - if (where.tag && where.tag !== t[0]) { - return false - } - - if (where.type && where.type !== last(t)) { - return false - } - - return true - }).map(t => t[1]) - ) - -export const findTag = (where, events) => first(filterTags(where, events)) - -// Support the deprecated version where tags are not marked as replies -export const findReply = e => - findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e) - -export const findRoot = e => - findTag({tag: "e", type: "root"}, e) - -export const filterMatches = (filter, e) => { - return Boolean(find( - f => { - return ( - (!f.ids || f.ids.includes(e.id)) - && (!f.authors || f.authors.includes(e.pubkey)) - && (!f.kinds || f.kinds.includes(e.kind)) - && (!f['#e'] || intersection(f['#e'], e.tags.filter(t => t[0] === 'e').map(t => t[1]))) - && (!f['#p'] || intersection(f['#p'], e.tags.filter(t => t[0] === 'p').map(t => t[1]))) - && (!f.since || f.since >= e.created_at) - && (!f.until || f.until <= e.created_at) - ) - }, - ensurePlural(filter) - )) -} - export class Channel { constructor(name) { this.name = name @@ -72,7 +25,7 @@ export class Channel { let resolve const eoseRelays = [] - const sub = nostr.sub({filter, cb}, this.name, r => { + const sub = relay.pool.sub({filter, cb}, this.name, r => { eoseRelays.push(r) if (eoseRelays.length === get(relays).length) { @@ -223,28 +176,6 @@ export class Listener { } } -// Augment nostr with some extra methods - -nostr.login = privkey => { - nostr.setPrivateKey(privkey) - nostr._privkey = privkey -} - -nostr.pubkeyLogin = pubkey => { - nostr.registerSigningFunction( async (event) => { - const {sig} = await window.nostr.signEvent(event) - return sig - }) - nostr._pubkey = pubkey -} - -nostr.event = (kind, content = '', tags = []) => { - const pubkey = nostr._pubkey || getPublicKey(nostr._privkey) - const createdAt = Math.round(new Date().valueOf() / 1000) - - return {kind, content, tags, pubkey, created_at: createdAt} -} - // Keep track of known relays export const knownRelays = writable((getLocalJson("coracle/knownRelays") || [ @@ -293,15 +224,13 @@ let prevRelays = [] relays.subscribe($relays => { prevRelays.forEach(url => { if (!$relays.includes(url)) { - nostr.removeRelay(url) - relay.worker.post('pool/removeRelay', url) + relay.pool.removeRelay(url) } }) $relays.forEach(url => { if (!prevRelays.includes(url)) { - nostr.addRelay(url) - relay.worker.post('pool/addRelay', url) + relay.pool.addRelay(url) } }) diff --git a/src/state/user.js b/src/state/user.js index 423f58a3..fd3a2535 100644 --- a/src/state/user.js +++ b/src/state/user.js @@ -1,6 +1,5 @@ import {writable} from "svelte/store" import {getLocalJson, setLocalJson} from "src/util/misc" -import {nostr} from 'src/state/nostr' import relay from 'src/relay' export const user = writable(getLocalJson("coracle/user")) @@ -10,11 +9,9 @@ user.subscribe($user => { // Keep nostr in sync if ($user?.privkey) { - nostr.login($user.privkey) - relay.worker.post('pool/setPrivateKey', $user.privkey) + relay.pool.setPrivateKey($user.privkey) } else if ($user?.pubkey) { - nostr.pubkeyLogin($user.pubkey) - relay.worker.post('pool/setPublicKey', $user.pubkey) + relay.pool.setPublicKey($user.pubkey) } // Migrate data from old formats diff --git a/src/util/nostr.js b/src/util/nostr.js new file mode 100644 index 00000000..885e5b48 --- /dev/null +++ b/src/util/nostr.js @@ -0,0 +1,62 @@ +import {last, intersection} from 'ramda' +import {ensurePlural, first} from 'hurdak/lib/hurdak' + +export const epoch = 1633046400 + +export const filterTags = (where, events) => + ensurePlural(events) + .flatMap( + e => e.tags.filter(t => { + if (where.tag && where.tag !== t[0]) { + return false + } + + if (where.type && where.type !== last(t)) { + return false + } + + return true + }).map(t => t[1]) + ) + +export const findTag = (where, events) => first(filterTags(where, events)) + +// Support the deprecated version where tags are not marked as replies +export const findReply = e => + findTag({tag: "e", type: "reply"}, e) || findTag({tag: "e"}, e) + +export const findRoot = e => + findTag({tag: "e", type: "root"}, e) + +export const filterMatches = (filter, e) => { + return Boolean(find( + f => { + return ( + (!f.ids || f.ids.includes(e.id)) + && (!f.authors || f.authors.includes(e.pubkey)) + && (!f.kinds || f.kinds.includes(e.kind)) + && (!f['#e'] || intersection(f['#e'], e.tags.filter(t => t[0] === 'e').map(t => t[1]))) + && (!f['#p'] || intersection(f['#p'], e.tags.filter(t => t[0] === 'p').map(t => t[1]))) + && (!f.since || f.since >= e.created_at) + && (!f.until || f.until <= e.created_at) + ) + }, + ensurePlural(filter) + )) +} + +export const getMuffleValue = pubkey => { + const $user = get(user) + + if (!$user) { + return 1 + } + + const tag = find(t => t[1] === pubkey, $user.muffle) + + if (!tag) { + return 1 + } + + return parseFloat(last(tag)) +} diff --git a/src/util/notes.js b/src/util/notes.js index 60a22ce2..0328043e 100644 --- a/src/util/notes.js +++ b/src/util/notes.js @@ -1,111 +1,10 @@ -import {identity, uniq, concat, propEq, uniqBy, prop, groupBy, find, last, pluck} from 'ramda' +import {identity, uniq, propEq, uniqBy, prop, groupBy, pluck} from 'ramda' import {debounce} from 'throttle-debounce' import {get} from 'svelte/store' -import {switcherFn, ellipsize, createMap} from 'hurdak/lib/hurdak' +import {getMuffleValue, epoch, filterMatches, findReply} from 'src/util/nostr' +import {switcherFn, createMap} from 'hurdak/lib/hurdak' import {timedelta, sleep} from "src/util/misc" -import {escapeHtml} from 'src/util/html' -import {user} from 'src/state/user' -import {epoch, filterMatches, Listener, channels, findReply, findRoot} from 'src/state/nostr' -import {accounts, ensureAccounts} from 'src/state/app' - -export const renderNote = (note, {showEntire = false}) => { - const shouldEllipsize = note.content.length > 500 && !showEntire - const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content - const $accounts = get(accounts) - - return escapeHtml(content) - .replace(/\n/g, '
    ') - .replace(/https?:\/\/([\w.-]+)[^ ]*/g, (url, domain) => { - return `${domain}` - }) - .replace(/#\[(\d+)\]/g, (tag, i) => { - if (!note.tags[parseInt(i)]) { - return tag - } - - const pubkey = note.tags[parseInt(i)][1] - const user = $accounts[pubkey] - const name = user?.name || pubkey.slice(0, 8) - - return `@${name}` - }) -} - -export const getMuffleValue = pubkey => { - const $user = get(user) - - if (!$user) { - return 1 - } - - const tag = find(t => t[1] === pubkey, $user.muffle) - - if (!tag) { - return 1 - } - - return parseFloat(last(tag)) -} - -export const threadify = async notes => { - if (notes.length === 0) { - return [] - } - - const noteIds = pluck('id', notes) - const rootIds = notes.map(findReply) - const parentIds = notes.map(findRoot) - const ancestorIds = concat(rootIds, parentIds).filter(identity) - - // Find all direct parents and thread roots - const filters = ancestorIds.length === 0 - ? [{kinds: [1, 7], '#e': noteIds}] - : [{kinds: [1], ids: ancestorIds}, - {kinds: [1, 7], '#e': noteIds.concat(ancestorIds)}] - - const events = await channels.getter.all(filters) - - await ensureAccounts(uniq(pluck('pubkey', notes.concat(events)))) - - const $accounts = get(accounts) - const reactionsByParent = groupBy(findReply, events.filter(propEq('kind', 7))) - const allNotes = uniqBy(prop('id'), notes.concat(events.filter(propEq('kind', 1)))) - const notesById = createMap('id', allNotes) - const notesByRoot = groupBy( - n => { - const rootId = findRoot(n) - const parentId = findReply(n) - - // Actually dereference the notes in case we weren't able to retrieve them - if (notesById[rootId]) { - return rootId - } - - if (notesById[parentId]) { - return parentId - } - - return n.id - }, - allNotes - ) - - const threads = [] - for (const [rootId, _notes] of Object.entries(notesByRoot)) { - const annotate = note => { - return { - ...note, - user: $accounts[note.pubkey], - reactions: reactionsByParent[note.id] || [], - children: uniqBy(prop('id'), _notes.filter(n => findReply(n) === note.id)).map(annotate), - } - } - - threads.push(annotate(notesById[rootId])) - } - - return threads -} +import {Listener, channels} from 'src/state/nostr' export const annotateNotes = async (notes, {showParent = false} = {}) => { if (notes.length === 0) { @@ -149,41 +48,6 @@ export const annotateNotes = async (notes, {showParent = false} = {}) => { }) } -export const annotateAlerts = async events => { - if (events.length === 0) { - return [] - } - - const eventIds = pluck('id', events) - const parentIds = events.map(findReply).filter(identity) - const filters = [ - {kinds: [1], ids: parentIds}, - {kinds: [7], '#e': parentIds}, - {kinds: [1, 7], '#e': eventIds}, - ] - - const relatedEvents = await channels.getter.all(filters) - - await ensureAccounts(uniq(pluck('pubkey', events.concat(relatedEvents)))) - - const $accounts = get(accounts) - const reactionsByParent = groupBy(findReply, relatedEvents.filter(e => e.kind === 7 && e.content === '+')) - const allNotes = uniqBy(prop('id'), events.concat(relatedEvents).filter(propEq('kind', 1))) - const notesById = createMap('id', allNotes) - - const annotate = note => ({ - ...note, - user: $accounts[note.pubkey], - reactions: reactionsByParent[note.id] || [], - children: uniqBy(prop('id'), allNotes.filter(n => findReply(n) === note.id)).map(annotate), - }) - - return uniqBy(e => e.parent?.id || e.id, events.map(event => { - const parentId = findReply(event) - - return {...annotate(event), parent: annotate(notesById[parentId])} - })) -} export const annotateNewNote = async (note) => { await ensureAccounts([note.pubkey])