diff --git a/ROADMAP.md b/ROADMAP.md index 86fea029..32c26160 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,20 +1,15 @@ # Current -- [ ] Fix re-connects - [ ] Fix memory usage - - Re-write database - - Use LRU cache and persist that instead. Use purgeStale/dump/load - - Split state persistence elsewhere - - Keep it focused to abstract interface, split actual tables out elsewhere - - Put all other state in same place - - Re-write to use arrays with an index of id to index - - Fix compatibility, or clear data on first load of new version - - Add table of user events, derive profile from this using `watch`. - - Refine sync, set up some kind of system where we register tables with events coming in - - People.petnames is massive. Split people caches up - - Display/picture/about - - Minimal zapper info - - Drop petnames + - [x] Add table of user events, derive profile from this using `watch`. + - [ ] Remove petnames from users, retrieve lazily. Then, increase people table size + - [ ] Make zapper info more compact + - [ ] Move settings storage to an encrypted event https://github.com/nostr-protocol/nips/blob/master/78.md + - [ ] Migrate + - [ ] Test + - [ ] Test that relays/follows made as anon don't stomp user settings on login + - [ ] Test anonymous usage, public key only usage +- [ ] Fix re-connects - [ ] Show loading/success on zap invoice screen - [ ] Fix iOS/safari/firefox - [ ] Update https://nostr.com/clients/coracle diff --git a/package.json b/package.json index ec1b7c77..c8da582d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "preview": "vite preview", "check:es": "eslint src/*/** --quiet", "check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error", - "check:fmt": "prettier --check $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)", + "check:fmt": "prettier --check $(git diff head --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)", "check": "run-p check:*", - "format": "prettier --write $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)", + "format": "prettier --write $(git diff head --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)", "watch": "find src -type f | entr -r" }, "devDependencies": { diff --git a/src/App.svelte b/src/App.svelte index 127732db..a8586479 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -14,12 +14,13 @@ import {timedelta, shuffle, now, sleep} from "src/util/misc" import {displayPerson, isLike} from "src/util/nostr" import cmd from "src/agent/cmd" - import {ready, onReady, relays} from "src/agent/state" + import {ready, onReady, relays} from "src/agent/tables" import keys from "src/agent/keys" import network from "src/agent/network" import pool from "src/agent/pool" import {getUserRelays, initializeRelayList} from "src/agent/relays" import sync from "src/agent/sync" + import * as tables from "src/agent/tables" import user from "src/agent/user" import {loadAppData} from "src/app" import alerts from "src/app/alerts" @@ -64,7 +65,7 @@ import AddRelay from "src/views/relays/AddRelay.svelte" import RelayCard from "src/views/relays/RelayCard.svelte" - Object.assign(window, {cmd, user, keys, network, pool, sync}) + Object.assign(window, {cmd, user, keys, network, pool, sync, tables}) export let url = "" diff --git a/src/agent/cmd.ts b/src/agent/cmd.ts index 7f59c76b..152c6bf3 100644 --- a/src/agent/cmd.ts +++ b/src/agent/cmd.ts @@ -1,14 +1,13 @@ -import {pick, last, prop, uniqBy} from 'ramda' -import {get} from 'svelte/store' -import {roomAttrs, displayPerson, findReplyId, findRootId} from 'src/util/nostr' -import {getPubkeyWriteRelays, getRelayForPersonHint, sampleRelays} from 'src/agent/relays' -import {getPersonWithFallback} from 'src/agent/state' -import pool from 'src/agent/pool' -import sync from 'src/agent/sync' -import keys from 'src/agent/keys' +import {pick, last, prop, uniqBy} from "ramda" +import {get} from "svelte/store" +import {roomAttrs, displayPerson, findReplyId, findRootId} from "src/util/nostr" +import {getPubkeyWriteRelays, getRelayForPersonHint, sampleRelays} from "src/agent/relays" +import {getPersonWithFallback} from "src/agent/tables" +import pool from "src/agent/pool" +import sync from "src/agent/sync" +import keys from "src/agent/keys" -const updateUser = updates => - new PublishableEvent(0, {content: JSON.stringify(updates)}) +const updateUser = updates => new PublishableEvent(0, {content: JSON.stringify(updates)}) const setRelays = newRelays => new PublishableEvent(10002, { @@ -16,18 +15,16 @@ const setRelays = newRelays => const t = ["r", r.url] if (!r.write) { - t.push('read') + t.push("read") } return t }), }) -const setPetnames = petnames => - new PublishableEvent(3, {tags: petnames}) +const setPetnames = petnames => new PublishableEvent(3, {tags: petnames}) -const setMutes = mutes => - new PublishableEvent(10000, {tags: mutes}) +const setMutes = mutes => new PublishableEvent(10000, {tags: mutes}) const createRoom = room => new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))}) @@ -58,7 +55,11 @@ const getReplyTags = n => { const {url} = getRelayForPersonHint(n.pubkey, n) const rootId = findRootId(n) || findReplyId(n) || n.id - return [["p", n.pubkey, url], ["e", n.id, url, 'reply'], ["e", rootId, url, 'root']] + return [ + ["p", n.pubkey, url], + ["e", n.id, url, "reply"], + ["e", rootId, url, "root"], + ] } const tagsFromParent = (n, newTags = []) => { @@ -66,21 +67,20 @@ const tagsFromParent = (n, newTags = []) => { return uniqBy( // Remove duplicates due to inheritance. Keep earlier ones - t => t.slice(0, 2).join(':'), + t => t.slice(0, 2).join(":"), // Mentions have to come first for interpolation to work newTags // Add standard reply tags .concat(getReplyTags(n)) // Inherit p and e tags, but remove marks and self-mentions .concat( - n.tags - .filter(t => { - if (t[1] === pubkey) return false - if (!["p", "e"].includes(t[0])) return false - if (['reply', 'root'].includes(last(t))) return false + n.tags.filter(t => { + if (t[1] === pubkey) return false + if (!["p", "e"].includes(t[0])) return false + if (["reply", "root"].includes(last(t))) return false - return true - }) + return true + }) ) ) } @@ -93,7 +93,7 @@ const createReply = (note, content, mentions = [], topics = []) => { const tags = tagsFromParent( note, mentions - .map(pk => ["p", pk, prop('url', getRelayForPersonHint(pk, note))]) + .map(pk => ["p", pk, prop("url", getRelayForPersonHint(pk, note))]) .concat(topics.map(t => ["t", t])) ) @@ -115,14 +115,13 @@ const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => { return new PublishableEvent(9734, {content, tags}) } -const deleteEvent = ids => - new PublishableEvent(5, {tags: ids.map(id => ["e", id])}) +const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id])}) // Utils class PublishableEvent { event: Record - constructor(kind, {content = '', tags = []}) { + constructor(kind, {content = "", tags = []}) { const pubkey = get(keys.pubkey) const createdAt = Math.round(new Date().valueOf() / 1000) @@ -139,7 +138,18 @@ class PublishableEvent { } export default { - updateUser, setRelays, setPetnames, setMutes, createRoom, updateRoom, - createChatMessage, createDirectMessage, createNote, createReaction, - createReply, requestZap, deleteEvent, PublishableEvent, + updateUser, + setRelays, + setPetnames, + setMutes, + createRoom, + updateRoom, + createChatMessage, + createDirectMessage, + createNote, + createReaction, + createReply, + requestZap, + deleteEvent, + PublishableEvent, } diff --git a/src/agent/network.ts b/src/agent/network.ts index 5df28706..e92a9e70 100644 --- a/src/agent/network.ts +++ b/src/agent/network.ts @@ -1,23 +1,26 @@ -import type {MyEvent} from 'src/util/types' -import {sortBy, 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' -import {batch, now, timedelta} from 'src/util/misc' +import type {MyEvent} from "src/util/types" +import {sortBy, 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" +import {batch, now, timedelta} from "src/util/misc" import { - getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores, - getRelaysForEventChildren, sampleRelays, -} from 'src/agent/relays' -import {people} from 'src/agent/state' -import pool from 'src/agent/pool' -import sync from 'src/agent/sync' + getRelaysForEventParent, + getAllPubkeyWriteRelays, + aggregateScores, + getRelaysForEventChildren, + sampleRelays, +} from "src/agent/relays" +import {people} from "src/agent/tables" +import pool from "src/agent/pool" +import sync from "src/agent/sync" const getStalePubkeys = pubkeys => { // If we're not reloading, only get pubkeys we don't already know about return uniq(pubkeys).filter(pubkey => { const p = people.get(pubkey) - return !p || p.updated_at < now() - timedelta(1, 'days') + return !p || p.updated_at < now() - timedelta(1, "days") }) } @@ -145,8 +148,8 @@ const streamContext = ({notes, onChunk, depth = 0}) => // Instead of recurring to depth, trampoline so we can batch requests while (events.length > 0 && depth > 0) { const chunk = events.splice(0) - const authors = getStalePubkeys(pluck('pubkey', chunk)) - const filter = [{kinds: [1, 7, 9735], '#e': pluck('id', chunk)}] as Array + const authors = getStalePubkeys(pluck("pubkey", chunk)) + 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 @@ -169,11 +172,11 @@ const streamContext = ({notes, onChunk, depth = 0}) => ) const applyContext = (notes, context) => { - context = 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 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) @@ -186,9 +189,9 @@ const applyContext = (notes, context) => { return { ...note, - replies: sortBy(e => -e.created_at, uniqBy(prop('id'), combinedReplies).map(annotate)), - reactions: uniqBy(prop('id'), combinedReactions), - zaps: uniqBy(prop('id'), combinedZaps), + replies: sortBy(e => -e.created_at, uniqBy(prop("id"), combinedReplies).map(annotate)), + reactions: uniqBy(prop("id"), combinedReactions), + zaps: uniqBy(prop("id"), combinedZaps), } } @@ -196,5 +199,10 @@ const applyContext = (notes, context) => { } export default { - listen, load, loadPeople, personKinds, loadParents, streamContext, applyContext, + listen, + load, + loadPeople, + loadParents, + streamContext, + applyContext, } diff --git a/src/agent/relays.ts b/src/agent/relays.ts index a3f93519..3435ed35 100644 --- a/src/agent/relays.ts +++ b/src/agent/relays.ts @@ -1,13 +1,13 @@ -import type {Relay} from 'src/util/types' -import LRUCache from 'lru-cache' -import {warn} from 'src/util/logger' -import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from 'ramda' -import {first} from 'hurdak/lib/hurdak' -import {Tags, isRelay, findReplyId} from 'src/util/nostr' -import {shuffle, fetchJson} from 'src/util/misc' -import {relays, routes} from 'src/agent/state' -import pool from 'src/agent/pool' -import user from 'src/agent/user' +import type {Relay} from "src/util/types" +import LRUCache from "lru-cache" +import {warn} from "src/util/logger" +import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from "ramda" +import {first} from "hurdak/lib/hurdak" +import {Tags, isRelay, findReplyId} from "src/util/nostr" +import {shuffle, fetchJson} from "src/util/misc" +import {relays, routes} from "src/agent/tables" +import pool from "src/agent/pool" +import user from "src/agent/user" // From Mike Dilger: // 1) Other people's write relays — pull events from people you follow, @@ -26,21 +26,21 @@ import user from 'src/agent/user' export const initializeRelayList = async () => { // Throw some hardcoded defaults in there await relays.bulkPatch([ - {url: 'wss://brb.io'}, - {url: 'wss://nostr.zebedee.cloud'}, - {url: 'wss://nostr-pub.wellorder.net'}, - {url: 'wss://relay.nostr.band'}, - {url: 'wss://nostr.pleb.network'}, - {url: 'wss://relay.nostrich.de'}, - {url: 'wss://relay.damus.io'}, + {url: "wss://brb.io"}, + {url: "wss://nostr.zebedee.cloud"}, + {url: "wss://nostr-pub.wellorder.net"}, + {url: "wss://relay.nostr.band"}, + {url: "wss://nostr.pleb.network"}, + {url: "wss://relay.nostrich.de"}, + {url: "wss://relay.damus.io"}, ]) // Load relays from nostr.watch via dufflepud try { - const url = import.meta.env.VITE_DUFFLEPUD_URL + '/relay' + const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay" const json = await fetchJson(url) - await relays.bulkPatch(map(objOf('url'), json.relays.filter(isRelay))) + await relays.bulkPatch(map(objOf("url"), json.relays.filter(isRelay))) } catch (e) { warn("Failed to fetch relays list", e) } @@ -52,49 +52,50 @@ const _getPubkeyRelaysCache = new LRUCache({max: 1000}) export const getPubkeyRelays = (pubkey, mode = null, routesOverride = null) => { const filter = mode ? {pubkey, mode} : {pubkey} - const key = [mode, pubkey].join(':') + const key = [mode, pubkey].join(":") let result = routesOverride || _getPubkeyRelaysCache.get(key) if (!result) { - result = routes.all(filter) - _getPubkeyRelaysCache.set(key, result) + result = routes.all(filter) + _getPubkeyRelaysCache.set(key, result) } - return sortByScore(map(pick(['url', 'score']), result)) + return sortByScore(map(pick(["url", "score"]), result)) } -export const getPubkeyReadRelays = pubkey => getPubkeyRelays(pubkey, 'read') +export const getPubkeyReadRelays = pubkey => getPubkeyRelays(pubkey, "read") -export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, 'write') +export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, "write") // Multiple pubkeys export const getAllPubkeyRelays = (pubkeys, mode = null) => { // As an optimization, filter the database once and group by pubkey const filter = mode ? {pubkey: pubkeys, mode} : {pubkey: pubkeys} - const routesByPubkey = groupBy(prop('pubkey'), routes.all(filter)) + const routesByPubkey = groupBy(prop("pubkey"), routes.all(filter)) return aggregateScores( - pubkeys.map( - pubkey => getPubkeyRelays(pubkey, mode, routesByPubkey[pubkey] || []) - ) + pubkeys.map(pubkey => getPubkeyRelays(pubkey, mode, routesByPubkey[pubkey] || [])) ) } -export const getAllPubkeyReadRelays = pubkeys => getAllPubkeyRelays(pubkeys, 'read') +export const getAllPubkeyReadRelays = pubkeys => getAllPubkeyRelays(pubkeys, "read") -export const getAllPubkeyWriteRelays = pubkeys => getAllPubkeyRelays(pubkeys, 'write') +export const getAllPubkeyWriteRelays = pubkeys => getAllPubkeyRelays(pubkeys, "write") // Current user -export const getUserRelays = () => - user.getRelays().map(assoc('score', 1)) +export const getUserRelays = () => user.getRelays().map(assoc("score", 1)) export const getUserReadRelays = () => - getUserRelays().filter(prop('read')).map(pick(['url', 'score'])) + getUserRelays() + .filter(prop("read")) + .map(pick(["url", "score"])) export const getUserWriteRelays = (): Array => - getUserRelays().filter(prop('write')).map(pick(['url', 'score'])) + getUserRelays() + .filter(prop("write")) + .map(pick(["url", "score"])) // Event-related special cases @@ -113,12 +114,10 @@ export const getRelaysForEventParent = event => { // will write replies there. However, this may include spam, so we may want // to read from the current user's network's read relays instead. export const getRelaysForEventChildren = event => { - return uniqByUrl(getPubkeyReadRelays(event.pubkey) - .concat({url: event.seen_on, score: 1})) + return uniqByUrl(getPubkeyReadRelays(event.pubkey).concat({url: event.seen_on, score: 1})) } -export const getRelayForEventHint = event => - ({url: event.seen_on, score: 1}) +export const getRelayForEventHint = event => ({url: event.seen_on, score: 1}) export const getRelayForPersonHint = (pubkey, event) => first(getPubkeyWriteRelays(pubkey)) || getRelayForEventHint(event) @@ -135,15 +134,14 @@ export const getEventPublishRelays = event => { return uniqByUrl(aggregateScores(relayChunks).concat(getUserWriteRelays())) } - // Utils -export const uniqByUrl = pipe(uniqBy(prop('url')), filter(prop('url'))) +export const uniqByUrl = pipe(uniqBy(prop("url")), filter(prop("url"))) export const sortByScore = sortBy(r => -r.score) export const sampleRelays = (relays, scale = 1) => { - let limit = user.getSetting('relayLimit') + let limit = user.getSetting("relayLimit") // Allow the caller to scale down how many relays we're bothering depending on // the use case, but only if we have enough relays to handle it @@ -159,21 +157,22 @@ export const sampleRelays = (relays, scale = 1) => { // If we're still under the limit, add user relays for good measure if (relays.length < limit) { - relays = relays.concat( - shuffle(getUserReadRelays()).slice(0, limit - relays.length) - ) + relays = relays.concat(shuffle(getUserReadRelays()).slice(0, limit - relays.length)) } return uniqByUrl(relays) } export const aggregateScores = relayGroups => { - const scores = {} as Record + const scores = {} as Record< + string, + { + score: number + count: number + weight?: number + weightedScore?: number + } + > for (const relays of relayGroups) { for (const relay of relays) { @@ -195,7 +194,6 @@ export const aggregateScores = relayGroups => { } return sortByScore( - Object.entries(scores) - .map(([url, {weightedScore}]) => ({url, score: weightedScore})) + Object.entries(scores).map(([url, {weightedScore}]) => ({url, score: weightedScore})) ) } diff --git a/src/agent/social.ts b/src/agent/social.ts index 11f14bd1..12b58a25 100644 --- a/src/agent/social.ts +++ b/src/agent/social.ts @@ -1,7 +1,7 @@ -import {uniq, without} from 'ramda' -import {Tags} from 'src/util/nostr' -import {getPersonWithFallback} from 'src/agent/state' -import user from 'src/agent/user' +import {uniq, without} from "ramda" +import {Tags} from "src/util/nostr" +import {getPersonWithFallback} from "src/agent/tables" +import user from "src/agent/user" export const getFollows = pubkey => Tags.wrap(getPersonWithFallback(pubkey).petnames).type("p").values().all() @@ -12,8 +12,7 @@ export const getNetwork = pubkey => { return uniq(without(follows, follows.flatMap(getFollows))) } -export const getUserFollows = (): Array => - Tags.wrap(user.getPetnames()).values().all() +export const getUserFollows = (): Array => Tags.wrap(user.getPetnames()).values().all() export const getUserNetwork = () => { const follows = getUserFollows() diff --git a/src/agent/storage.ts b/src/agent/storage.ts index d9df45c9..8ad3ecb6 100644 --- a/src/agent/storage.ts +++ b/src/agent/storage.ts @@ -1,20 +1,23 @@ -import {error} from 'src/util/logger' +import type {Writable} from "svelte/store" +import LRUCache from "lru-cache" +import {throttle} from "throttle-debounce" +import {objOf, is, without} from "ramda" +import {writable} from "svelte/store" +import {isObject, mapValues, ensurePlural} from "hurdak/lib/hurdak" +import {log, error} from "src/util/logger" +import {where} from "src/util/misc" -// Types +// ---------------------------------------------------------------------------- +// Localforage interface via web worker type Message = { topic: string payload: object } -// Plumbing +const worker = new Worker(new URL("./workers/database.js", import.meta.url), {type: "module"}) -const worker = new Worker( - new URL('./workers/database.js', import.meta.url), - {type: 'module'} -) - -worker.addEventListener('error', error) +worker.addEventListener("error", error) class Channel { id: string @@ -27,10 +30,10 @@ class Channel { } } - worker.addEventListener('message', this.onMessage) + worker.addEventListener("message", this.onMessage) } close() { - worker.removeEventListener('message', this.onMessage) + worker.removeEventListener("message", this.onMessage) } send(topic, payload) { worker.postMessage({channel: this.id, topic, payload}) @@ -50,12 +53,209 @@ const call = (topic, payload): Promise => { }) } -export const lf = async (method, ...args) => { - const message = await call('localforage.call', {method, args}) +const lf = async (method, ...args) => { + const message = await call("localforage.call", {method, args}) - if (message.topic !== 'localforage.return') { + if (message.topic !== "localforage.return") { throw new Error(`callLocalforage received invalid response: ${message}`) } return message.payload } + +export const setItem = (k, v) => lf("setItem", k, v) +export const removeItem = k => lf("removeItem", k) +export const getItem = k => lf("getItem", k) + +// ---------------------------------------------------------------------------- +// Database table abstraction, synced to worker storage + +type CacheEntry = [string, {value: any}] + +type TableOpts = { + maxEntries?: number + initialize?: (table: Table) => Promise> +} + +export const registry = {} as Record + +export class Table { + name: string + pk: string + opts: TableOpts + cache: LRUCache + listeners: Array<(Table) => void> + ready: Writable + constructor(name, pk, opts: TableOpts = {}) { + this.name = name + this.pk = pk + this.opts = {maxEntries: 1000, initialize: t => this.dump(), ...opts} + this.cache = new LRUCache({max: this.opts.maxEntries}) + this.listeners = [] + this.ready = writable(false) + + registry[name] = this + + // Sync from storage initially + ;(async () => { + const t = Date.now() + + this.cache.load((await this.opts.initialize(this)) || []) + this._notify() + + log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size} records)`) + + this.ready.set(true) + })() + } + _persist = throttle(4_000, () => { + setItem(this.name, this.cache.dump()) + }) + _notify() { + // Notify subscribers + for (const cb of this.listeners) { + cb(this) + } + + // Save to localstorage + this._persist() + } + subscribe(cb) { + cb = throttle(100, cb) + + this.listeners.push(cb) + + cb(this) + + return () => { + this.listeners = without([cb], this.listeners) + } + } + bulkPut(items) { + for (const item of items) { + const k = item[this.pk] + + if (!k) { + throw new Error(`Missing primary key on ${this.name}`) + } + + this.cache.set(k, item) + } + + this._persist() + } + put(item) { + this.bulkPut([item]) + } + bulkPatch(items) { + for (const item of items) { + const k = item[this.pk] + + if (!k) { + throw new Error(`Missing primary key on ${this.name}`) + } + + this.cache.set(k, {...this.cache.get(k), ...item}) + } + + this._persist() + } + patch(item) { + this.bulkPatch([item]) + } + bulkRemove(ks) { + for (const k of ks) { + this.cache.delete(k) + } + + this._persist() + } + remove(k) { + this.bulkRemove([k]) + } + async drop() { + this.cache.clear() + + return removeItem(this.name) + } + async dump() { + let data = (await getItem(this.name)) || [] + + // Backwards compat - we used to store objects rather than cache dump arrays + if (isObject(data)) { + data = Object.entries(mapValues(objOf("value"), data)) + } + + return data as Array + } + toArray() { + const result = [] + for (const item of this.cache.values()) { + result.push(item) + } + + return result + } + all(spec = {}) { + return this.toArray().filter(where(spec)) + } + find(spec = {}) { + return this.cache.find(where(spec)) + } + get(k) { + return this.cache.get(k) + } +} + +const listener = (() => { + let listeners = [] + + for (const table of Object.values(registry) as Array) { + table.subscribe(() => listeners.forEach(f => f(table.name))) + } + + return { + subscribe: f => { + listeners.push(f) + + return () => { + listeners = without([f], listeners) + } + }, + } +})() + +export const watch = (names, f) => { + names = ensurePlural(names) + + const store = writable(null) + const tables = names.map(name => registry[name]) + + // Initialize synchronously if possible + const initialValue = f(...tables) + if (is(Promise, initialValue)) { + initialValue.then(v => store.set(v)) + } else { + store.set(initialValue) + } + + // Debounce refresh so we don't get UI lag + const refresh = throttle(300, async () => store.set(await f(...tables))) + + // Listen for changes + listener.subscribe(name => { + if (names.includes(name)) { + refresh() + } + }) + + return store +} + +export const dropAll = async () => { + for (const table of Object.values(registry)) { + await table.drop() + + log(`Successfully dropped table ${table.name}`) + } +} diff --git a/src/agent/sync.ts b/src/agent/sync.ts index 96031b5f..0e047452 100644 --- a/src/agent/sync.ts +++ b/src/agent/sync.ts @@ -12,25 +12,28 @@ import { timedelta, hash, } from "src/util/misc" -import { - Tags, - roomAttrs, - isRelay, - isShareableRelay, - normalizeRelayUrl, -} from "src/util/nostr" -import {getPersonWithFallback, people, relays, rooms, routes} from "src/agent/state" +import {Tags, roomAttrs, isRelay, isShareableRelay, normalizeRelayUrl} from "src/util/nostr" +import {people, userEvents, relays, rooms, routes} from "src/agent/tables" import {uniqByUrl} from "src/agent/relays" +import user from "src/agent/user" const handlers = {} -const addHandler = (kind, f) => (handlers[kind] || []).push(f) +const addHandler = (kind, f) => { + handlers[kind] = handlers[kind] || [] + handlers[kind].push(f) +} const processEvents = async events => { + const userPubkey = user.getPubkey() const chunks = chunk(100, ensurePlural(events)) for (let i = 0; i < chunks.length; i++) { for (const event of chunks[i]) { + if (event.pubkey === userPubkey) { + userEvents.put(event) + } + for (const handler of handlers[event.kind] || []) { handler(event) } @@ -45,12 +48,19 @@ const processEvents = async events => { // People +const updatePerson = (pubkey, data) => { + people.patch({pubkey, updated_at: now(), ...data}) + + // If our pubkey matches, copy to our user's profile as well + if (pubkey === user.getPubkey()) { + user.profile.update($p => ({...$p, ...data})) + } +} + const verifyNip05 = (pubkey, as) => nip05.queryProfile(as).then(result => { if (result?.pubkey === pubkey) { - const person = getPersonWithFallback(pubkey) - - people.patch({...person, verified_as: as}) + updatePerson(pubkey, {verified_as: as}) if (result.relays?.length > 0) { const urls = result.relays.filter(isRelay) @@ -92,7 +102,7 @@ const verifyZapper = async (pubkey, address) => { const lnurl = lnurlEncode("lnurl", url) if (zapper?.allowsNostr && zapper?.nostrPubkey) { - people.patch({pubkey, zapper, lnurl}) + updatePerson(pubkey, {zapper, lnurl}) } } @@ -116,48 +126,59 @@ addHandler(0, e => { verifyZapper(e.pubkey, address.toLowerCase()) } - people.patch({ - pubkey: e.pubkey, - updated_at: now(), + updatePerson(e.pubkey, { kind0: {...person?.kind0, ...kind0}, kind0_updated_at: e.created_at, }) }) }) -addHandler(2, e => { - const person = people.get(e.pubkey) - - if (e.created_at < person?.relays_updated_at) { - return - } - - people.patch({ - pubkey: e.pubkey, - updated_at: now(), - relays_updated_at: e.created_at, - relays: uniqByUrl((person?.relays || []).concat({url: e.content})), - }) -}) - addHandler(3, e => { const person = people.get(e.pubkey) - if (e.created_at > (person?.petnames_updated_at || 0)) { - people.patch({ - pubkey: e.pubkey, - updated_at: now(), - petnames_updated_at: e.created_at, - petnames: e.tags.filter(t => t[0] === "p"), - }) + if (e.created_at < person?.petnames_updated_at) { + return } - if (e.created_at > (person.relays_updated_at || 0)) { - tryJson(() => { - people.patch({ - pubkey: e.pubkey, - relays_updated_at: e.created_at, - relays: Object.entries(JSON.parse(e.content)) + updatePerson(e.pubkey, { + petnames_updated_at: e.created_at, + petnames: e.tags.filter(t => t[0] === "p"), + }) +}) + +// User profile, except for events also handled for other users + +const profileHandler = (key, getValue) => e => { + const profile = user.getProfile() + + if (e.pubkey !== profile.pubkey) { + return + } + + const updated_at_key = `${key}_updated_at` + + if (e.created_at < profile?.[updated_at_key]) { + return + } + + user.profile.update($p => ({ + ...$p, + [key]: getValue(e, $p), + [updated_at_key]: e.created_at, + })) +} + +addHandler( + 2, + profileHandler("relays", (e, p) => uniqByUrl(p.relays.concat({url: e.content}))) +) + +addHandler( + 3, + profileHandler("relays", (e, p) => { + return ( + tryJson(() => { + return Object.entries(JSON.parse(e.content)) .map(([url, conditions]) => { const {write, read} = conditions as Record @@ -167,62 +188,36 @@ addHandler(3, e => { read: [false, "!"].includes(read) ? false : true, } }) - .filter(r => isRelay(r.url)), - }) - }) - } -}) - -addHandler(10000, e => { - const person = people.get(e.pubkey) - - if (e.created_at < person?.mutes_updated_at) { - return - } - - people.patch({ - pubkey: e.pubkey, - updated_at: now(), - mutes_updated_at: e.created_at, - mutes: e.tags, + .filter(r => isRelay(r.url)) + }) || p.relays + ) }) -}) +) + +addHandler( + 10000, + profileHandler("mutes", (e, p) => e.tags) +) // DEPRECATED -addHandler(12165, e => { - const person = people.get(e.pubkey) - - if (e.created_at < person?.mutes_updated_at) { - return - } - - people.patch({ - pubkey: e.pubkey, - updated_at: now(), - mutes_updated_at: e.created_at, - mutes: e.tags, +addHandler( + 10001, + profileHandler("relays", (e, p) => { + return e.tags.map(([url, read, write]) => ({url, read: read !== "!", write: write !== "!"})) }) -}) +) -addHandler(10002, e => { - const person = people.get(e.pubkey) - - if (e.created_at < person?.relays_updated_at) { - return - } - - people.patch({ - pubkey: e.pubkey, - updated_at: now(), - relays_updated_at: e.created_at, - relays: e.tags.map(([_, url, mode]) => { +addHandler( + 10002, + profileHandler("relays", (e, p) => { + return e.tags.map(([_, url, mode]) => { const read = (mode || "read") === "read" const write = (mode || "write") === "write" return {url, read, write} - }), + }) }) -}) +) // Rooms diff --git a/src/agent/table.ts b/src/agent/table.ts deleted file mode 100644 index 55870a91..00000000 --- a/src/agent/table.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type {Writable} from 'svelte/store' -import LRUCache from 'lru-cache' -import {throttle} from 'throttle-debounce' -import {objOf, is, without} from 'ramda' -import {writable} from 'svelte/store' -import {isObject, mapValues, ensurePlural} from 'hurdak/lib/hurdak' -import {log} from 'src/util/logger' -import {where} from 'src/util/misc' -import {lf} from 'src/agent/storage' - -// Local copy of data so we can provide a sync observable interface. The worker -// is just for storing data and processing expensive queries - -type CacheEntry = [string, {value: any}] - -type TableOpts = { - maxEntries?: number - initialize?: (table: Table) => Promise> -} - -export const registry = {} as Record - -export class Table { - name: string - pk: string - opts: TableOpts - cache: LRUCache - listeners: Array<(Table) => void> - ready: Writable - constructor(name, pk, opts: TableOpts = {}) { - this.name = name - this.pk = pk - this.opts = {maxEntries: 1000, initialize: t => this.dump(), ...opts} - this.cache = new LRUCache({max: this.opts.maxEntries}) - this.listeners = [] - this.ready = writable(false) - - registry[name] = this - - // Sync from storage initially - ;(async () => { - const t = Date.now() - - this.cache.load(await this.opts.initialize(this) || []) - this._notify() - - log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size} records)`) - - this.ready.set(true) - })() - } - _persist = throttle(4_000, () => { - lf('setItem', this.name, this.cache.dump()) - }) - _notify() { - // Notify subscribers - for (const cb of this.listeners) { - cb(this) - } - - // Save to localstorage - this._persist() - } - subscribe(cb) { - cb = throttle(100, cb) - - this.listeners.push(cb) - - cb(this) - - return () => { - this.listeners = without([cb], this.listeners) - } - } - bulkPut(items) { - for (const item of items) { - const k = item[this.pk] - - if (!k) { - throw new Error(`Missing primary key on ${this.name}`) - } - - this.cache.set(k, item) - } - - this._persist() - } - put(item) { - this.bulkPut([item]) - } - bulkPatch(items) { - for (const item of items) { - const k = item[this.pk] - - if (!k) { - throw new Error(`Missing primary key on ${this.name}`) - } - - this.cache.set(k, {...this.cache.get(k), ...item}) - } - - this._persist() - } - patch(item) { - this.bulkPatch([item]) - } - bulkRemove(ks) { - for (const k of ks) { - this.cache.delete(k) - } - - this._persist() - } - remove(k) { - this.bulkRemove([k]) - } - async drop() { - this.cache.clear() - - return lf('removeItem', this.name) - } - async dump() { - let data = await lf('getItem', this.name) || [] - - // Backwards compat - we used to store objects rather than cache dump arrays - if (isObject(data)) { - data = Object.entries(mapValues(objOf('value'), data)) - } - - return data as Array - } - toArray() { - const result = [] - for (const item of this.cache.values()) { - result.push(item) - } - - return result - } - all(spec = {}) { - return this.toArray().filter(where(spec)) - } - find(spec = {}) { - return this.cache.find(where(spec)) - } - get(k) { - return this.cache.get(k) - } -} - -// Helper to allow us to listen to changes of any given table - -const listener = (() => { - let listeners = [] - - for (const table of Object.values(registry) as Array
) { - table.subscribe(() => listeners.forEach(f => f(table.name))) - } - - return { - subscribe: f => { - listeners.push(f) - - return () => { - listeners = without([f], listeners) - } - }, - } -})() - -// Helper to re-run a query every time a given table changes - -export const watch = (names, f) => { - names = ensurePlural(names) - - const store = writable(null) - const tables = names.map(name => registry[name]) - - // Initialize synchronously if possible - const initialValue = f(...tables) - if (is(Promise, initialValue)) { - initialValue.then(v => store.set(v)) - } else { - store.set(initialValue) - } - - // Debounce refresh so we don't get UI lag - const refresh = throttle(300, async () => store.set(await f(...tables))) - - // Listen for changes - listener.subscribe(name => { - if (names.includes(name)) { - refresh() - } - }) - - return store -} - -// Methods that work on all tables - -export const dropAll = async () => { - for (const table of Object.values(registry)) { - await table.drop() - - log(`Successfully dropped table ${table.name}`) - } -} diff --git a/src/agent/state.ts b/src/agent/tables.ts similarity index 85% rename from src/agent/state.ts rename to src/agent/tables.ts index e12b333f..df5ae153 100644 --- a/src/agent/state.ts +++ b/src/agent/tables.ts @@ -1,7 +1,8 @@ import {pluck, all, identity} from "ramda" import {derived} from "svelte/store" -import {Table, registry} from "src/agent/table" +import {Table, registry} from "src/agent/storage" +export const userEvents = new Table("userEvents", "id") export const people = new Table("people", "pubkey") export const contacts = new Table("contacts", "pubkey") export const rooms = new Table("rooms", "id") @@ -21,5 +22,3 @@ export const onReady = cb => { } }) } - -(window as any).t = {people, contacts, rooms, alerts, relays, routes} diff --git a/src/agent/user.ts b/src/agent/user.ts index 71f44497..f658e19f 100644 --- a/src/agent/user.ts +++ b/src/agent/user.ts @@ -1,91 +1,56 @@ -import type {Person} from 'src/util/types' -import type {Readable} from 'svelte/store' -import {slice, identity, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from 'ramda' -import {findReplyId, findRootId} from 'src/util/nostr' -import {synced} from 'src/util/misc' -import {derived} from 'svelte/store' -import {people} from 'src/agent/state' -import keys from 'src/agent/keys' -import cmd from 'src/agent/cmd' +import type {Relay} from "src/util/types" +import type {Readable} from "svelte/store" +import {slice, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from "ramda" +import {findReplyId, findRootId} from "src/util/nostr" +import {synced} from "src/util/misc" +import {derived} from "svelte/store" +import keys from "src/agent/keys" +import cmd from "src/agent/cmd" -// Create a special wrapper to manage profile data, follows, and relays in the same -// way whether the user is logged in or not. This involves creating a store that we -// allow an anonymous user to write to, then once the user logs in we use that until -// we have actual event data for them, which we then prefer. For extra fun, we also -// sync this stuff to regular private variables so we don't have to constantly call -// `get` on our stores. - -let settingsCopy = null -let profileCopy = null -let petnamesCopy = [] -let relaysCopy = [] -let mutesCopy = [] - -const anonPetnames = synced('agent/user/anonPetnames', []) -const anonRelays = synced('agent/user/anonRelays', []) -const anonMutes = synced('agent/user/anonMutes', []) - -const settings = synced("agent/user/settings", { - relayLimit: 20, - defaultZap: 21, - showMedia: true, - reportAnalytics: true, - dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, +const profile = synced("agent/user/profile", { + pubkey: null, + kind0: null, + lnurl: null, + zapper: null, + settings: { + relayLimit: 20, + defaultZap: 21, + showMedia: true, + reportAnalytics: true, + dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, + }, + petnames: [], + relays: [], + mutes: [], }) -const profile = derived( - [keys.pubkey, people as Readable], - ([pubkey, t]) => pubkey ? (t.get(pubkey) || {pubkey}) : null -) as Readable - -const profileKeyWithDefault = (key, stores) => derived( - [profile, ...stores], - ([$profile, ...values]) => - $profile?.[key] || find(identity, values) -) - -const petnames = profileKeyWithDefault('petnames', [anonPetnames]) -const relays = profileKeyWithDefault('relays', [anonRelays]) - -// Backwards compat, migrate muffle to mute temporarily -const mutes = profileKeyWithDefault('mutes', [anonMutes, derived(profile, prop('muffle'))]) +const settings = derived(profile, prop("settings")) +const petnames = derived(profile, prop("petnames")) +const relays = derived(profile, prop("relays")) as Readable> +const mutes = derived(profile, prop("mutes")) const canPublish = derived( [keys.pubkey, relays], - ([$pubkey, $relays]) => - keys.canSign() && find(prop('write'), $relays) + ([$pubkey, $relays]) => keys.canSign() && find(prop("write"), $relays) ) -// Keep our copies up to date +// Keep a copy so we can avoid calling `get` all the time -settings.subscribe($settings => { - settingsCopy = $settings -}) +let profileCopy = null profile.subscribe($profile => { profileCopy = $profile }) -petnames.subscribe($petnames => { - petnamesCopy = $petnames +// Watch pubkey and add to profile + +keys.pubkey.subscribe($pubkey => { + if ($pubkey) { + profile.update($p => ({...$p, pubkey: $pubkey})) + } }) -mutes.subscribe($mutes => { - mutesCopy = $mutes -}) - -relays.subscribe($relays => { - relaysCopy = $relays -}) - -const user = { - // Settings - - settings, - getSettings: () => settingsCopy, - getSetting: k => settingsCopy[k], - dufflepud: path => `${settingsCopy.dufflepudUrl}${path}`, - +export default { // Profile profile, @@ -93,24 +58,36 @@ const user = { getProfile: () => profileCopy, getPubkey: () => profileCopy?.pubkey, + // Settings + + settings, + getSettings: () => profileCopy.settings, + getSetting: k => profileCopy.settings[k], + dufflepud: path => `${profileCopy.settings.dufflepudUrl}${path}`, + // Petnames petnames, - getPetnames: () => petnamesCopy, + getPetnames: () => profileCopy.petnames, petnamePubkeys: derived(petnames, map(nth(1))) as Readable>, updatePetnames(f) { - const $petnames = f(petnamesCopy) + const $petnames = f(profileCopy.petnames) - anonPetnames.set($petnames) + profile.update(assoc("petnames", $petnames)) if (profileCopy) { - return cmd.setPetnames($petnames).publish(relaysCopy) + return cmd.setPetnames($petnames).publish(profileCopy.relays) } }, addPetname(pubkey, url, name) { const tag = ["p", pubkey, url, name || ""] - return this.updatePetnames(pipe(reject(t => t[1] === pubkey), concat([tag]))) + return this.updatePetnames( + pipe( + reject(t => t[1] === pubkey), + concat([tag]) + ) + ) }, removePetname(pubkey) { return this.updatePetnames(reject(t => t[1] === pubkey)) @@ -119,11 +96,11 @@ const user = { // Relays relays, - getRelays: () => relaysCopy, + getRelays: () => profileCopy.relays, updateRelays(f) { - const $relays = f(relaysCopy) + const $relays = f(profileCopy.relays) - anonRelays.set($relays) + profile.update(assoc("relays", $relays)) if (profileCopy) { return cmd.setRelays($relays).publish($relays) @@ -136,28 +113,27 @@ const user = { return this.updateRelays(reject(whereEq({url}))) }, setRelayWriteCondition(url, write) { - return this.updateRelays(map(when(whereEq({url}), assoc('write', write)))) + return this.updateRelays(map(when(whereEq({url}), assoc("write", write)))) }, // Mutes mutes, - getMutes: () => mutesCopy, + getMutes: () => profileCopy.mutes, applyMutes: events => { - const m = new Set(mutesCopy.map(m => m[1])) + const m = new Set(profileCopy.mutes.map(m => m[1])) - return events.filter(e => - !(m.has(e.id) || m.has(e.pubkey) || m.has(findReplyId(e)) || m.has(findRootId(e))) + return events.filter( + e => !(m.has(e.id) || m.has(e.pubkey) || m.has(findReplyId(e)) || m.has(findRootId(e))) ) }, updateMutes(f) { - const $mutes = f(mutesCopy) - console.log(mutesCopy, $mutes) + const $mutes = f(profileCopy.mutes) - anonMutes.set($mutes) + profile.update(assoc("mutes", $mutes)) if (profileCopy) { - return cmd.setMutes($mutes.map(slice(0, 2))).publish(relaysCopy) + return cmd.setMutes($mutes.map(slice(0, 2))).publish(profileCopy.relays) } }, addMute(type, value) { @@ -172,5 +148,3 @@ const user = { return this.updateMutes(reject(t => t[1] === pubkey)) }, } - -export default user diff --git a/src/app/alerts.ts b/src/app/alerts.ts index b4465e7d..b86c413e 100644 --- a/src/app/alerts.ts +++ b/src/app/alerts.ts @@ -1,13 +1,13 @@ -import type {DisplayEvent} from 'src/util/types' -import {max, find, pluck, propEq, partition, uniq} from 'ramda' -import {derived} from 'svelte/store' -import {createMap} from 'hurdak/lib/hurdak' -import {synced, tryJson, now, timedelta} from 'src/util/misc' -import {Tags, personKinds, isAlert, asDisplayEvent, findReplyId} from 'src/util/nostr' -import {getUserReadRelays} from 'src/agent/relays' -import {alerts, contacts, rooms} from 'src/agent/state' -import {watch} from 'src/agent/table' -import network from 'src/agent/network' +import type {DisplayEvent} from "src/util/types" +import {max, find, pluck, propEq, partition, uniq} from "ramda" +import {derived} from "svelte/store" +import {createMap} from "hurdak/lib/hurdak" +import {synced, tryJson, now, timedelta} from "src/util/misc" +import {Tags, userKinds, isAlert, asDisplayEvent, findReplyId} from "src/util/nostr" +import {getUserReadRelays} from "src/agent/relays" +import {alerts, contacts, rooms} from "src/agent/tables" +import {watch} from "src/agent/storage" +import network from "src/agent/network" let listener @@ -20,25 +20,23 @@ type AlertEvent = DisplayEvent & { // State -const seenAlertIds = synced('app/alerts/seenAlertIds', []) +const seenAlertIds = synced("app/alerts/seenAlertIds", []) -export const lastChecked = synced('app/alerts/lastChecked', {}) +export const lastChecked = synced("app/alerts/lastChecked", {}) export const newAlerts = derived( - [watch('alerts', t => pluck('created_at', t.all()).reduce(max, 0)), lastChecked], + [watch("alerts", t => pluck("created_at", t.all()).reduce(max, 0)), lastChecked], ([$lastAlert, $lastChecked]) => $lastAlert > ($lastChecked.alerts || 0) ) export const newDirectMessages = derived( - [watch('contacts', t => t.all()), lastChecked], - ([contacts, $lastChecked]) => - Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts)) + [watch("contacts", t => t.all()), lastChecked], + ([contacts, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts)) ) export const newChatMessages = derived( - [watch('rooms', t => t.all()), lastChecked], - ([rooms, $lastChecked]) => - Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms)) + [watch("rooms", t => t.all()), lastChecked], + ([rooms, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms)) ) // Synchronization from events to state @@ -59,10 +57,15 @@ const processAlerts = async (pubkey, events) => { return } - const parents = createMap('id', await network.loadParents(events)) + const parents = createMap("id", await network.loadParents(events)) - const asAlert = (e): AlertEvent => - ({repliesFrom: [], likedBy: [], zappedBy: [], isMention: false, ...asDisplayEvent(e)}) + const asAlert = (e): AlertEvent => ({ + repliesFrom: [], + likedBy: [], + zappedBy: [], + isMention: false, + ...asDisplayEvent(e), + }) const isPubkeyChild = e => { const parentId = findReplyId(e) @@ -70,9 +73,9 @@ const processAlerts = async (pubkey, events) => { return parents[parentId]?.pubkey === pubkey } - const [replies, mentions] = partition(isPubkeyChild, events.filter(propEq('kind', 1))) - const likes = events.filter(propEq('kind', 7)) - const zaps = events.filter(propEq('kind', 9735)) + const [replies, mentions] = partition(isPubkeyChild, events.filter(propEq("kind", 1))) + const likes = events.filter(propEq("kind", 7)) + const zaps = events.filter(propEq("kind", 9735)) zaps.filter(isPubkeyChild).forEach(e => { const parent = parents[findReplyId(e)] @@ -107,7 +110,7 @@ const processAlerts = async (pubkey, events) => { } const processMessages = async (pubkey, events) => { - const messages = events.filter(propEq('kind', 4)) + const messages = events.filter(propEq("kind", 4)) if (messages.length === 0) { return @@ -133,7 +136,7 @@ const processMessages = async (pubkey, events) => { } const processChats = async (pubkey, events) => { - const messages = events.filter(propEq('kind', 42)) + const messages = events.filter(propEq("kind", 42)) if (messages.length === 0) { return @@ -159,8 +162,8 @@ const processChats = async (pubkey, events) => { const listen = async pubkey => { // Include an offset so we don't miss alerts on one relay but not another - const since = now() - timedelta(7, 'days') - const roomIds = pluck('id', rooms.all({joined: true})) + const since = now() - timedelta(7, "days") + const roomIds = pluck("id", rooms.all({joined: true})) if (listener) { listener.unsub() @@ -170,13 +173,13 @@ const listen = async pubkey => { delay: 10000, relays: getUserReadRelays(), filter: [ - {kinds: personKinds, authors: [pubkey], since}, + {kinds: userKinds, authors: [pubkey], since}, {kinds: [4], authors: [pubkey], since}, - {kinds: [1, 7, 4, 9735], '#p': [pubkey], since}, - {kinds: [42], '#e': roomIds, since}, + {kinds: [1, 7, 4, 9735], "#p": [pubkey], since}, + {kinds: [42], "#e": roomIds, since}, ], onChunk: async events => { - await network.loadPeople(pluck('pubkey', events)) + await network.loadPeople(pluck("pubkey", events)) await processMessages(pubkey, events) await processAlerts(pubkey, events) await processChats(pubkey, events) diff --git a/src/app/index.ts b/src/app/index.ts index 7aa4691e..cd46758f 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -1,15 +1,15 @@ -import type {DisplayEvent} from 'src/util/types' -import {omit, sortBy} from 'ramda' -import {createMap, ellipsize} from 'hurdak/lib/hurdak' -import {renderContent} from 'src/util/html' -import {displayPerson, findReplyId} from 'src/util/nostr' -import {getUserFollows} from 'src/agent/social' -import {getUserReadRelays} from 'src/agent/relays' -import {getPersonWithFallback} from 'src/agent/state' -import network from 'src/agent/network' -import keys from 'src/agent/keys' -import alerts from 'src/app/alerts' -import {routes, modal, toast} from 'src/app/ui' +import type {DisplayEvent} from "src/util/types" +import {omit, sortBy} from "ramda" +import {createMap, ellipsize} from "hurdak/lib/hurdak" +import {renderContent} from "src/util/html" +import {displayPerson, findReplyId} from "src/util/nostr" +import {getUserFollows} from "src/agent/social" +import {getUserReadRelays} from "src/agent/relays" +import {getPersonWithFallback} from "src/agent/tables" +import network from "src/agent/network" +import keys from "src/agent/keys" +import alerts from "src/app/alerts" +import {routes, modal, toast} from "src/app/ui" export const loadAppData = async pubkey => { if (getUserReadRelays().length > 0) { @@ -24,40 +24,37 @@ export const loadAppData = async pubkey => { export const login = (method, key) => { keys.login(method, key) - modal.set({type: 'login/connect', noEscape: true}) + modal.set({type: "login/connect", noEscape: true}) } export const renderNote = (note, {showEntire = false}) => { let content // Ellipsize - content = note.content.length > 500 && !showEntire - ? ellipsize(note.content, 500) - : note.content + content = note.content.length > 500 && !showEntire ? ellipsize(note.content, 500) : note.content // Escape html, replace urls content = renderContent(content) // Mentions - content = content - .replace(/#\[(\d+)\]/g, (tag, i) => { - if (!note.tags[parseInt(i)]) { - return tag - } + content = content.replace(/#\[(\d+)\]/g, (tag, i) => { + if (!note.tags[parseInt(i)]) { + return tag + } - const pubkey = note.tags[parseInt(i)][1] - const person = getPersonWithFallback(pubkey) - const name = displayPerson(person) - const path = routes.person(pubkey) + const pubkey = note.tags[parseInt(i)][1] + const person = getPersonWithFallback(pubkey) + const name = displayPerson(person) + const path = routes.person(pubkey) - return `@${name}` - }) + return `@${name}` + }) return content } export const mergeParents = (notes: Array) => { - const notesById = createMap('id', notes) as Record + const notesById = createMap("id", notes) as Record const childIds = [] for (const note of Object.values(notesById)) { @@ -94,8 +91,8 @@ export const publishWithToast = (relays, thunk) => } if (extra.length > 0) { - message += ` (${extra.join(', ')})` + message += ` (${extra.join(", ")})` } - toast.show('info', message, pending.size ? null : 5) + toast.show("info", message, pending.size ? null : 5) }) diff --git a/src/partials/Channel.svelte b/src/partials/Channel.svelte index 93f4e226..48614864 100644 --- a/src/partials/Channel.svelte +++ b/src/partials/Channel.svelte @@ -6,7 +6,7 @@ import {sleep, createScroller, Cursor} from "src/util/misc" import Spinner from "src/partials/Spinner.svelte" import user from "src/agent/user" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import network from "src/agent/network" export let loadMessages diff --git a/src/partials/Compose.svelte b/src/partials/Compose.svelte index d6c7a402..17755236 100644 --- a/src/partials/Compose.svelte +++ b/src/partials/Compose.svelte @@ -7,7 +7,7 @@ import {displayPerson} from "src/util/nostr" import {fromParentOffset} from "src/util/html" import Badge from "src/partials/Badge.svelte" - import {people} from "src/agent/state" + import {people} from "src/agent/tables" export let onSubmit diff --git a/src/routes/Alerts.svelte b/src/routes/Alerts.svelte index 51033107..33bf3b70 100644 --- a/src/routes/Alerts.svelte +++ b/src/routes/Alerts.svelte @@ -7,7 +7,7 @@ import Content from "src/partials/Content.svelte" import Alert from "src/views/alerts/Alert.svelte" import Mention from "src/views/alerts/Mention.svelte" - import {alerts} from "src/agent/state" + import {alerts} from "src/agent/tables" import user from "src/agent/user" import {lastChecked} from "src/app/alerts" diff --git a/src/routes/ChatDetail.svelte b/src/routes/ChatDetail.svelte index a4b8ea42..71d3f8cf 100644 --- a/src/routes/ChatDetail.svelte +++ b/src/routes/ChatDetail.svelte @@ -9,7 +9,7 @@ import user from "src/agent/user" import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays" import network from "src/agent/network" - import {watch} from "src/agent/table" + import {watch} from "src/agent/storage" import cmd from "src/agent/cmd" import {modal} from "src/app/ui" import {lastChecked} from "src/app/alerts" diff --git a/src/routes/ChatList.svelte b/src/routes/ChatList.svelte index c0c62567..c3d66d82 100644 --- a/src/routes/ChatList.svelte +++ b/src/routes/ChatList.svelte @@ -5,7 +5,7 @@ import Content from "src/partials/Content.svelte" import Anchor from "src/partials/Anchor.svelte" import ChatListItem from "src/views/chat/ChatListItem.svelte" - import {watch} from "src/agent/table" + import {watch} from "src/agent/storage" import network from "src/agent/network" import {getUserReadRelays} from "src/agent/relays" import {modal} from "src/app/ui" diff --git a/src/routes/Logout.svelte b/src/routes/Logout.svelte index 1740dc75..ddc59012 100644 --- a/src/routes/Logout.svelte +++ b/src/routes/Logout.svelte @@ -2,7 +2,7 @@ import {fly} from "svelte/transition" import Anchor from "src/partials/Anchor.svelte" import Content from "src/partials/Content.svelte" - import {dropAll} from "src/agent/table" + import {dropAll} from "src/agent/storage" let confirmed = false diff --git a/src/routes/MessagesDetail.svelte b/src/routes/MessagesDetail.svelte index a8ddcaf4..c70c5507 100644 --- a/src/routes/MessagesDetail.svelte +++ b/src/routes/MessagesDetail.svelte @@ -8,8 +8,8 @@ import Channel from "src/partials/Channel.svelte" import Anchor from "src/partials/Anchor.svelte" import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/state" - import {watch} from "src/agent/table" + import {getPersonWithFallback} from "src/agent/tables" + import {watch} from "src/agent/storage" import network from "src/agent/network" import keys from "src/agent/keys" import user from "src/agent/user" diff --git a/src/routes/MessagesList.svelte b/src/routes/MessagesList.svelte index 63697286..98ffe2e6 100644 --- a/src/routes/MessagesList.svelte +++ b/src/routes/MessagesList.svelte @@ -4,7 +4,7 @@ import Tabs from "src/partials/Tabs.svelte" import Content from "src/partials/Content.svelte" import MessagesListItem from "src/views/messages/MessagesListItem.svelte" - import {watch} from "src/agent/table" + import {watch} from "src/agent/storage" let activeTab = "messages" diff --git a/src/routes/PersonDetail.svelte b/src/routes/PersonDetail.svelte index 9153c0d0..fab0dca6 100644 --- a/src/routes/PersonDetail.svelte +++ b/src/routes/PersonDetail.svelte @@ -18,7 +18,7 @@ import user from "src/agent/user" import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays" import network from "src/agent/network" - import {getPersonWithFallback, people} from "src/agent/state" + import {getPersonWithFallback, people} from "src/agent/tables" import {routes, modal} from "src/app/ui" import PersonCircle from "src/partials/PersonCircle.svelte" diff --git a/src/routes/RelayDetail.svelte b/src/routes/RelayDetail.svelte index 13bdee72..26e498ab 100644 --- a/src/routes/RelayDetail.svelte +++ b/src/routes/RelayDetail.svelte @@ -7,7 +7,7 @@ import Content from "src/partials/Content.svelte" import Anchor from "src/partials/Anchor.svelte" import Feed from "src/views/feed/Feed.svelte" - import {relays} from "src/agent/state" + import {relays} from "src/agent/tables" import pool from "src/agent/pool" import user from "src/agent/user" diff --git a/src/util/misc.ts b/src/util/misc.ts index 35e675f9..b6d6e2e4 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -1,10 +1,25 @@ -import {bech32, utf8} from '@scure/base' -import {debounce, throttle} from 'throttle-debounce' -import {gt, aperture, path as getPath, allPass, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda" +import {bech32, utf8} from "@scure/base" +import {debounce, throttle} from "throttle-debounce" +import { + gt, + mergeDeepRight, + 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, round} from 'hurdak/lib/hurdak' -import {warn} from 'src/util/logger' +import {writable} from "svelte/store" +import {isObject, round} from "hurdak/lib/hurdak" +import {warn} from "src/util/logger" export const fuzzy = (data, opts = {}) => { const fuse = new Fuse(data, opts) @@ -16,7 +31,7 @@ export const fuzzy = (data, opts = {}) => { export const hash = s => Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)) -export const getLocalJson = (k) => { +export const getLocalJson = k => { try { return JSON.parse(localStorage.getItem(k)) } catch (e) { @@ -36,20 +51,29 @@ export const setLocalJson = (k, v) => { export const now = () => Math.round(new Date().valueOf() / 1000) -export const timedelta = (n, unit = 'seconds') => { +export const timedelta = (n, unit = "seconds") => { switch (unit) { - case 'seconds': case 'second': return n - case 'minutes': case 'minute': return n * 60 - case 'hours': case 'hour': return n * 60 * 60 - case 'days': case 'day': return n * 60 * 60 * 24 - default: throw new Error(`Invalid unit ${unit}`) + case "seconds": + case "second": + return n + case "minutes": + case "minute": + return n * 60 + case "hours": + case "hour": + return n * 60 * 60 + case "days": + case "day": + return n * 60 * 60 * 24 + default: + throw new Error(`Invalid unit ${unit}`) } } export const formatTimestamp = ts => { - const formatter = new Intl.DateTimeFormat('en-US', { - dateStyle: 'medium', - timeStyle: 'short', + const formatter = new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "short", }) return formatter.format(new Date(ts * 1000)) @@ -58,21 +82,21 @@ export const formatTimestamp = ts => { export const formatTimestampRelative = ts => { let unit let delta = now() - ts - if (delta < timedelta(1, 'minute')) { - unit = 'second' - } else if (delta < timedelta(1, 'hour')) { - unit = 'minute' - delta = Math.round(delta / timedelta(1, 'minute')) - } else if (delta < timedelta(2, 'day')) { - unit = 'hour' - delta = Math.round(delta / timedelta(1, 'hour')) + if (delta < timedelta(1, "minute")) { + unit = "second" + } else if (delta < timedelta(1, "hour")) { + unit = "minute" + delta = Math.round(delta / timedelta(1, "minute")) + } else if (delta < timedelta(2, "day")) { + unit = "hour" + delta = Math.round(delta / timedelta(1, "hour")) } else { - unit = 'day' - delta = Math.round(delta / timedelta(1, 'day')) + unit = "day" + delta = Math.round(delta / timedelta(1, "day")) } - const formatter = new Intl.RelativeTimeFormat('en-US', { - numeric: 'auto', + const formatter = new Intl.RelativeTimeFormat("en-US", { + numeric: "auto", }) return formatter.format(-delta, unit as Intl.RelativeTimeFormatUnit) @@ -137,15 +161,6 @@ export const createScroller = (loadMore, {reverse = false} = {}) => { export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)] -export const getLastSync = (k, fallback = 0) => { - const key = `${k}/lastSync` - const lastSync = getLocalJson(key) || fallback - - setLocalJson(key, now()) - - return lastSync -} - export class Cursor { until: number limit: number @@ -179,17 +194,14 @@ export class Cursor { // feed. Multiply it by the number of events we have but scale down to avoid // blowing past big gaps due to misbehaving relays skewing the results. Trim off // outliers and scale based on results/requests to help with that - const timestamps = sortBy(identity, pluck('created_at', events)) + const timestamps = sortBy(identity, pluck("created_at", events)) const gaps = aperture(2, timestamps).map(([a, b]) => b - a) const high = quantile(gaps, 0.5) const gap = avg(gaps.filter(gt(high))) // If we're just warming up, scale the window down even further to avoid // blowing past the most relevant time period - const scale = ( - Math.min(1, Math.log10(events.length)) - * Math.min(1, Math.log10(this.count + 1)) - ) + const scale = Math.min(1, Math.log10(events.length)) * Math.min(1, Math.log10(this.count + 1)) // Only paginate part of the way so we can avoid missing stuff this.until -= Math.round(gap * scale * this.limit) @@ -201,8 +213,8 @@ export const synced = (key, defaultValue = null) => { // If it's an object, merge defaults const store = writable( isObject(defaultValue) - ? {...defaultValue, ...getLocalJson(key)} - : (getLocalJson(key) || defaultValue) + ? mergeDeepRight(defaultValue, getLocalJson(key) || {}) + : getLocalJson(key) || defaultValue ) store.subscribe(debounce(1000, $value => setLocalJson(key, $value))) @@ -210,7 +222,7 @@ export const synced = (key, defaultValue = null) => { return store } -export const shuffle = sortBy(() => Math.random() > 0.5) +export const shuffle = sortBy(() => Math.random() > 0.5) export const batch = (t, f) => { const xs = [] @@ -236,42 +248,43 @@ export const avg = xs => sum(xs) / xs.length export const where = filters => allPass( - Object.entries(filters) - .map(([key, value]) => { - /* eslint prefer-const: 0 */ - let [field, operator = 'eq'] = key.split(':') - let test, modifier = identity, parts = field.split('.') + Object.entries(filters).map(([key, value]) => { + /* eslint prefer-const: 0 */ + let [field, operator = "eq"] = key.split(":") + let test, + modifier = identity, + parts = field.split(".") - if (operator.startsWith('!')) { - operator = operator.slice(1) - modifier = complement - } + if (operator.startsWith("!")) { + operator = operator.slice(1) + modifier = complement + } - if (operator === 'eq' && is(Array, value)) { - test = v => (value as Array).includes(v) - } else if (operator === 'eq') { - test = equals(value) - } else if (operator === 'lt') { - test = v => (v || 0) < value - } else if (operator === 'lte') { - test = v => (v || 0) <= value - } else if (operator === 'gt') { - test = v => (v || 0) > value - } else if (operator === 'gte') { - test = v => (v || 0) >= value - } else if (operator === 'nil') { - test = isNil - } else { - throw new Error(`Invalid operator ${operator}`) - } + if (operator === "eq" && is(Array, value)) { + test = v => (value as Array).includes(v) + } else if (operator === "eq") { + test = equals(value) + } else if (operator === "lt") { + test = v => (v || 0) < value + } else if (operator === "lte") { + test = v => (v || 0) <= value + } else if (operator === "gt") { + test = v => (v || 0) > value + } else if (operator === "gte") { + test = v => (v || 0) >= value + } else if (operator === "nil") { + test = isNil + } else { + throw new Error(`Invalid operator ${operator}`) + } - return pipe(getPath(parts), modifier(test)) - }) + return pipe(getPath(parts), modifier(test)) + }) ) // https://stackoverflow.com/a/21682946 export const stringToHue = value => { - let hash = 0; + let hash = 0 for (let i = 0; i < value.length; i++) { hash = value.charCodeAt(i) + ((hash << 5) - hash) hash = hash & hash @@ -303,14 +316,12 @@ export const tryFunc = (f, ignore = null) => { } } -export const tryJson = f => tryFunc(f, 'JSON') -export const tryFetch = f => tryFunc(f, 'fetch') +export const tryJson = f => tryFunc(f, "JSON") +export const tryFetch = f => tryFunc(f, "fetch") -export const union = (...sets) => - new Set(sets.flatMap(s => Array.from(s))) +export const union = (...sets) => new Set(sets.flatMap(s => Array.from(s))) -export const difference = (a, b) => - new Set(Array.from(a).filter(x => !b.has(x))) +export const difference = (a, b) => new Set(Array.from(a).filter(x => !b.has(x))) export const quantile = (a, q) => { const sorted = sortBy(identity, a) @@ -334,7 +345,7 @@ export const fetchJson = async (url, opts: FetchOpts = {}) => { opts.headers = {} } - opts.headers['Accept'] = 'application/json' + opts.headers["Accept"] = "application/json" const res = await fetch(url, opts as RequestInit) const json = await res.json() @@ -344,14 +355,14 @@ export const fetchJson = async (url, opts: FetchOpts = {}) => { export const postJson = async (url, data, opts: FetchOpts = {}) => { if (!opts.method) { - opts.method = 'POST' + opts.method = "POST" } if (!opts.headers) { opts.headers = {} } - opts.headers['Content-Type'] = 'application/json' + opts.headers["Content-Type"] = "application/json" opts.body = JSON.stringify(data) return fetchJson(url, opts) @@ -362,20 +373,19 @@ export const uploadFile = (url, fileObj) => { body.append("file", fileObj) - return fetchJson(url, {method: 'POST', body}) + return fetchJson(url, {method: "POST", body}) } export const lnurlEncode = (prefix, url) => bech32.encode(prefix, bech32.toWords(utf8.decode(url)), false) -export const lnurlDecode = b32 => - utf8.encode(bech32.fromWords(bech32.decode(b32, false).words)) +export const lnurlDecode = b32 => utf8.encode(bech32.fromWords(bech32.decode(b32, false).words)) 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' + 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 4bd28dfa..50114ddd 100644 --- a/src/util/nostr.ts +++ b/src/util/nostr.ts @@ -1,9 +1,10 @@ -import type {DisplayEvent} from 'src/util/types' -import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from 'ramda' -import {nip19} from 'nostr-tools' -import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak' +import type {DisplayEvent} from "src/util/types" +import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from "ramda" +import {nip19} from "nostr-tools" +import {ensurePlural, ellipsize, first} from "hurdak/lib/hurdak" -export const personKinds = [0, 2, 3, 10000, 10001, 10002, 12165] +export const personKinds = [0, 2, 3, 10001, 10002] +export const userKinds = personKinds.concat([10000]) export class Tags { tags: Array @@ -11,7 +12,7 @@ export class Tags { this.tags = tags } static from(events) { - return new Tags(ensurePlural(events).flatMap(prop('tags'))) + return new Tags(ensurePlural(events).flatMap(prop("tags"))) } static wrap(tags) { return new Tags((tags || []).filter(identity)) @@ -26,7 +27,7 @@ export class Tags { return last(this.tags) } relays() { - return uniq(flatten(this.tags).filter(isShareableRelay)).map(objOf('url')) + return uniq(flatten(this.tags).filter(isShareableRelay)).map(objOf("url")) } pubkeys() { return this.type("p").values().all() @@ -55,12 +56,17 @@ export class Tags { export const findReply = e => Tags.from(e).type("e").mark("reply").first() || Tags.from(e).type("e").last() -export const findReplyId = e => Tags.wrap([findReply(e)]).values().first() +export const findReplyId = e => + Tags.wrap([findReply(e)]) + .values() + .first() -export const findRoot = e => - Tags.from(e).type("e").mark("root").first() +export const findRoot = e => Tags.from(e).type("e").mark("root").first() -export const findRootId = e => Tags.wrap([findRoot(e)]).values().first() +export const findRootId = e => + Tags.wrap([findRoot(e)]) + .values() + .first() export const displayPerson = p => { if (p.kind0?.display_name) { @@ -76,13 +82,13 @@ export const displayPerson = p => { } catch (e) { console.error(e) - return '' + return "" } } -export const displayRelay = ({url}) => last(url.split('://')) +export const displayRelay = ({url}) => last(url.split("://")) -export const isLike = content => ['', '+', '🤙', '👍', '❤️', '😎', '🏅'].includes(content) +export const isLike = content => ["", "+", "🤙", "👍", "❤️", "😎", "🏅"].includes(content) export const isAlert = (e, pubkey) => { if (![1, 7, 9735].includes(e.kind)) { @@ -102,28 +108,26 @@ export const isAlert = (e, pubkey) => { return true } -export const isRelay = url => ( - typeof url === 'string' +export const isRelay = url => + typeof url === "string" && // It should have the protocol included - && url.match(/^wss?:\/\/.+/) -) + url.match(/^wss?:\/\/.+/) -export const isShareableRelay = url => ( - isRelay(url) +export const isShareableRelay = url => + isRelay(url) && // Don't match stuff with a port number - && !url.slice(6).match(/:\d+/) + !url.slice(6).match(/:\d+/) && // Don't match raw ip addresses - && !url.slice(6).match(/\d+\.\d+\.\d+\.\d+/) + !url.slice(6).match(/\d+\.\d+\.\d+\.\d+/) && // Skip nostr.wine's virtual relays - && !url.slice(6).match(/\/npub/) -) + !url.slice(6).match(/\/npub/) -export const normalizeRelayUrl = url => url.replace(/\/+$/, '').toLowerCase().trim() +export const normalizeRelayUrl = url => url.replace(/\/+$/, "").toLowerCase().trim() -export const roomAttrs = ['name', 'about', 'picture'] +export const roomAttrs = ["name", "about", "picture"] export const asDisplayEvent = event => - ({replies: [], reactions: [], zaps: [], ...event}) as DisplayEvent + ({replies: [], reactions: [], zaps: [], ...event} as DisplayEvent) export const toHex = (data: string): string | null => { try { diff --git a/src/views/EnsureData.svelte b/src/views/EnsureData.svelte index 797d85fd..46871ba8 100644 --- a/src/views/EnsureData.svelte +++ b/src/views/EnsureData.svelte @@ -6,7 +6,7 @@ import RelaySearch from "src/views/relays/RelaySearch.svelte" import RelayCard from "src/views/relays/RelayCard.svelte" import PersonSearch from "src/views/person/PersonSearch.svelte" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import user from "src/agent/user" export let enforceRelays = true diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index 7491ab4f..8dbf54d8 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -21,7 +21,7 @@ const submit = async event => { event.preventDefault() - user.settings.set(values) + user.profile.update($p => ({...$p, settings: values})) toast.show("info", "Your settings have been saved!") } diff --git a/src/views/SideNav.svelte b/src/views/SideNav.svelte index 05a781f9..dce80040 100644 --- a/src/views/SideNav.svelte +++ b/src/views/SideNav.svelte @@ -5,7 +5,7 @@ import {menuIsOpen, installPrompt, routes} from "src/app/ui" import {newAlerts, newDirectMessages, newChatMessages} from "src/app/alerts" import {slowConnections} from "src/app/connection" - import PersonCircle from "src/partials/PersonCircle.svelte"; + import PersonCircle from "src/partials/PersonCircle.svelte" const {profile, canPublish} = user @@ -28,7 +28,7 @@ class="fixed top-0 bottom-0 left-0 z-20 mt-16 w-56 overflow-hidden border-r border-medium bg-dark pt-4 pb-20 text-white shadow-xl transition-all lg:mt-0 lg:ml-0" class:-ml-56={!$menuIsOpen}> - {#if $profile} + {#if $profile.pubkey}
  • diff --git a/src/views/alerts/Alert.svelte b/src/views/alerts/Alert.svelte index 26d97d57..78d1b944 100644 --- a/src/views/alerts/Alert.svelte +++ b/src/views/alerts/Alert.svelte @@ -3,7 +3,7 @@ import Badge from "src/partials/Badge.svelte" import Popover from "src/partials/Popover.svelte" import {formatTimestamp} from "src/util/misc" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import {modal} from "src/app/ui" export let note diff --git a/src/views/alerts/Mention.svelte b/src/views/alerts/Mention.svelte index 9a4f71c6..35c0bfe1 100644 --- a/src/views/alerts/Mention.svelte +++ b/src/views/alerts/Mention.svelte @@ -4,7 +4,7 @@ import {displayPerson} from "src/util/nostr" import Popover from "src/partials/Popover.svelte" import PersonSummary from "src/views/person/PersonSummary.svelte" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import {modal} from "src/app/ui" import PersonCircle from "src/partials/PersonCircle.svelte" diff --git a/src/views/chat/ChatEdit.svelte b/src/views/chat/ChatEdit.svelte index dda765cf..2a414fcb 100644 --- a/src/views/chat/ChatEdit.svelte +++ b/src/views/chat/ChatEdit.svelte @@ -8,7 +8,7 @@ import Textarea from "src/partials/Textarea.svelte" import Button from "src/partials/Button.svelte" import {getUserWriteRelays} from "src/agent/relays" - import {rooms} from "src/agent/state" + import {rooms} from "src/agent/tables" import cmd from "src/agent/cmd" import {toast, modal} from "src/app/ui" import {publishWithToast} from "src/app" diff --git a/src/views/chat/ChatListItem.svelte b/src/views/chat/ChatListItem.svelte index 25df1c9f..cb1603f3 100644 --- a/src/views/chat/ChatListItem.svelte +++ b/src/views/chat/ChatListItem.svelte @@ -4,7 +4,7 @@ import {fly} from "svelte/transition" import {ellipsize} from "hurdak/lib/hurdak" import Anchor from "src/partials/Anchor.svelte" - import {rooms} from "src/agent/state" + import {rooms} from "src/agent/tables" export let room diff --git a/src/views/login/ConnectUser.svelte b/src/views/login/ConnectUser.svelte index 3e2bc9a4..82808a6c 100644 --- a/src/views/login/ConnectUser.svelte +++ b/src/views/login/ConnectUser.svelte @@ -4,7 +4,7 @@ import {onDestroy, onMount} from "svelte" import {navigate} from "svelte-routing" import {sleep, shuffle} from "src/util/misc" - import {isRelay} from "src/util/nostr" + import {isRelay, userKinds} from "src/util/nostr" import Content from "src/partials/Content.svelte" import Spinner from "src/partials/Spinner.svelte" import Input from "src/partials/Input.svelte" @@ -12,7 +12,7 @@ import RelayCardSimple from "src/partials/RelayCardSimple.svelte" import Anchor from "src/partials/Anchor.svelte" import Modal from "src/partials/Modal.svelte" - import {watch} from "src/agent/table" + import {watch} from "src/agent/storage" import network from "src/agent/network" import user from "src/agent/user" import {loadAppData} from "src/app" @@ -48,23 +48,25 @@ attemptedRelays.add(relay.url) currentRelays[i] = relay - network.loadPeople([user.getPubkey()], {relays: [relay], force: true}).then(async () => { - // Wait a bit before removing the relay to smooth out the ui - await sleep(1000) + network + .loadPeople([user.getPubkey()], {relays: [relay], force: true, kinds: userKinds}) + .then(async () => { + // Wait a bit before removing the relay to smooth out the ui + await sleep(1000) - currentRelays[i] = null + currentRelays[i] = null - // Make sure we have relays and follows before calling it good. This helps us avoid - // nuking follow lists later on - if (searching && user.getRelays().length > 0 && user.getPetnames().length > 0) { - searching = false - modal = "success" + // Make sure we have relays and follows before calling it good. This helps us avoid + // nuking follow lists later on + if (searching && user.getRelays().length > 0 && user.getPetnames().length > 0) { + searching = false + modal = "success" - await Promise.all([loadAppData(user.getPubkey()), sleep(3000)]) + await Promise.all([loadAppData(user.getPubkey()), sleep(3000)]) - navigate("/notes/follows") - } - }) + navigate("/notes/follows") + } + }) } if (all(isNil, Object.values(currentRelays)) && isNil(customRelayUrl)) { diff --git a/src/views/messages/MessagesListItem.svelte b/src/views/messages/MessagesListItem.svelte index b204c4be..89b3029e 100644 --- a/src/views/messages/MessagesListItem.svelte +++ b/src/views/messages/MessagesListItem.svelte @@ -4,7 +4,7 @@ import {fly} from "svelte/transition" import {ellipsize} from "hurdak/lib/hurdak" import {displayPerson} from "src/util/nostr" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import {lastChecked} from "src/app/alerts" import PersonCircle from "src/partials/PersonCircle.svelte" diff --git a/src/views/notes/Note.svelte b/src/views/notes/Note.svelte index 6dd8841e..177e6472 100644 --- a/src/views/notes/Note.svelte +++ b/src/views/notes/Note.svelte @@ -29,8 +29,8 @@ import keys from "src/agent/keys" import network from "src/agent/network" import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/state" - import {watch} from "src/agent/table" + import {getPersonWithFallback} from "src/agent/tables" + import {watch} from "src/agent/storage" import cmd from "src/agent/cmd" import {routes} from "src/app/ui" import {publishWithToast} from "src/app" diff --git a/src/views/notes/NoteCreate.svelte b/src/views/notes/NoteCreate.svelte index d9d144af..c452f01c 100644 --- a/src/views/notes/NoteCreate.svelte +++ b/src/views/notes/NoteCreate.svelte @@ -16,8 +16,8 @@ import Modal from "src/partials/Modal.svelte" import Heading from "src/partials/Heading.svelte" import {getUserWriteRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/state" - import {watch} from "src/agent/table" + import {getPersonWithFallback} from "src/agent/tables" + import {watch} from "src/agent/storage" import cmd from "src/agent/cmd" import {toast, modal} from "src/app/ui" import {publishWithToast} from "src/app" diff --git a/src/views/onboarding/Onboarding.svelte b/src/views/onboarding/Onboarding.svelte index 3a801d02..a5c00091 100644 --- a/src/views/onboarding/Onboarding.svelte +++ b/src/views/onboarding/Onboarding.svelte @@ -13,7 +13,7 @@ import OnboardingComplete from "src/views/onboarding/OnboardingComplete.svelte" import {getFollows} from "src/agent/social" import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/state" + import {getPersonWithFallback} from "src/agent/tables" import network from "src/agent/network" import user from "src/agent/user" import keys from "src/agent/keys" diff --git a/src/views/onboarding/OnboardingFollows.svelte b/src/views/onboarding/OnboardingFollows.svelte index df9e8d59..9104d1cc 100644 --- a/src/views/onboarding/OnboardingFollows.svelte +++ b/src/views/onboarding/OnboardingFollows.svelte @@ -6,8 +6,8 @@ import Heading from "src/partials/Heading.svelte" import Content from "src/partials/Content.svelte" import PersonInfo from "src/partials/PersonInfo.svelte" - import {getPersonWithFallback} from "src/agent/state" - import {watch} from "src/agent/table" + import {getPersonWithFallback} from "src/agent/tables" + import {watch} from "src/agent/storage" import {modal} from "src/app/ui" export let follows diff --git a/src/views/onboarding/OnboardingRelays.svelte b/src/views/onboarding/OnboardingRelays.svelte index f0d34eb0..d6f779ab 100644 --- a/src/views/onboarding/OnboardingRelays.svelte +++ b/src/views/onboarding/OnboardingRelays.svelte @@ -6,7 +6,7 @@ import Heading from "src/partials/Heading.svelte" import Content from "src/partials/Content.svelte" import RelayCard from "src/partials/RelayCard.svelte" - import {watch} from "src/agent/table" + import {watch} from "src/agent/storage" import {modal} from "src/app/ui" export let relays diff --git a/src/views/person/PersonList.svelte b/src/views/person/PersonList.svelte index fcbfe396..c46f1a58 100644 --- a/src/views/person/PersonList.svelte +++ b/src/views/person/PersonList.svelte @@ -1,8 +1,8 @@