From 3dae3494dd66f664007a973f881f53638912dca0 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Sat, 7 Jan 2023 09:48:01 -0800 Subject: [PATCH] Re-write everything again --- README.md | 24 ++- src/App.svelte | 44 +----- src/{relay/db.js => agent/data.js} | 35 ++--- src/agent/index.js | 48 +++++- src/agent/keys.js | 27 ++-- src/agent/pool.js | 126 ++++++++++------ src/app/alerts.js | 33 ++++ src/{relay => app}/cmd.js | 26 ++-- src/app/defaults.js | 16 ++ src/app/index.js | 90 +++++++++++ src/app/loaders.js | 77 ++++++++++ src/{relay/index.js => app/query.js} | 142 +----------------- src/app/ui.js | 52 +++++++ src/partials/Anchor.svelte | 8 +- src/partials/Compose.svelte | 2 +- src/partials/Like.svelte | 4 +- src/partials/Note.svelte | 11 +- src/partials/Notes.svelte | 7 +- src/partials/Preview.svelte | 18 ++- src/relay/pool.js | 217 --------------------------- src/routes/AddRelay.svelte | 7 +- src/routes/Alerts.svelte | 19 ++- src/routes/Keys.svelte | 4 +- src/routes/Login.svelte | 22 +-- src/routes/Logout.svelte | 15 +- src/routes/NoteCreate.svelte | 7 +- src/routes/Notes.svelte | 27 ++-- src/routes/Person.svelte | 32 ++-- src/routes/Profile.svelte | 7 +- src/routes/RelayList.svelte | 22 ++- src/routes/Settings.svelte | 5 +- src/state/app.js | 96 ------------ src/state/toast.js | 19 --- src/util/data.js | 35 ----- src/util/misc.js | 34 ++++- src/util/nostr.js | 14 ++ src/views/NoteDetail.svelte | 18 ++- src/views/PersonSettings.svelte | 7 +- src/views/SearchNotes.svelte | 7 +- src/views/SearchPeople.svelte | 2 +- src/views/notes/Global.svelte | 32 ++-- src/views/notes/Network.svelte | 61 ++++---- src/views/person/Likes.svelte | 13 +- src/views/person/Network.svelte | 14 +- src/views/person/Notes.svelte | 13 +- 45 files changed, 722 insertions(+), 817 deletions(-) rename src/{relay/db.js => agent/data.js} (74%) create mode 100644 src/app/alerts.js rename src/{relay => app}/cmd.js (73%) create mode 100644 src/app/defaults.js create mode 100644 src/app/index.js create mode 100644 src/app/loaders.js rename src/{relay/index.js => app/query.js} (58%) create mode 100644 src/app/ui.js delete mode 100644 src/relay/pool.js delete mode 100644 src/state/app.js delete mode 100644 src/state/toast.js delete mode 100644 src/util/data.js diff --git a/README.md b/README.md index 54193561..f175a4f0 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ If you like Coracle and want to support its development, you can donate sats via - https://github.com/nbd-wtf/nostr-tools/blob/master/nip19.ts - [ ] Support key delegation - https://github.com/nbd-wtf/nostr-tools/blob/master/nip26.ts +- [ ] Add relay selector when publishing a note +- [ ] Add keyword mutes # Bugs @@ -46,9 +48,25 @@ If you like Coracle and want to support its development, you can donate sats via ## Current -- [ ] Upgrade nostr-tools - - [ ] Close connections that haven't been used in a while - - [ ] Move key management stuff +- [x] Upgrade nostr-tools +- [ ] Publish user relays using nip 23 +- [ ] Use user relays for feeds +- [ ] Publish to user relays + target relays: + - If a reply or reaction, publish to the parent event's best relay, which is: + - e tag relay + - p tag relay + - or pubkey's recommended relays +- [ ] Add recommended relay to tags +- [ ] Close connections that haven't been used in a while +- [ ] Support some read/write config on relays page +- [ ] Get real home relays for default pubkeys +- [ ] Add settings storage +- [ ] Use hexToBech32 from nostr-tools +- [ ] Warn that everything will be cleared on logout +- [ ] Clear dexie on page load, we don't need any persistence other than people/relays +- [ ] Clean up login page to prefer extension, make private key entry "advanced" +- [ ] Do I need to implement re-connecting now? +- [ ] handle localstorage limits https://stackoverflow.com/questions/2989284/what-is-the-max-size-of-localstorage-values ## 0.2.6 diff --git a/src/App.svelte b/src/App.svelte index a07ff997..1f7e8b1f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,15 +6,13 @@ import {writable, get} from "svelte/store" import {fly, fade} from "svelte/transition" import {cubicInOut} from "svelte/easing" - import {throttle} from 'throttle-debounce' import {Router, Route, links, navigate} from "svelte-routing" import {globalHistory} from "svelte-routing/src/history" import {hasParent} from 'src/util/html' import {displayPerson, isLike} from 'src/util/nostr' import {timedelta, now} from 'src/util/misc' - import {store as toast} from "src/state/toast" - import {modal, settings, alerts} from "src/state/app" - import relay, {user, connections} from 'src/relay' + import {user} from 'src/agent' + import {modal, toast, settings, alerts} from "src/app" import Anchor from 'src/partials/Anchor.svelte' import NoteDetail from "src/views/NoteDetail.svelte" import PersonSettings from "src/views/PersonSettings.svelte" @@ -32,8 +30,6 @@ import Person from "src/routes/Person.svelte" import NoteCreate from "src/routes/NoteCreate.svelte" - window.relay = relay - export let url = "" const menuIsOpen = writable(false) @@ -45,7 +41,7 @@ let menuIcon let scrollY let suspendedSubs = [] - let mostRecentAlert = $alerts.since + let {since, latest} = alerts onMount(() => { // Close menu on click outside @@ -55,35 +51,6 @@ } }) - let prevPubkey = null - - const unsubUser = user.subscribe($user => { - if ($user && $user.pubkey !== prevPubkey) { - relay.pool.syncNetwork() - relay.pool.listenForEvents( - 'App/alerts', - [{kinds: [1, 7], '#p': [$user.pubkey], since: mostRecentAlert}], - e => { - // Don't alert about people's own stuff - if (e.pubkey === $user.pubkey) { - return - } - - // Only notify users about positive reactions - if (e.kind === 7 && !isLike(e.content)) { - return - } - - relay.loadNotesContext([e]) - - mostRecentAlert = Math.max(e.created_at, mostRecentAlert) - } - ) - } - - prevPubkey = $user?.pubkey - }) - const unsubModal = modal.subscribe($modal => { // Keep scroll position on body, but don't allow scrolling if ($modal) { @@ -101,7 +68,6 @@ }) return () => { - unsubUser() unsubModal() } }) @@ -153,7 +119,7 @@
  • Alerts - {#if mostRecentAlert > $alerts.since} + {#if $latest > $since}
    {/if} @@ -209,7 +175,7 @@

    Coracle

    - {#if mostRecentAlert > $alerts.since} + {#if $latest > $since}
    {/if}
    diff --git a/src/relay/db.js b/src/agent/data.js similarity index 74% rename from src/relay/db.js rename to src/agent/data.js index 9f0785c8..a713214f 100644 --- a/src/relay/db.js +++ b/src/agent/data.js @@ -1,11 +1,11 @@ import Dexie from 'dexie' -import {writable, get} from 'svelte/store' +import {writable} from 'svelte/store' import {groupBy, prop, flatten, pick} from 'ramda' import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak' -import {now, timedelta, getLocalJson, setLocalJson} from 'src/util/misc' +import {now, timedelta} from 'src/util/misc' import {filterTags, findReply, findRoot} from 'src/util/nostr' -export const db = new Dexie('coracle/relay') +export const db = new Dexie('agent/data/db') db.version(6).stores({ relays: '++url, name', @@ -13,23 +13,20 @@ db.version(6).stores({ tags: '++key, event, value, created_at, loaded_at', }) -window.db = db - // Some things work better as observables than database tables -db.user = writable(getLocalJson("db/user")) -db.people = writable(getLocalJson('db/people') || {}) -db.network = writable(getLocalJson('db/network') || []) -db.connections = writable(getLocalJson("db/connections") || []) +export const people = writable({}) -db.user.subscribe($user => setLocalJson("db/user", $user)) -db.people.subscribe($people => setLocalJson("db/people", $people)) -db.network.subscribe($network => setLocalJson("db/network", $network)) -db.connections.subscribe($connections => setLocalJson("db/connections", $connections)) +let $people = {} +people.subscribe($p => { + $people = $p +}) + +export const getPerson = pubkey => $people[pubkey] // Hooks -db.events.process = async events => { +export const processEvents = async events => { // Only persist ones we care about, the rest can be ephemeral and used to update people etc const eventsByKind = groupBy(prop('kind'), ensurePlural(events)) const notesAndReactions = flatten(Object.values(pick([1, 7], eventsByKind))) @@ -70,9 +67,7 @@ db.events.process = async events => { } // Update our people - db.people.update($people => { - let $user = get(db.user) - + people.update($people => { for (const event of profileUpdates) { const {pubkey, kind, content, tags} = event const putPerson = data => { @@ -82,10 +77,6 @@ db.events.process = async events => { pubkey, updated_at: now(), } - - if ($user?.pubkey === pubkey) { - $user = {...$user, ...data} - } } switcherFn(kind, { @@ -98,8 +89,6 @@ db.events.process = async events => { }) } - db.user.set($user) - return $people }) } diff --git a/src/agent/index.js b/src/agent/index.js index 36e5b9ac..42b8e7ab 100644 --- a/src/agent/index.js +++ b/src/agent/index.js @@ -1,4 +1,48 @@ -import keys from 'src/agent/keys' +import {derived} from 'svelte/store' import pool from 'src/agent/pool' +import keys from 'src/agent/keys' +import {db, people, getPerson, processEvents} from 'src/agent/data' -export default {pool, keys} +Object.assign(window, {pool, db}) + +export {pool, keys, db, people, getPerson} + +export const user = derived( + [keys.pubkey, people], + ([pubkey, $people]) => $people[pubkey] || {pubkey} +) + +export const publish = async (relays, event) => { + const signedEvent = keys.sign(event) + + await Promise.all([ + pool.publish(relays, signedEvent), + processEvents(signedEvent), + ]) + + return signedEvent +} + +export const load = async (relays, filter) => { + const events = await pool.request(relays, filter) + + await processEvents(events) + + return events +} + +export const listen = async (relays, filter, onEvent, {shouldProcess = true} = {}) => { + const sub = await pool.subscribe(relays, filter) + + sub.onEvent(e => { + if (shouldProcess) { + processEvents(e) + } + + if (onEvent) { + onEvent(e) + } + }) + + return sub +} diff --git a/src/agent/keys.js b/src/agent/keys.js index 99a28a89..369f8a31 100644 --- a/src/agent/keys.js +++ b/src/agent/keys.js @@ -1,15 +1,15 @@ import {getPublicKey, getEventHash, signEvent} from 'nostr-tools' +import {get} from 'svelte/store' +import {synced} from 'src/util/misc' -let pubkey -let privkey let signingFunction -const getPubkey = () => { - return pubkey || getPublicKey(privkey) -} +const pubkey = synced('agent/user/pubkey') +const privkey = synced('agent/user/privkey') const setPrivateKey = _privkey => { - privkey = _privkey + privkey.set(_privkey) + pubkey.set(getPublicKey(_privkey)) } const setPublicKey = _pubkey => { @@ -19,15 +19,22 @@ const setPublicKey = _pubkey => { return sig } - pubkey = _pubkey + pubkey.set(_pubkey) } const sign = async event => { - event.pubkey = pubkey + event.pubkey = get(pubkey) event.id = getEventHash(event) - event.sig = signingFunction ? await signingFunction(event) : signEvent(event, privkey) + event.sig = signingFunction + ? await signingFunction(event) + : signEvent(event, get(privkey)) return event } -export default {getPubkey, setPrivateKey, setPublicKey, sign} +const clear = () => { + pubkey.set(null) + privkey.set(null) +} + +export default {pubkey, setPrivateKey, setPublicKey, sign, clear} diff --git a/src/agent/pool.js b/src/agent/pool.js index c48b12b2..feb58b69 100644 --- a/src/agent/pool.js +++ b/src/agent/pool.js @@ -1,56 +1,78 @@ import {relayInit} from 'nostr-tools' -import {partial, uniqBy, prop} from 'ramda' +import {uniqBy, filter, identity, prop} from 'ramda' import {ensurePlural} from 'hurdak/lib/hurdak' const relays = {} -const connect = async url => { - if (!relays[url]) { - relays[url] = relayInit(url) - relays[url].url = url - relays[url].stats = { - count: 0, - timer: 0, - timeouts: 0, - activeCount: 0, - } +const init = url => { + const relay = relayInit(url) - relays[url].on('disconnect', () => { - delete relays[url] - }) - - relays[url].connected = relays[url].connect() + relay.url = url + relay.stats = { + count: 0, + timer: 0, + timeouts: 0, + activeCount: 0, } - await relays[url].connected + relay.on('error', () => { + console.log(`failed to connect to ${url}`) + }) + + relay.on('disconnect', () => { + delete relays[url] + }) + + // Do initialization synchonously and wait on retrieval + // so we don't open multiple connections simultaneously + return relay.connect().then( + () => relay, + e => console.log(`Failed to connect to ${url}: ${e}`) + ) +} + +const connect = url => { + if (!relays[url]) { + relays[url] = init(url) + } return relays[url] } -const publish = (urls, event) => { - urls.forEach(async url => { - const relay = await connect(url) - - relay.publish(event) - }) -} - -const sub = async (urls, filters) => { - const subs = await Promise.all( +const publish = async (urls, event) => { + return Promise.all( urls.map(async url => { const relay = await connect(url) + + return relay.publish(event) + }) + ) +} + +const subscribe = async (urls, filters) => { + const subs = filter(identity, await Promise.all( + urls.map(async url => { + const relay = await connect(url) + + // If the relay failed to connect, give up + if (!relay) { + return null + } + const sub = relay.sub(ensurePlural(filters)) sub.relay = relay sub.relay.stats.activeCount += 1 if (sub.relay.stats.activeCount > 10) { - console.warning(`Relay ${url} has >10 active subscriptions`) + console.warn(`Relay ${url} has >10 active subscriptions`) } return sub }) - ) + )) + + const seen = new Set() return { unsub: () => { @@ -59,42 +81,62 @@ const sub = async (urls, filters) => { sub.relay.stats.activeCount -= 1 }) }, - on: (type, cb) => { + onEvent: cb => { subs.forEach(sub => { - sub.on(type, partial(cb, [sub.relay.url])) + sub.on('event', e => { + if (!seen.has(e.id)) { + e.seen_on = sub.relay.url + seen.add(e.id) + cb(e) + } + }) + }) + }, + onEose: cb => { + subs.forEach(sub => { + sub.on('eose', () => cb(sub.relay.url)) }) }, } } -const all = (urls, filters) => { +const request = (urls, filters) => { return new Promise(async resolve => { - const subscription = await sub(urls, filters) + const subscription = await subscribe(urls, filters) const now = Date.now() const events = [] const eose = [] const done = () => { + subscription.unsub() + resolve(uniqBy(prop('id'), events)) // Keep track of relay timeouts - urls.forEach(url => { + urls.forEach(async url => { if (!eose.includes(url)) { - relays[url].stats.count += 1 - relays[url].stats.timer += Date.now() - now - relays[url].stats.timeouts += 1 + const relay = await connect(url) + + // Relay may be undefined if we failed to connect + if (relay) { + relay.stats.count += 1 + relay.stats.timer += Date.now() - now + relay.stats.timeouts += 1 + } } }) } - subscription.on('event', (url, e) => events.push(e)) + subscription.onEvent(e => events.push(e)) + + subscription.onEose(async url => { + const relay = await relays[url] - subscription.on('eose', url => { eose.push(url) // Keep track of relay timing stats - relays[url].stats.count += 1 - relays[url].stats.timer += Date.now() - now + relay.stats.count += 1 + relay.stats.timer += Date.now() - now if (eose.length === urls.length) { done() @@ -106,4 +148,4 @@ const all = (urls, filters) => { }) } -export default {relays, connect, publish, sub, all} +export default {relays, connect, publish, subscribe, request} diff --git a/src/app/alerts.js b/src/app/alerts.js new file mode 100644 index 00000000..b41f64ea --- /dev/null +++ b/src/app/alerts.js @@ -0,0 +1,33 @@ +import {get} from 'svelte/store' +import {synced, now, timedelta} from 'src/util/misc' +import {isAlert} from 'src/util/nostr' +import {listen as _listen} from 'src/agent' +import {getRelays} from 'src/app' +import loaders from 'src/app/loaders' + +let listener + +const start = now() - timedelta(30, 'days') + +export const since = synced("app/alerts/since", start) +export const latest = synced("app/alerts/latest", start) + +export const listen = async (relays, pubkey) => { + if (listener) { + listener.unsub() + } + + listener = await _listen( + relays, + [{kinds: [1, 7], '#p': [pubkey], since: get(since)}], + e => { + if (isAlert(e, pubkey)) { + loaders.loadNotesContext(getRelays(), [e]) + + latest.set(Math.max(e.created_at, get(latest))) + } + } + ) +} + +export default {latest, listen} diff --git a/src/relay/cmd.js b/src/app/cmd.js similarity index 73% rename from src/relay/cmd.js rename to src/app/cmd.js index 818ac274..a74c9bb4 100644 --- a/src/relay/cmd.js +++ b/src/app/cmd.js @@ -1,14 +1,13 @@ import {isNil, uniqBy, last} from 'ramda' +import {get} from 'svelte/store' import {first} from "hurdak/lib/hurdak" -import relay from 'src/relay' +import {keys, publish, user} from 'src/agent' const updateUser = updates => publishEvent(0, JSON.stringify(updates)) -const addPetname = (person, pubkey, name) => - publishEvent(3, '', uniqBy(t => t[1], person.petnames.concat([t("p", pubkey, name)]))) +const setRelays = relays => publishEvent(10001, "", relays.map(url => [url, "", ""])) -const removePetname = (person, pubkey) => - publishEvent(3, '', uniqBy(t => t[1], person.petnames.filter(t => t[1] !== pubkey))) +const setPetnames = petnames => publishEvent(3, "", petnames) const muffle = (person, pubkey, value) => { const muffle = person.muffle @@ -53,7 +52,8 @@ const copyTags = (e, newTags = []) => { } export const t = (type, content, marker) => { - const tag = [type, content, first(relay.pool.getRelays())] + const relays = get(user).relays || [] + const tag = [type, content, first(relays)] if (!isNil(marker)) { tag.push(marker) @@ -63,21 +63,23 @@ export const t = (type, content, marker) => { } const makeEvent = (kind, content = '', tags = []) => { - const pubkey = relay.pool.getPubkey() + const pubkey = get(keys.pubkey) const createdAt = Math.round(new Date().valueOf() / 1000) return {kind, content, tags, pubkey, created_at: createdAt} } -const publishEvent = async (...args) => { - const event = makeEvent(...args) +const publishEvent = (...args) => { + const relays = get(user).relays || [] - await relay.pool.publishEvent(event) + if (relays.length === 0) { + throw new Error("Unable to publish, user has no relays") + } - return event + publish(relays, makeEvent(...args)) } export default { - updateUser, addPetname, removePetname, muffle, createRoom, updateRoom, createMessage, createNote, + updateUser, setRelays, setPetnames, muffle, createRoom, updateRoom, createMessage, createNote, createReaction, createReply, deleteEvent, publishEvent, } diff --git a/src/app/defaults.js b/src/app/defaults.js new file mode 100644 index 00000000..c0367956 --- /dev/null +++ b/src/app/defaults.js @@ -0,0 +1,16 @@ +export default { + petnames: [ + ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://relay.damus.io"], + ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", "jb55", "wss://relay.damus.io"], + ["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://relay.damus.io"], + ["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"], + ["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://relay.damus.io"], + ["p", "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", "preston", "wss://relay.damus.io"], + ], + relays: [ + // 'wss://nostr.rocks', + // 'wss://astral.ninja', + // 'wss://relay.damus.io', + 'wss://nostr-pub.wellorder.net', + ], +} diff --git a/src/app/index.js b/src/app/index.js new file mode 100644 index 00000000..8a49dc6e --- /dev/null +++ b/src/app/index.js @@ -0,0 +1,90 @@ +import {without, reject} from 'ramda' +import {first} from 'hurdak/lib/hurdak' +import {get} from 'svelte/store' +import {getPerson, keys, db} from 'src/agent' +import {toast, modal, settings} from 'src/app/ui' +import cmd from 'src/app/cmd' +import alerts from 'src/app/alerts' +import loaders from 'src/app/loaders' +import defaults from 'src/app/defaults' + +export {toast, modal, settings, alerts} + +export const getRelays = pubkey => { + const person = getPerson(pubkey) + + return person && person.relays.length > 0 ? person.relays : defaults.relays +} + +export const getBestRelay = pubkey => { + const person = getPerson(pubkey) + + if (!person) { + return null + } + + return person.relays.length > 0 ? first(person.relays) : person.recommendedRelay +} + +export const login = async ({privkey, pubkey}) => { + if (privkey) { + keys.setPrivateKey(privkey) + } else { + keys.setPublicKey(pubkey) + } + + await loaders.loadNetwork(getRelays(), pubkey) + await alerts.listen(getRelays(), pubkey) +} + +export const logout = async () => { + keys.clear() + + await db.tags.clear() + await db.events.clear() +} + +export const addRelay = async url => { + const pubkey = get(keys.pubkey) + const person = getPerson(pubkey) + const relays = person?.relays || [] + + await cmd.setRelays(relays.concat(url)) + await loaders.loadNetwork(relays, pubkey) + await alerts.listen(relays, pubkey) +} + +export const removeRelay = async url => { + const pubkey = get(keys.pubkey) + const person = getPerson(pubkey) + const relays = person?.relays || [] + + await cmd.setRelays(without([url], relays)) +} + +export const follow = async targetPubkey => { + const pubkey = get(keys.pubkey) + const person = getPerson(pubkey) + const petnames = person?.petnames || [] + const target = getPerson(targetPubkey) + const relay = ( + getBestRelay(targetPubkey) + || getBestRelay(pubkey) + || first(defaults.relays) + ) + + await cmd.setPetnames( + reject(t => t[1] === targetPubkey, petnames) + .concat([["p", pubkey, relay, target?.name || ""]]) + ) + + await loaders.loadNetwork(getRelays(), pubkey) +} + +export const unfollow = async targetPubkey => { + const pubkey = get(keys.pubkey) + const person = getPerson(pubkey) + const petnames = person?.petnames || [] + + await cmd.setPetnames(reject(t => t[1] === targetPubkey, petnames)) +} diff --git a/src/app/loaders.js b/src/app/loaders.js new file mode 100644 index 00000000..e07feb10 --- /dev/null +++ b/src/app/loaders.js @@ -0,0 +1,77 @@ +import {uniq, pluck, groupBy, prop, identity} from 'ramda' +import {ensurePlural, createMap} from 'hurdak/lib/hurdak' +import {filterTags, findReply} from 'src/util/nostr' +import {load, db, getPerson} from 'src/agent' +import defaults from 'src/app/defaults' + +const personKinds = [0, 2, 3, 10001, 12165] + +const loadPeople = (relays, pubkeys, {kinds = personKinds, ...opts} = {}) => + pubkeys.length > 0 ? load(relays, {kinds, authors: pubkeys}, opts) : [] + +const loadNetwork = async (relays, pubkey) => { + // Get this user's profile to start with. This may update what relays + // are available, so don't assign relays to a variable here. + let events = pubkey ? await loadPeople(relays, [pubkey]) : [] + let petnames = events.filter(e => e.kind === 3).flatMap(e => e.tags.filter(t => t[0] === "p")) + + // Default to some cool guys we know + if (petnames.length === 0) { + petnames = defaults.petnames + } + + // Get the user's follows, with a fallback if we have no pubkey, then use nip-2 recommended + // relays to load our user's second-order follows in order to bootstrap our social graph + await Promise.all( + Object.entries(groupBy(t => t[2], petnames)) + .map(([relay, petnames]) => loadPeople([relay], petnames.map(t => t[1]))) + ) +} + +const loadNotesContext = async (relays, notes, {loadParents = false} = {}) => { + notes = ensurePlural(notes) + + if (notes.length === 0) { + return + } + + const authors = uniq(pluck('pubkey', notes)).filter(k => !getPerson(k)) + const parentIds = loadParents ? uniq(notes.map(findReply).filter(identity)) : [] + const filter = [{kinds: [1, 5, 7], '#e': pluck('id', notes)}] + + // Load authors if needed + if (authors.length > 0) { + filter.push({kinds: personKinds, authors}) + } + + // Load the note parents + if (parentIds.length > 0) { + filter.push({kinds: [1], ids: parentIds}) + } + + // Load the events + const events = await load(relays, filter) + const eventsById = createMap('id', events) + const parents = parentIds.map(id => eventsById[id]).filter(identity) + + // Load the parents' context as well + if (parents.length > 0) { + await loadNotesContext(relays, parents) + } +} + +const getOrLoadNote = async (relays, id) => { + if (!await db.events.get(id)) { + await load(relays, {kinds: [1], ids: [id]}) + } + + const note = await db.events.get(id) + + if (note) { + await loadNotesContext(relays, [note], {loadParent: true}) + } + + return note +} + +export default {getOrLoadNote, loadNotesContext, loadNetwork, loadPeople} diff --git a/src/relay/index.js b/src/app/query.js similarity index 58% rename from src/relay/index.js rename to src/app/query.js index e4bf12ae..51bb57fb 100644 --- a/src/relay/index.js +++ b/src/app/query.js @@ -1,24 +1,9 @@ -import {liveQuery} from 'dexie' import {get} from 'svelte/store' -import {uniq, pluck, intersection, sortBy, propEq, uniqBy, groupBy, concat, without, prop, isNil, identity} from 'ramda' +import {intersection, sortBy, propEq, uniqBy, groupBy, concat, prop, isNil, identity} from 'ramda' import {ensurePlural, createMap, ellipsize} from 'hurdak/lib/hurdak' import {renderContent} from 'src/util/html' import {filterTags, displayPerson, getTagValues, findReply, findRoot} from 'src/util/nostr' -import {db} from 'src/relay/db' -import pool from 'src/relay/pool' -import cmd from 'src/relay/cmd' - -// Livequery appears to swallow errors - -const lq = f => liveQuery(async () => { - try { - return await f() - } catch (e) { - console.error(e) - } -}) - -// Utils for filtering db - nothing below should load events from the network +import {db, people, getPerson} from 'src/agent' const filterEvents = async ({limit, ...filter}) => { let events = db.events @@ -77,7 +62,7 @@ const findNote = async (id, {showEntire = false, depth = 1} = {}) => { const reactions = await filterReactions(note.id) const replies = await filterReplies(note.id) - const person = prop(note.pubkey, get(db.people)) + const person = getPerson(note.pubkey) const html = await renderNote(note, {showEntire}) let parent = null @@ -89,7 +74,7 @@ const findNote = async (id, {showEntire = false, depth = 1} = {}) => { parent = { ...parent, reactions: await filterReactions(parent.id), - person: prop(parent.pubkey, get(db.people)), + person: getPerson(parent.pubkey), html: await renderNote(parent, {showEntire}), } } @@ -143,7 +128,7 @@ const annotateChunk = async chunk => { const renderNote = async (note, {showEntire = false}) => { const shouldEllipsize = note.content.length > 500 && !showEntire - const $people = get(db.people) + const $people = get(people) const peopleByPubkey = createMap( 'pubkey', filterTags({tag: "p"}, note).map(k => $people[k]).filter(identity) @@ -174,119 +159,4 @@ const renderNote = async (note, {showEntire = false}) => { return content } -// Synchronization - -const login = ({privkey, pubkey}) => { - db.user.set({relays: [], muffle: [], petnames: [], updated_at: 0, pubkey, privkey}) - - pool.syncNetwork() -} - -const addRelay = url => { - db.connections.update($connections => $connections.concat(url)) - - pool.syncNetwork() -} - -const removeRelay = url => { - db.connections.update($connections => without([url], $connections)) -} - -const follow = async pubkey => { - db.network.update($network => $network.concat(pubkey)) - - pool.syncNetwork() -} - -const unfollow = async pubkey => { - db.network.update($network => $network.concat(pubkey)) -} - -// Methods that wil attempt to load from the database and fall back to the network. -// This is intended only for bootstrapping listeners - -const loadNotesContext = async (notes, {loadParents = false} = {}) => { - notes = ensurePlural(notes) - - if (notes.length === 0) { - return - } - - const $people = get(people) - const authors = uniq(pluck('pubkey', notes)).filter(k => !$people[k]) - const parentIds = loadParents ? uniq(notes.map(findReply).filter(identity)) : [] - const filter = [{kinds: [1, 5, 7], '#e': pluck('id', notes)}] - - // Load authors if needed - if (authors.length > 0) { - filter.push({kinds: [0], authors}) - } - - // Load the note parents - if (parentIds.length > 0) { - filter.push({kinds: [1], ids: parentIds}) - } - - // Load the events - const events = await pool.loadEvents(filter) - const eventsById = createMap('id', events) - const parents = parentIds.map(id => eventsById[id]).filter(identity) - - // Load the parents' context as well - if (parents.length > 0) { - await loadNotesContext(parents) - } -} - -const getOrLoadNote = async id => { - if (!await db.events.get(id)) { - await pool.loadEvents({kinds: [1], ids: [id]}) - } - - const note = await db.events.get(id) - - if (note) { - await loadNotesContext([note], {loadParent: true}) - } - - return note -} - -// Initialization - -db.user.subscribe($user => { - if ($user?.privkey) { - pool.setPrivateKey($user.privkey) - } else if ($user?.pubkey) { - pool.setPublicKey($user.pubkey) - } -}) - -db.connections.subscribe($connections => { - const poolRelays = pool.getRelays() - - for (const url of $connections) { - if (!poolRelays.includes(url)) { - pool.addRelay(url) - } - } - - for (const url of poolRelays) { - if (!$connections.includes(url)) { - pool.removeRelay(url) - } - } -}) - -// Export stores on their own for convenience - -export const user = db.user -export const people = db.people -export const network = db.network -export const connections = db.connections - -export default { - db, pool, cmd, lq, filterEvents, getOrLoadNote, filterReplies, findNote, - annotateChunk, renderNote, login, addRelay, removeRelay, - follow, unfollow, loadNotesContext, -} +export default {filterEvents, filterReplies, filterReactions, annotateChunk, renderNote, findNote} diff --git a/src/app/ui.js b/src/app/ui.js new file mode 100644 index 00000000..bfd9d5ec --- /dev/null +++ b/src/app/ui.js @@ -0,0 +1,52 @@ +import {prop} from "ramda" +import {uuid} from "hurdak/lib/hurdak" +import {navigate} from "svelte-routing" +import {writable, get} from "svelte/store" +import {globalHistory} from "svelte-routing/src/history" +import {synced} from "src/util/misc" + +// Toast + +export const toast = writable(null) + +toast.show = (type, message, timeout = 5) => { + const id = uuid() + + toast.set({id, type, message}) + + setTimeout(() => { + if (prop("id", get(toast)) === id) { + toast.set(null) + } + }, timeout * 1000) +} + +// Modals + +export const modal = { + subscribe: cb => { + const getModal = () => + location.hash.includes('#modal=') + ? JSON.parse(decodeURIComponent(escape(atob(location.hash.replace('#modal=', ''))))) + : null + + cb(getModal()) + + return globalHistory.listen(() => cb(getModal())) + }, + set: data => { + let path = location.pathname + if (data) { + path += '#modal=' + btoa(unescape(encodeURIComponent(JSON.stringify(data)))) + } + + navigate(path) + }, +} + +// Settings, alerts, etc + +export const settings = synced("coracle/settings", { + showLinkPreviews: true, + dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, +}) diff --git a/src/partials/Anchor.svelte b/src/partials/Anchor.svelte index 03c6cced..5630bc14 100644 --- a/src/partials/Anchor.svelte +++ b/src/partials/Anchor.svelte @@ -3,12 +3,16 @@ import {switcher} from "hurdak/lib/hurdak" export let external = false + export let loading = false export let type = "anchor" export let href = null - const className = cx( + let className + + $: className = cx( $$props.class, - "cursor-pointer", + "cursor-pointer transition-all", + {"opacity-50": loading}, switcher(type, { anchor: "underline", button: "py-2 px-4 rounded bg-white text-accent", diff --git a/src/partials/Compose.svelte b/src/partials/Compose.svelte index 15a00fe1..fea9088b 100644 --- a/src/partials/Compose.svelte +++ b/src/partials/Compose.svelte @@ -3,7 +3,7 @@ import {fuzzy} from "src/util/misc" import {fromParentOffset} from "src/util/html" import Badge from "src/partials/Badge.svelte" - import {people} from "src/relay" + import {people} from "src/agent/data" export let onSubmit diff --git a/src/partials/Like.svelte b/src/partials/Like.svelte index daaec311..4ae2e6b3 100644 --- a/src/partials/Like.svelte +++ b/src/partials/Like.svelte @@ -1,11 +1,11 @@ diff --git a/src/routes/Alerts.svelte b/src/routes/Alerts.svelte index e07684f3..76d8ce57 100644 --- a/src/routes/Alerts.svelte +++ b/src/routes/Alerts.svelte @@ -2,10 +2,11 @@ import {propEq, sortBy} from 'ramda' import {onMount} from 'svelte' import {fly} from 'svelte/transition' - import {alerts} from 'src/state/app' - import {findReply, isLike} from 'src/util/nostr' - import relay, {people, user} from 'src/relay' import {now} from 'src/util/misc' + import {findReply, isLike} from 'src/util/nostr' + import {getPerson, user} from 'src/agent' + import {alerts} from 'src/app' + import query from 'src/app/query' import Spinner from "src/partials/Spinner.svelte" import Note from 'src/partials/Note.svelte' import Like from 'src/partials/Like.svelte' @@ -13,9 +14,9 @@ let annotatedNotes = [] onMount(async () => { - alerts.set({since: now()}) + alerts.since.set(now()) - const events = await relay.filterEvents({ + const events = await query.filterEvents({ kinds: [1, 7], '#p': [$user.pubkey], customFilter: e => { @@ -33,7 +34,7 @@ } }) - const notes = await relay.annotateChunk( + const notes = await query.annotateChunk( events.filter(propEq('kind', 1)) ) @@ -42,8 +43,8 @@ .filter(e => e.kind === 7) .map(async e => ({ ...e, - person: $people[e.pubkey] || {pubkey: e.pubkey}, - parent: await relay.findNote(findReply(e)), + person: getPerson(e.pubkey), + parent: await query.findNote(findReply(e)), })) ) @@ -64,8 +65,6 @@ .concat(Object.values(likesById)) ) }) - - alerts.set({since: now()})
      diff --git a/src/routes/Keys.svelte b/src/routes/Keys.svelte index d192b3ff..1c7f4835 100644 --- a/src/routes/Keys.svelte +++ b/src/routes/Keys.svelte @@ -6,8 +6,8 @@ import {hexToBech32} from "src/util/misc" import Input from "src/partials/Input.svelte" import Anchor from "src/partials/Anchor.svelte" - import toast from "src/state/toast" - import {user} from "src/relay" + import {user} from "src/agent" + import {toast} from "src/app" const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/' const delegationUrl = 'https://github.com/nostr-protocol/nips/blob/b62aa418dee13aac1899ea7c6946a0f55dd7ee84/26.md' diff --git a/src/routes/Login.svelte b/src/routes/Login.svelte index c1f8fc70..ac9cb5a1 100644 --- a/src/routes/Login.svelte +++ b/src/routes/Login.svelte @@ -7,11 +7,11 @@ import {copyToClipboard} from "src/util/html" import Anchor from "src/partials/Anchor.svelte" import Input from "src/partials/Input.svelte" - import toast from "src/state/toast" - import relay, {connections} from 'src/relay' + import {toast, login} from "src/app" let nsec = '' let hasExtension = false + let loading = false onMount(() => { setTimeout(() => { @@ -32,13 +32,11 @@ } const logIn = async ({privkey, pubkey}) => { - relay.login({privkey, pubkey}) + loading = true - if ($connections.length === 0) { - navigate('/relays') - } else { - navigate('/notes/network') - } + await login({privkey, pubkey}) + + navigate('/notes/network') } const logInWithExtension = async () => { @@ -47,7 +45,7 @@ if (!pubkey.match(/[a-z0-9]{64}/)) { toast.show("error", "Sorry, but that's an invalid public key.") } else { - logIn({pubkey}) + await logIn({pubkey}) } } @@ -57,7 +55,7 @@ if (!privkey.match(/[a-z0-9]{64}/)) { toast.show("error", "Sorry, but that's an invalid private key.") } else { - logIn({privkey, pubkey: getPublicKey(privkey)}) + await logIn({privkey, pubkey: getPublicKey(privkey)}) } } @@ -98,6 +96,8 @@
    - Log In + + Log In + diff --git a/src/routes/Logout.svelte b/src/routes/Logout.svelte index 784e751b..dde33583 100644 --- a/src/routes/Logout.svelte +++ b/src/routes/Logout.svelte @@ -1,23 +1,14 @@ -{#if $connections.length === 0} -
    -
    - You aren't yet connected to any relays. Please click here to get started. -
    -
    -{:else if $user} - -{#if activeTab === 'network'} - -{:else} - -{/if} -{:else} +{#if !$user}
    Don't have an account? Click here to join the nostr network.
    - {/if} + + +{#if activeTab === 'network'} + +{:else} + +{/if} diff --git a/src/routes/Person.svelte b/src/routes/Person.svelte index be5bda27..41ed8c90 100644 --- a/src/routes/Person.svelte +++ b/src/routes/Person.svelte @@ -11,32 +11,34 @@ import Notes from "src/views/person/Notes.svelte" import Likes from "src/views/person/Likes.svelte" import Network from "src/views/person/Network.svelte" - import {modal} from "src/state/app" - import relay, {user, people} from 'src/relay' + import {getPerson, listen, user} from "src/agent" + import {modal, getRelays} from "src/app" + import loaders from "src/app/loaders" + import cmd from "src/app/cmd" export let pubkey export let activeTab let subs = [] - let following = $user && find(t => t[1] === pubkey, $user.petnames) + let following = find(t => t[1] === pubkey, $user?.petnames || []) let followers = new Set() let followersCount = 0 let person $: { - person = $people[pubkey] || {pubkey} + person = getPerson(pubkey) || {pubkey} } onMount(async () => { - subs.push(await relay.pool.listenForEvents( - 'routes/Person', + subs.push(await listen( + getRelays(), [{kinds: [1, 5, 7], authors: [pubkey], since: now()}, {kinds: [0, 3, 12165], authors: [pubkey]}], - when(propEq('kind', 1), relay.loadNoteContext) + when(propEq('kind', 1), loaders.loadNoteContext) )) - subs.push(await relay.pool.listenForEvents( - 'routes/Person/followers', + subs.push(await listen( + getRelays(), [{kinds: [3], '#p': [pubkey]}], e => { followers.add(e.pubkey) @@ -58,18 +60,18 @@ following = true // Make sure our follow list is up to date - await relay.pool.loadPeople([$user.pubkey], {kinds: [3]}) + await loaders.loadPeople([$user.pubkey], {kinds: [3]}) - relay.cmd.addPetname($user, pubkey, person.name) + cmd.addPetname($user, pubkey, person.name) } const unfollow = async () => { following = false // Make sure our follow list is up to date - await relay.pool.loadPeople([$user.pubkey], {kinds: [3]}) + await loaders.loadPeople([$user.pubkey], {kinds: [3]}) - relay.cmd.removePetname($user, pubkey) + cmd.removePetname($user, pubkey) } const openAdvanced = () => { @@ -97,7 +99,7 @@ Edit - {:else} + {:else if $user.petnames}
    {#if following} @@ -123,7 +125,7 @@ {:else if activeTab === 'likes'} {:else if activeTab === 'network'} -{#if person} +{#if person?.petnames} {:else}
    diff --git a/src/routes/Profile.svelte b/src/routes/Profile.svelte index b3b1fb2a..d0baccf4 100644 --- a/src/routes/Profile.svelte +++ b/src/routes/Profile.svelte @@ -8,8 +8,9 @@ import Textarea from "src/partials/Textarea.svelte" import Anchor from "src/partials/Anchor.svelte" import Button from "src/partials/Button.svelte" - import toast from "src/state/toast" - import relay, {user} from "src/relay" + import {user} from "src/agent" + import {toast} from "src/app" + import cmd from "src/app/cmd" let values = {picture: null, about: null, name: null} @@ -37,7 +38,7 @@ const submit = async event => { event.preventDefault() - await relay.cmd.updateUser(values) + await cmd.updateUser(values) navigate(`/people/${$user.pubkey}/profile`) diff --git a/src/routes/RelayList.svelte b/src/routes/RelayList.svelte index aee92b29..789bc0cf 100644 --- a/src/routes/RelayList.svelte +++ b/src/routes/RelayList.svelte @@ -1,25 +1,33 @@
    @@ -34,7 +42,7 @@

    Your relays

    {#each ($knownRelays || []) as r} - {#if $connections.includes(r.url)} + {#if relays.includes(r.url)}
    {r.name || r.url} @@ -59,7 +67,7 @@
    {/if} {#each (search(q) || []).slice(0, 50) as r} - {#if !$connections.includes(r.url)} + {#if !relays.includes(r.url)}
    {r.name || r.url} diff --git a/src/routes/Settings.svelte b/src/routes/Settings.svelte index f21218e2..6f553d83 100644 --- a/src/routes/Settings.svelte +++ b/src/routes/Settings.svelte @@ -5,9 +5,8 @@ import Toggle from "src/partials/Toggle.svelte" import Input from "src/partials/Input.svelte" import Button from "src/partials/Button.svelte" - import {settings} from "src/state/app" - import toast from "src/state/toast" - import {user} from "src/relay" + import {user} from 'src/agent' + import {toast, settings} from "src/app" let values = {...$settings} diff --git a/src/state/app.js b/src/state/app.js deleted file mode 100644 index 2469adbd..00000000 --- a/src/state/app.js +++ /dev/null @@ -1,96 +0,0 @@ -import {writable, get} from 'svelte/store' -import {navigate} from "svelte-routing" -import {globalHistory} from "svelte-routing/src/history" -import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc" -import relay from 'src/relay' - -// Modals - -export const modal = { - subscribe: cb => { - const getModal = () => - location.hash.includes('#modal=') - ? JSON.parse(decodeURIComponent(escape(atob(location.hash.replace('#modal=', ''))))) - : null - - cb(getModal()) - - return globalHistory.listen(() => cb(getModal())) - }, - set: data => { - let path = location.pathname - if (data) { - path += '#modal=' + btoa(unescape(encodeURIComponent(JSON.stringify(data)))) - } - - navigate(path) - }, -} - -// Settings, alerts, etc - -export const settings = writable({ - showLinkPreviews: true, - dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, - ...getLocalJson("coracle/settings"), -}) - -settings.subscribe($settings => { - setLocalJson("coracle/settings", $settings) -}) - -export const alerts = writable({ - since: now() - timedelta(30, 'days'), - ...getLocalJson("coracle/alerts"), -}) - -alerts.subscribe($alerts => { - setLocalJson("coracle/alerts", $alerts) -}) - -// Populate relays initially. Hardcode some, load the rest asynchronously - -fetch(get(settings).dufflepudUrl + '/relay').then(r => r.json()).then(({relays}) => { - for (const url of relays) { - relay.db.relays.put({url}) - } -}) - -const defaultRelays = [ - 'wss://no.contry.xyz', - 'wss://nostr.ethtozero.fr', - 'wss://relay.nostr.ro', - 'wss://nostr.actn.io', - 'wss://relay.realsearch.cc', - 'wss://nostr.mrbits.it', - 'wss://relay.nostr.vision', - 'wss://nostr.massmux.com', - 'wss://nostr.robotechy.com', - 'wss://satstacker.cloud', - 'wss://relay.kronkltd.net', - 'wss://nostr.developer.li', - 'wss://nostr.vulpem.com', - 'wss://nostr.openchain.fr', - 'wss://nostr-01.bolt.observer', - 'wss://nostr.oxtr.dev', - 'wss://nostr.zebedee.cloud', - 'wss://nostr-verif.slothy.win', - 'wss://nostr.rewardsbunny.com', - 'wss://nostr.onsats.org', - 'wss://relay.boring.surf', - 'wss://no.str.watch', - 'wss://relay.nostr.pro', - 'wss://nostr.ono.re', - 'wss://nostr.rocks', - 'wss://btc.klendazu.com', - 'wss://nostr-relay.untethr.me', - 'wss://nostr.orba.ca', - 'wss://sg.qemura.xyz', - 'wss://nostr.hyperlingo.com', - 'wss://nostr.d11n.net', - 'wss://relay.nostr.express', -] - -for (const url of defaultRelays) { - relay.db.relays.put({url}) -} diff --git a/src/state/toast.js b/src/state/toast.js deleted file mode 100644 index 001210eb..00000000 --- a/src/state/toast.js +++ /dev/null @@ -1,19 +0,0 @@ -import prop from "ramda/src/prop" -import {uuid} from "hurdak/lib/hurdak" -import {writable, get} from "svelte/store" - -export const store = writable(null) - -export default { - show: (type, message, timeout = 5) => { - const id = uuid() - - store.set({id, type, message}) - - setTimeout(() => { - if (prop("id", get(store)) === id) { - store.set(null) - } - }, timeout * 1000) - }, -} diff --git a/src/util/data.js b/src/util/data.js deleted file mode 100644 index 4885f01f..00000000 --- a/src/util/data.js +++ /dev/null @@ -1,35 +0,0 @@ -const fees = { - items: [ - {amount: 10, type: "content/rich"}, - {amount: 100, type: "content/image"}, - {amount: 1000, type: "content/video"}, - {amount: 1, type: "vote/create"}, - ], -} - -export const servers = [ - { - name: "Test Relay", - url: "http://localhost:8485", - description: "My local relay", - fees, - }, - { - name: "Bitcoin Hackers", - url: "http://localhost:3001", - description: "Dudes who build software on and around the soundest money on earth.", - fees, - }, - { - name: "Moscow Kirk", - url: "http://localhost:3002", - description: "The Moscow, ID Church Community.", - fees, - }, - { - name: "Dogwood Meta", - url: "http://localhost:3003", - description: "A place to talk about the network itself.", - fees, - }, -] diff --git a/src/util/misc.js b/src/util/misc.js index bbd2ccad..594944ad 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,7 +1,10 @@ import {Buffer} from 'buffer' import {bech32} from 'bech32' -import {pluck} from "ramda" +import {debounce} from 'throttle-debounce' +import {pluck, sortBy} from "ramda" import Fuse from "fuse.js/dist/fuse.min.js" +import {writable} from 'svelte/store' +import {isObject} from 'hurdak/lib/hurdak' export const fuzzy = (data, opts = {}) => { const fuse = new Fuse(data, opts) @@ -17,6 +20,8 @@ export const getLocalJson = k => { try { return JSON.parse(localStorage.getItem(k)) } catch (e) { + console.warn(`Unable to parse ${k}: ${e}`) + return null } } @@ -25,7 +30,7 @@ export const setLocalJson = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)) } catch (e) { - // pass + console.warn(`Unable to set ${k}: ${e}`) } } @@ -146,3 +151,28 @@ export const hexToBech32 = (prefix, hex) => export const bech32ToHex = b32 => Buffer.from(bech32.fromWords(bech32.decode(b32).words)).toString('hex') + +export const synced = (key, defaultValue = null) => { + // If it's an object, merge defaults + const store = writable( + isObject(defaultValue) + ? {...defaultValue, ...getLocalJson(key)} + : (getLocalJson(key) || defaultValue) + ) + + store.subscribe($value => setLocalJson(key, $value)) + + return store +} + +export const shuffle = sortBy(() => Math.random() > 0.5) + +export const batch = (t, f) => { + const xs = [] + const cb = debounce(t, () => f(xs.splice(0))) + + return x => { + xs.push(x) + cb() + } +} diff --git a/src/util/nostr.js b/src/util/nostr.js index 3cad2aa4..7ac824b2 100644 --- a/src/util/nostr.js +++ b/src/util/nostr.js @@ -57,3 +57,17 @@ export const displayPerson = p => { } export const isLike = content => ['', '+', '🤙', '👍', '❤️'].includes(content) + +export const isAlert = (e, pubkey) => { + // Don't show people's own stuff + if (e.pubkey === pubkey) { + return false + } + + // Only notify users about positive reactions + if (e.kind === 7 && !isLike(e.content)) { + return false + } + + return true +} diff --git a/src/views/NoteDetail.svelte b/src/views/NoteDetail.svelte index f2a42878..6492a285 100644 --- a/src/views/NoteDetail.svelte +++ b/src/views/NoteDetail.svelte @@ -1,9 +1,13 @@ {#if search}
      - {#await Promise.all(search(q).slice(0, 30).map(n => relay.findNote(n.id)))} + {#await Promise.all(search(q).slice(0, 30).map(n => query.findNote(n.id)))} {:then results} {#each results as e (e.id)} diff --git a/src/views/SearchPeople.svelte b/src/views/SearchPeople.svelte index 9c295145..1517a8fc 100644 --- a/src/views/SearchPeople.svelte +++ b/src/views/SearchPeople.svelte @@ -5,7 +5,7 @@ import {fuzzy} from "src/util/misc" import {renderContent} from "src/util/html" import {displayPerson} from "src/util/nostr" - import {user, people} from 'src/relay' + import {user, people} from 'src/agent' export let q diff --git a/src/views/notes/Global.svelte b/src/views/notes/Global.svelte index 41974ed4..ce40176c 100644 --- a/src/views/notes/Global.svelte +++ b/src/views/notes/Global.svelte @@ -1,24 +1,25 @@