diff --git a/ROADMAP.md b/ROADMAP.md index 0db11dce..aa5a578e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,9 +1,8 @@ # Current - [ ] Refactor + - Split out Note pieces - Move global modals to child components? - - Consolidate person search/list, other person components - - Consolidate relay components - [ ] Relays bounty - [ ] Ability to create custom feeds diff --git a/package-lock.json b/package-lock.json index 06491e1a..3552babc 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 3e266123..c660dd32 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,7 @@ "fuse.js": "^6.6.2", "hurdak": "github:ConsignCloud/hurdak", "husky": "^8.0.3", - "localforage": "^1.10.0", - "localforage-memoryStorageDriver": "^0.9.2", + "lokijs": "^1.5.12", "lru-cache": "^7.18.3", "nostr-tools": "^1.7.4", "npm-run-all": "^4.1.5", diff --git a/src/agent/cmd.ts b/src/agent/cmd.ts index e828c887..ee034d43 100644 --- a/src/agent/cmd.ts +++ b/src/agent/cmd.ts @@ -4,7 +4,7 @@ import {doPipe} from "hurdak/lib/hurdak" import {parseContent} from "src/util/html" import {Tags, roomAttrs, displayPerson, findReplyId, findRootId} from "src/util/nostr" import {getRelayForPersonHint} from "src/agent/relays" -import {getPersonWithFallback} from "src/agent/tables" +import {getPersonWithFallback} from "src/agent/db" import pool from "src/agent/pool" import sync from "src/agent/sync" import keys from "src/agent/keys" diff --git a/src/agent/db.ts b/src/agent/db.ts new file mode 100644 index 00000000..4b77fbe0 --- /dev/null +++ b/src/agent/db.ts @@ -0,0 +1,200 @@ +import type {Writable} from "svelte/store" +import Loki from "lokijs" +import IncrementalIndexedAdapter from "lokijs/src/incremental-indexeddb-adapter" +import {partition, sortBy, prop, pluck, without, is} from "ramda" +import {throttle} from "throttle-debounce" +import {writable} from "svelte/store" +import {ensurePlural, createMap} from "hurdak/lib/hurdak" +import {log} from "src/util/logger" +import {Tags} from "src/util/nostr" +import user from "src/agent/user" + +const loki = new Loki("agent.db", { + autoload: true, + autosave: true, + adapter: window.indexedDB ? new IncrementalIndexedAdapter() : new Loki.LokiMemoryAdapter(), + autoloadCallback: () => ready.set(true), +}) + +// ---------------------------------------------------------------------------- +// Database table abstraction around loki + +const registry = {} as Record + +class Table { + name: string + pk: string + _max: number + _sort: (xs: Array>) => Array> + _coll: Loki + constructor(name, pk, {max = 500, sort = null} = {}) { + this.name = name + this.pk = pk + this._max = max + this._sort = sort + this._coll = loki.addCollection(name, {unique: [pk]}) + + registry[name] = this + } + subscribe(cb) { + const keys = ["insert", "update"] + + this._coll.addListener(keys, cb) + + cb(this) + + return () => this._coll.removeListener(keys, cb) + } + patch(items) { + const [updates, creates] = partition(item => this.get(item[this.pk]), ensurePlural(items)) + + if (creates.length > 0) { + this._coll.insert(creates) + } + + if (updates.length > 0) { + const updatesByPk = createMap(this.pk, updates) + + this._coll.updateWhere( + item => Boolean(updatesByPk[item[this.pk]]), + item => ({...item, ...updatesByPk[item[this.pk]]}) + ) + } + } + remove(ks) { + this._coll.chain().removeWhere({[this.pk]: {$in: ks}}) + } + prune() { + if (this._coll.count() < this._max * 1.1) { + return + } + + let data = this.all() + + if (this._sort) { + data = this._sort(data) + } + + const pks = pluck(this.pk, data.slice(this._max)) + + this._coll.findAndRemove({[this.pk]: {$in: pks}}) + } + drop() { + this._coll.clear({removeIndices: true}) + } + all(spec = null) { + return this._coll.find(spec) + } + find(spec = null) { + return this._coll.findOne(spec) + } + get(k) { + return this._coll.by(this.pk, k) + } +} + +const listener = (() => { + let listeners = [] + + return { + connect: () => { + for (const table of Object.values(registry) as Array) { + table.subscribe(() => listeners.forEach(f => f(table.name))) + } + }, + subscribe: f => { + listeners.push(f) + + return () => { + listeners = without([f], listeners) + } + }, + } +})() + +// Periodically prune data. One table at a time to avoid interfering with the UI +setInterval(() => { + const tables = Object.values(registry) + const table = tables[Math.floor(tables.length * Math.random())] + + table.prune() +}, 10_000) + +type WatchStore = Writable & { + refresh: () => void +} + +export const watch = (names, f) => { + names = ensurePlural(names) + + const store = writable(null) as WatchStore + 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 + store.refresh = throttle(300, async () => store.set(await f(...tables))) + + // Listen for changes + listener.subscribe(name => { + if (names.includes(name)) { + store.refresh() + } + }) + + return store +} + +export const dropAll = async () => { + for (const table of Object.values(registry)) { + await table.drop() + + log(`Successfully dropped table ${table.name}`) + } +} + +// ---------------------------------------------------------------------------- +// Domain-specific collections + +const sortByCreatedAt = sortBy(prop("created_at")) +const sortByLastSeen = sortBy(prop("last_seen")) + +export const people = new Table("people", "pubkey", { + max: 5000, + // Don't delete the user's own profile or those of direct follows + sort: xs => { + const follows = Tags.wrap(user.getPetnames()).values().all() + const whitelist = new Set(follows.concat(user.getPubkey())) + + return sortBy(x => (whitelist.has(x.pubkey) ? 0 : x.created_at), xs) + }, +}) + +export const userEvents = new Table("userEvents", "id", {max: 2000, sort: sortByCreatedAt}) +export const notifications = new Table("notifications", "id") +export const contacts = new Table("contacts", "pubkey") +export const rooms = new Table("rooms", "id") +export const relays = new Table("relays", "url") +export const routes = new Table("routes", "id", {max: 3000, sort: sortByLastSeen}) + +listener.connect() + +export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey} +export const getRelayWithFallback = url => relays.get(url) || {url} + +const ready = writable(false) + +export const onReady = cb => { + const unsub = ready.subscribe($ready => { + if ($ready) { + cb() + setTimeout(() => unsub()) + } + }) +} diff --git a/src/agent/network.ts b/src/agent/network.ts index a27815b5..6600b087 100644 --- a/src/agent/network.ts +++ b/src/agent/network.ts @@ -10,7 +10,7 @@ import { getRelaysForEventChildren, sampleRelays, } from "src/agent/relays" -import {people} from "src/agent/tables" +import {people} from "src/agent/db" import pool from "src/agent/pool" import sync from "src/agent/sync" diff --git a/src/agent/relays.ts b/src/agent/relays.ts index c48f6734..667102e4 100644 --- a/src/agent/relays.ts +++ b/src/agent/relays.ts @@ -5,7 +5,7 @@ import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} fr 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 {relays, routes} from "src/agent/db" import pool from "src/agent/pool" import user from "src/agent/user" @@ -25,7 +25,7 @@ import user from "src/agent/user" export const initializeRelayList = async () => { // Throw some hardcoded defaults in there - await relays.bulkPatch([ + await relays.patch([ {url: "wss://brb.io"}, {url: "wss://nostr.zebedee.cloud"}, {url: "wss://nostr-pub.wellorder.net"}, @@ -40,7 +40,7 @@ export const initializeRelayList = async () => { const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay" const json = await fetchJson(url) - await relays.bulkPatch(json.relays.filter(isRelay).map(objOf("url"))) + await relays.patch(json.relays.filter(isRelay).map(objOf("url"))) } catch (e) { warn("Failed to fetch relays list", e) } @@ -74,7 +74,7 @@ export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, "write") 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 filter = mode ? {pubkey: {$in: pubkeys}, mode} : {pubkey: {$in: pubkeys}} const routesByPubkey = groupBy(prop("pubkey"), routes.all(filter)) return aggregateScores( diff --git a/src/agent/social.ts b/src/agent/social.ts index 12b58a25..58d29336 100644 --- a/src/agent/social.ts +++ b/src/agent/social.ts @@ -1,6 +1,6 @@ import {uniq, without} from "ramda" import {Tags} from "src/util/nostr" -import {getPersonWithFallback} from "src/agent/tables" +import {getPersonWithFallback} from "src/agent/db" import user from "src/agent/user" export const getFollows = pubkey => diff --git a/src/agent/storage.ts b/src/agent/storage.ts deleted file mode 100644 index 13bcbe80..00000000 --- a/src/agent/storage.ts +++ /dev/null @@ -1,264 +0,0 @@ -import type {Writable} from "svelte/store" -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 Cache from "src/util/cache" -import {log, error} from "src/util/logger" -import {where} from "src/util/misc" - -// ---------------------------------------------------------------------------- -// Localforage interface via web worker - -type Message = { - topic: string - payload: object -} - -const worker = new Worker(new URL("./workers/database.js", import.meta.url), {type: "module"}) - -worker.addEventListener("error", error) - -class Channel { - id: string - onMessage: (e: MessageEvent) => void - constructor({onMessage}) { - this.id = Math.random().toString().slice(2) - this.onMessage = e => { - if (e.data.channel === this.id) { - onMessage(e.data as Message) - } - } - - worker.addEventListener("message", this.onMessage) - } - close() { - worker.removeEventListener("message", this.onMessage) - } - send(topic, payload) { - worker.postMessage({channel: this.id, topic, payload}) - } -} - -const call = (topic, payload): Promise => { - return new Promise(resolve => { - const channel = new Channel({ - onMessage: message => { - resolve(message) - channel.close() - }, - }) - - channel.send(topic, payload) - }) -} - -const lf = async (method, ...args) => { - const message = await call("localforage.call", {method, args}) - - 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; lru: number}] - -type TableOpts = { - cache?: Cache - initialize?: (table: Table) => Promise> -} - -export const registry = {} as Record - -export class Table { - name: string - pk: string - opts: TableOpts - cache: Cache - listeners: Array<(Table) => void> - ready: Writable - interval: number - constructor(name, pk, opts: TableOpts = {}) { - this.name = name - this.pk = pk - this.opts = opts - this.cache = this.opts.cache || new Cache() - this.listeners = [] - this.ready = writable(false) - - registry[name] = this - - // Sync from storage initially - ;(async () => { - const t = Date.now() - - 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)) - } - - // Initialize our cache and notify listeners - this.cache.load(data) - this._notify() - - log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size()} records)`) - - this.ready.set(true) - })() - - // Prune the cache periodically, with jitter to avoid doing all caches at once - this.interval = window.setInterval(() => { - this.cache.prune() - this._persist() - }, 30_000 + 10_000 * Math.random()) - } - _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) { - 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 gray-6 key on ${this.name}`) - } - - this.cache.set(k, item) - } - - this._notify() - } - put(item) { - this.bulkPut([item]) - } - bulkPatch(items) { - for (const item of items) { - const k = item[this.pk] - - if (!k) { - throw new Error(`Missing gray-6 key on ${this.name}`) - } - - this.cache.set(k, {...this.cache.get(k), ...item}) - } - - this._notify() - } - patch(item) { - this.bulkPatch([item]) - } - bulkRemove(ks) { - for (const k of ks) { - this.cache.delete(k) - } - - this._notify() - } - remove(k) { - this.bulkRemove([k]) - } - async drop() { - this.cache.clear() - - await removeItem(this.name) - } - toArray() { - return Array.from(this.cache.values()) - } - all(spec = {}) { - return this.toArray().filter(where(spec)) - } - find(spec = {}) { - return this.cache.find(where(spec)) - } - get(k) { - return this.cache.get(k) - } -} - -export const listener = (() => { - let listeners = [] - - return { - connect: () => { - for (const table of Object.values(registry) as Array
) { - table.subscribe(() => listeners.forEach(f => f(table.name))) - } - }, - subscribe: f => { - listeners.push(f) - - return () => { - listeners = without([f], listeners) - } - }, - } -})() - -type WatchStore = Writable & { - refresh: () => void -} - -export const watch = (names, f) => { - names = ensurePlural(names) - - const store = writable(null) as WatchStore - 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 - store.refresh = throttle(300, async () => store.set(await f(...tables))) - - // Listen for changes - listener.subscribe(name => { - if (names.includes(name)) { - store.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 c62bd70e..006b29ee 100644 --- a/src/agent/sync.ts +++ b/src/agent/sync.ts @@ -13,7 +13,7 @@ import { hash, } from "src/util/misc" import {Tags, roomAttrs, isRelay, isShareableRelay, normalizeRelayUrl} from "src/util/nostr" -import {people, userEvents, relays, rooms, routes} from "src/agent/tables" +import {people, userEvents, relays, rooms, routes} from "src/agent/db" import {uniqByUrl} from "src/agent/relays" import user from "src/agent/user" @@ -31,7 +31,7 @@ const processEvents = async events => { for (let i = 0; i < chunks.length; i++) { for (const event of chunks[i]) { if (event.pubkey === userPubkey) { - userEvents.put(event) + userEvents.patch(event) } for (const handler of handlers[event.kind] || []) { @@ -57,15 +57,15 @@ const updatePerson = (pubkey, data) => { } } -const verifyNip05 = (pubkey, as) => - nip05.queryProfile(as).then(result => { +const verifyNip05 = (pubkey, alias) => + nip05.queryProfile(alias).then(result => { if (result?.pubkey === pubkey) { - updatePerson(pubkey, {verified_as: as}) + updatePerson(pubkey, {verified_as: alias}) if (result.relays?.length > 0) { const urls = result.relays.filter(isRelay) - relays.bulkPatch(urls.map(url => ({url: normalizeRelayUrl(url)}))) + relays.patch(urls.map(url => ({url: normalizeRelayUrl(url)}))) urls.forEach(url => { addRoute(pubkey, url, "nip05", "write", now()) @@ -299,7 +299,7 @@ const addRoute = (pubkey, rawUrl, type, mode, created_at) => { url: route.url, }) - routes.put({ + routes.patch({ ...route, count: newCount, score: newTotalScore / newCount, diff --git a/src/agent/tables.ts b/src/agent/tables.ts deleted file mode 100644 index a7179f18..00000000 --- a/src/agent/tables.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {sortBy, pluck, all, identity} from "ramda" -import {derived} from "svelte/store" -import Cache from "src/util/cache" -import {Tags} from "src/util/nostr" -import {Table, listener, registry} from "src/agent/storage" -import user from "src/agent/user" - -const sortByCreatedAt = sortBy(([k, x]) => x.value.created_at) -const sortByLastSeen = sortBy(([k, x]) => x.value.last_seen) - -export const people = new Table("people", "pubkey", { - cache: new Cache({ - max: 5000, - // Don't delete the user's own profile or those of direct follows - sort: xs => { - const follows = Tags.wrap(user.getPetnames()).values().all() - const whitelist = new Set(follows.concat(user.getPubkey())) - - return sortBy(([k, {lru, value}]) => (whitelist.has(value.pubkey) ? 0 : value.created_at), xs) - }, - }), -}) - -export const userEvents = new Table("userEvents", "id", { - cache: new Cache({max: 2000, sort: sortByCreatedAt}), -}) - -export const notifications = new Table("notifications", "id") -export const contacts = new Table("contacts", "pubkey") -export const rooms = new Table("rooms", "id") -export const relays = new Table("relays", "url") -export const routes = new Table("routes", "id", { - cache: new Cache({max: 3000, sort: sortByLastSeen}), -}) - -listener.connect() - -export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey} -export const getRelayWithFallback = url => relays.get(url) || {url} - -const ready = derived(pluck("ready", Object.values(registry)), all(identity)) - -export const onReady = cb => { - const unsub = ready.subscribe($ready => { - if ($ready) { - cb() - setTimeout(() => unsub()) - } - }) -} diff --git a/src/agent/user.ts b/src/agent/user.ts index 48919ba6..0cdd1fea 100644 --- a/src/agent/user.ts +++ b/src/agent/user.ts @@ -42,7 +42,7 @@ const profile = synced("agent/user/profile", { 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 mutes = derived(profile, prop("mutes")) as Readable> const canPublish = derived( [keys.pubkey, relays], diff --git a/src/agent/workers/database.js b/src/agent/workers/database.js deleted file mode 100644 index ead90c0b..00000000 --- a/src/agent/workers/database.js +++ /dev/null @@ -1,28 +0,0 @@ -import lf from "localforage" -import memoryStorageDriver from "localforage-memoryStorageDriver" -import {switcherFn} from "hurdak/lib/hurdak" -import {error} from "src/util/logger" - -// Firefox private mode doesn't have access to any storage options -lf.defineDriver(memoryStorageDriver) -lf.setDriver([lf.INDEXEDDB, lf.WEBSQL, lf.LOCALSTORAGE, "memoryStorageDriver"]) - -addEventListener("message", async ({data: {topic, payload, channel}}) => { - const reply = (topic, payload) => postMessage({channel, topic, payload}) - - switcherFn(topic, { - "localforage.call": async () => { - const {method, args} = payload - - const result = await lf[method](...args) - - reply("localforage.return", result) - }, - default: () => { - throw new Error(`invalid topic: ${topic}`) - }, - }) -}) - -addEventListener("error", error) -addEventListener("unhandledrejection", error) diff --git a/src/app/listener.ts b/src/app/listener.ts index 5c04b156..bea78079 100644 --- a/src/app/listener.ts +++ b/src/app/listener.ts @@ -4,8 +4,8 @@ import {doPipe} from "hurdak/lib/hurdak" import {synced, now, timedelta} from "src/util/misc" import {Tags, isNotification} from "src/util/nostr" import {getUserReadRelays} from "src/agent/relays" -import {notifications, userEvents, contacts, rooms} from "src/agent/tables" -import {watch} from "src/agent/storage" +import {notifications, userEvents, contacts, rooms} from "src/agent/db" +import {watch} from "src/agent/db" import network from "src/agent/network" import user from "src/agent/user" @@ -33,7 +33,7 @@ export const newChatMessages = derived( // Synchronization from events to state const processNotifications = async (pubkey, events) => { - notifications.bulkPut(events.filter(e => isNotification(e, pubkey))) + notifications.patch(events.filter(e => isNotification(e, pubkey))) } const processMessages = async (pubkey, events) => { @@ -91,7 +91,7 @@ const listen = async pubkey => { // Include an offset so we don't miss notifications on one relay but not another const since = now() - timedelta(30, "days") const roomIds = pluck("id", rooms.all({joined: true})) - const eventIds = doPipe(userEvents.all({"created_at:gt": since, kind: 1}), [ + const eventIds = doPipe(userEvents.all({kind: 1, created_at: {$gt: since}}), [ sortBy(e => -e.created_at), slice(0, 256), pluck("id"), diff --git a/src/app2/App.svelte b/src/app2/App.svelte index d28aa335..806ba313 100644 --- a/src/app2/App.svelte +++ b/src/app2/App.svelte @@ -10,13 +10,13 @@ import {warn} from "src/util/logger" import {timedelta, hexToBech32, bech32ToHex, shuffle, now} from "src/util/misc" import cmd from "src/agent/cmd" - import {onReady, relays} from "src/agent/tables" + import {onReady, relays} from "src/agent/db" import keys from "src/agent/keys" import network from "src/agent/network" import pool from "src/agent/pool" import {initializeRelayList} from "src/agent/relays" import sync from "src/agent/sync" - import * as tables from "src/agent/tables" + import * as db from "src/agent/db" import user from "src/agent/user" import {loadAppData} from "src/app" import {theme, getThemeVariables} from "src/app/ui" @@ -28,7 +28,7 @@ import Modal from "src/app2/Modal.svelte" import ForegroundButtons from "src/app2/ForegroundButtons.svelte" - Object.assign(window, {cmd, user, keys, network, pool, sync, tables, bech32ToHex, hexToBech32}) + Object.assign(window, {cmd, user, keys, network, pool, sync, db, bech32ToHex, hexToBech32}) export let pathname = location.pathname @@ -117,11 +117,8 @@ // Find relays with old/missing metadata and refresh them. Only pick a // few so we're not sending too many concurrent http requests - const staleRelays = shuffle( - await relays.all({ - "refreshed_at:lt": now() - timedelta(7, "days"), - }) - ).slice(0, 10) + const query = {refreshed_at: {$lt: now() - timedelta(7, "days")}} + const staleRelays = shuffle(relays.all(query)).slice(0, 10) const freshRelays = await Promise.all( staleRelays.map(async ({url}) => { @@ -145,7 +142,7 @@ }) ) - relays.bulkPatch(freshRelays.filter(identity)) + relays.patch(freshRelays.filter(identity)) }, 30_000) return () => { diff --git a/src/app2/EnsureData.svelte b/src/app2/EnsureData.svelte index 92a07a64..fc28a354 100644 --- a/src/app2/EnsureData.svelte +++ b/src/app2/EnsureData.svelte @@ -6,7 +6,7 @@ import RelaySearch from "src/app2/shared/RelaySearch.svelte" import RelayCard from "src/app2/shared/RelayCard.svelte" import PersonSearch from "src/app2/shared/PersonSearch.svelte" - import {getPersonWithFallback} from "src/agent/tables" + import {getPersonWithFallback} from "src/agent/db" import user from "src/agent/user" export let enforceRelays = true diff --git a/src/app2/Routes.svelte b/src/app2/Routes.svelte index 8dddb671..8a5a6958 100644 --- a/src/app2/Routes.svelte +++ b/src/app2/Routes.svelte @@ -1,6 +1,6 @@
@@ -49,16 +50,26 @@
{/if} -
+
{#if $canPublish} - {#if following} - - - + {#if muted} + {:else} - - - + + {/if} + {#if following} + + {:else} + {/if} {/if}
diff --git a/src/app2/shared/RelayActions.svelte b/src/app2/shared/RelayActions.svelte index a81cee2a..1a086475 100644 --- a/src/app2/shared/RelayActions.svelte +++ b/src/app2/shared/RelayActions.svelte @@ -4,7 +4,7 @@ import Popover from "src/partials/Popover.svelte" import OverflowMenu from "src/partials/OverflowMenu.svelte" import user from "src/agent/user" - import {getRelayWithFallback} from "src/agent/tables" + import {getRelayWithFallback} from "src/agent/db" export let relay diff --git a/src/app2/shared/RelaySearch.svelte b/src/app2/shared/RelaySearch.svelte index cb84ace6..ca495858 100644 --- a/src/app2/shared/RelaySearch.svelte +++ b/src/app2/shared/RelaySearch.svelte @@ -3,7 +3,7 @@ import {fuzzy} from "src/util/misc" import Input from "src/partials/Input.svelte" import RelayCard from "src/app2/shared/RelayCard.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" import user from "src/agent/user" export let q = "" diff --git a/src/app2/views/ChatDetail.svelte b/src/app2/views/ChatDetail.svelte index e8133106..33149407 100644 --- a/src/app2/views/ChatDetail.svelte +++ b/src/app2/views/ChatDetail.svelte @@ -10,7 +10,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/storage" + import {watch} from "src/agent/db" import cmd from "src/agent/cmd" import {modal} from "src/app/ui" import {lastChecked} from "src/app/listener" diff --git a/src/app2/views/ChatEdit.svelte b/src/app2/views/ChatEdit.svelte index 8b0a5bdd..f90b5f78 100644 --- a/src/app2/views/ChatEdit.svelte +++ b/src/app2/views/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/tables" + import {rooms} from "src/agent/db" import cmd from "src/agent/cmd" import {toast, modal} from "src/app/ui" import {publishWithToast} from "src/app" diff --git a/src/app2/views/ChatList.svelte b/src/app2/views/ChatList.svelte index 4e19bbb9..9b9193d5 100644 --- a/src/app2/views/ChatList.svelte +++ b/src/app2/views/ChatList.svelte @@ -5,7 +5,7 @@ import Content from "src/partials/Content.svelte" import Anchor from "src/partials/Anchor.svelte" import ChatListItem from "src/app2/views/ChatListItem.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" import network from "src/agent/network" import {getUserReadRelays} from "src/agent/relays" import {modal} from "src/app/ui" @@ -15,7 +15,7 @@ let results = [] const userRooms = watch("rooms", t => t.all({joined: true})) - const otherRooms = watch("rooms", t => t.all({"joined:!eq": true})) + const otherRooms = watch("rooms", t => t.all({joined: {$ne: true}})) $: search = fuzzy($otherRooms, {keys: ["name", "about"]}) $: results = search(q).slice(0, 50) diff --git a/src/app2/views/ChatListItem.svelte b/src/app2/views/ChatListItem.svelte index 30db1822..f324d84e 100644 --- a/src/app2/views/ChatListItem.svelte +++ b/src/app2/views/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/tables" + import {rooms} from "src/agent/db" export let room diff --git a/src/app2/views/LoginConnect.svelte b/src/app2/views/LoginConnect.svelte index c5b210f5..ef0a5118 100644 --- a/src/app2/views/LoginConnect.svelte +++ b/src/app2/views/LoginConnect.svelte @@ -12,7 +12,7 @@ import Anchor from "src/partials/Anchor.svelte" import Modal from "src/partials/Modal.svelte" import RelayCard from "src/app2/shared/RelayCard.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" import network from "src/agent/network" import user from "src/agent/user" import pool from "src/agent/pool" diff --git a/src/app2/views/Logout.svelte b/src/app2/views/Logout.svelte index ddc59012..5fb0c000 100644 --- a/src/app2/views/Logout.svelte +++ b/src/app2/views/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/storage" + import {dropAll} from "src/agent/db" let confirmed = false diff --git a/src/app2/views/MessagesDetail.svelte b/src/app2/views/MessagesDetail.svelte index 1b3a6fcc..17793e8b 100644 --- a/src/app2/views/MessagesDetail.svelte +++ b/src/app2/views/MessagesDetail.svelte @@ -8,8 +8,8 @@ import Anchor from "src/partials/Anchor.svelte" import NoteContent from "src/app2/shared/NoteContent.svelte" import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/tables" - import {watch} from "src/agent/storage" + import {getPersonWithFallback} from "src/agent/db" + import {watch} from "src/agent/db" import network from "src/agent/network" import keys from "src/agent/keys" import user from "src/agent/user" diff --git a/src/app2/views/MessagesList.svelte b/src/app2/views/MessagesList.svelte index 77a1a7cf..33568e22 100644 --- a/src/app2/views/MessagesList.svelte +++ b/src/app2/views/MessagesList.svelte @@ -4,7 +4,7 @@ import Tabs from "src/partials/Tabs.svelte" import Content from "src/partials/Content.svelte" import MessagesListItem from "src/app2/views/MessagesListItem.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" let activeTab = "messages" let contacts = [] @@ -19,7 +19,7 @@ const accepted = watch("contacts", t => sortBy(e => -e.lastMessage, t.all({accepted: true}))) const requests = watch("contacts", t => - sortBy(e => -e.lastMessage, t.all({"accepted:!eq": true})) + sortBy(e => -e.lastMessage, t.all({accepted: {$ne: true}})) ) const getDisplay = tab => ({ diff --git a/src/app2/views/MessagesListItem.svelte b/src/app2/views/MessagesListItem.svelte index bb635cf3..cc41448a 100644 --- a/src/app2/views/MessagesListItem.svelte +++ b/src/app2/views/MessagesListItem.svelte @@ -3,7 +3,7 @@ import {navigate} from "svelte-routing" import {ellipsize} from "hurdak/lib/hurdak" import {displayPerson} from "src/util/nostr" - import {getPersonWithFallback} from "src/agent/tables" + import {getPersonWithFallback} from "src/agent/db" import {lastChecked} from "src/app/listener" import PersonCircle from "src/app2/shared/PersonCircle.svelte" import Card from "src/partials/Card.svelte" diff --git a/src/app2/views/NoteCreate.svelte b/src/app2/views/NoteCreate.svelte index f6e94951..43ad9e20 100644 --- a/src/app2/views/NoteCreate.svelte +++ b/src/app2/views/NoteCreate.svelte @@ -17,7 +17,7 @@ import RelayCard from "src/app2/shared/RelayCard.svelte" import RelaySearch from "src/app2/shared/RelaySearch.svelte" import {getUserWriteRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/tables" + import {getPersonWithFallback} from "src/agent/db" import cmd from "src/agent/cmd" import user from "src/agent/user" import {toast, modal} from "src/app/ui" diff --git a/src/app2/views/Notification.svelte b/src/app2/views/Notification.svelte index 67ba387a..f83a6c4d 100644 --- a/src/app2/views/Notification.svelte +++ b/src/app2/views/Notification.svelte @@ -8,7 +8,7 @@ import Popover from "src/partials/Popover.svelte" import NoteContent from "src/app2/shared/NoteContent.svelte" import NotificationSection from "src/app2/views/NotificationSection.svelte" - import {getPersonWithFallback, userEvents} from "src/agent/tables" + import {getPersonWithFallback, userEvents} from "src/agent/db" import {modal} from "src/app/ui" export let event diff --git a/src/app2/views/NotificationSection.svelte b/src/app2/views/NotificationSection.svelte index 566f2483..9b894691 100644 --- a/src/app2/views/NotificationSection.svelte +++ b/src/app2/views/NotificationSection.svelte @@ -1,6 +1,6 @@ diff --git a/src/app2/views/Notifications.svelte b/src/app2/views/Notifications.svelte index b183646b..a6d57a09 100644 --- a/src/app2/views/Notifications.svelte +++ b/src/app2/views/Notifications.svelte @@ -7,9 +7,9 @@ import Spinner from "src/partials/Spinner.svelte" import Content from "src/partials/Content.svelte" import Notification from "src/app2/views/Notification.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" import user from "src/agent/user" - import {userEvents} from "src/agent/tables" + import {userEvents} from "src/agent/db" import {lastChecked} from "src/app/listener" let limit = 0 diff --git a/src/app2/views/Onboarding.svelte b/src/app2/views/Onboarding.svelte index 19790e92..a2913b7e 100644 --- a/src/app2/views/Onboarding.svelte +++ b/src/app2/views/Onboarding.svelte @@ -13,7 +13,7 @@ import OnboardingComplete from "src/app2/views/OnboardingComplete.svelte" import {getFollows} from "src/agent/social" import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays" - import {getPersonWithFallback} from "src/agent/tables" + import {getPersonWithFallback} from "src/agent/db" import network from "src/agent/network" import pool from "src/agent/pool" import user from "src/agent/user" diff --git a/src/app2/views/OnboardingFollows.svelte b/src/app2/views/OnboardingFollows.svelte index 39c82427..f789e973 100644 --- a/src/app2/views/OnboardingFollows.svelte +++ b/src/app2/views/OnboardingFollows.svelte @@ -6,8 +6,8 @@ import Heading from "src/partials/Heading.svelte" import Content from "src/partials/Content.svelte" import PersonInfo from "src/app2/shared/PersonInfo.svelte" - import {getPersonWithFallback} from "src/agent/tables" - import {watch} from "src/agent/storage" + import {getPersonWithFallback} from "src/agent/db" + import {watch} from "src/agent/db" import {modal} from "src/app/ui" export let follows @@ -15,7 +15,7 @@ let q = "" let search - const knownPeople = watch("people", t => t.all({"kind0.name:!nil": null})) + const knownPeople = watch("people", t => t.all({"kind0.name": {$type: "string"}})) $: search = fuzzy( $knownPeople.filter(p => !follows.includes(p.pubkey)), diff --git a/src/app2/views/OnboardingRelays.svelte b/src/app2/views/OnboardingRelays.svelte index 258d5340..30c8a3de 100644 --- a/src/app2/views/OnboardingRelays.svelte +++ b/src/app2/views/OnboardingRelays.svelte @@ -6,7 +6,7 @@ import Heading from "src/partials/Heading.svelte" import Content from "src/partials/Content.svelte" import RelayCard from "src/app2/shared/RelayCard.svelte" - import {watch} from "src/agent/storage" + import {watch} from "src/agent/db" import {modal} from "src/app/ui" export let relays diff --git a/src/app2/views/PersonDetail.svelte b/src/app2/views/PersonDetail.svelte index e4e1e694..c73310d0 100644 --- a/src/app2/views/PersonDetail.svelte +++ b/src/app2/views/PersonDetail.svelte @@ -19,7 +19,7 @@ import pool from "src/agent/pool" import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays" import network from "src/agent/network" - import {getPersonWithFallback} from "src/agent/tables" + import {getPersonWithFallback, watch} from "src/agent/db" import {routes, modal, theme, getThemeColor} from "src/app/ui" import PersonCircle from "src/app2/shared/PersonCircle.svelte" import PersonAbout from "src/app2/shared/PersonAbout.svelte" @@ -31,12 +31,12 @@ const interpolate = (a, b) => t => a + Math.round((b - a) * t) const {petnamePubkeys, canPublish, mutes} = user const tabs = ["notes", "likes", pool.forceUrls.length === 0 && "relays"].filter(identity) + const pubkey = toHex(npub) + const person = watch("people", () => getPersonWithFallback(pubkey)) - let pubkey = toHex(npub) let following = false let muted = false let followersCount = tweened(0, {interpolate, duration: 1000}) - let person = getPersonWithFallback(pubkey) let loading = true let actions = [] let rgb, rgba @@ -95,14 +95,13 @@ } onMount(async () => { - log("Person", npub, person) + log("Person", npub, $person) - document.title = displayPerson(person) + document.title = displayPerson($person) // Refresh our person network.loadPeople([pubkey], {relays, force: true}).then(() => { ownRelays = getPubkeyWriteRelays(pubkey) - person = getPersonWithFallback(pubkey) loading = false }) @@ -142,7 +141,7 @@ const follow = async () => { const [{url}] = relays - user.addPetname(pubkey, url, displayPerson(person)) + user.addPetname(pubkey, url, displayPerson($person)) } const unfollow = async () => { @@ -158,11 +157,11 @@ } const openProfileInfo = () => { - modal.set({type: "person/info", person}) + modal.set({type: "person/info", $person}) } const share = () => { - modal.set({type: "person/share", person}) + modal.set({type: "person/share", $person}) } @@ -172,31 +171,31 @@ background-size: cover; background-image: linear-gradient(to bottom, {rgba}, {rgb}), - url('{person.kind0?.banner}')" /> + url('{$person.kind0?.banner}')" />
- +
-

{displayPerson(person)}

+

{displayPerson($person)}

- {#if person.verified_as} + {#if $person.verified_as}
- {last(person.verified_as.split("@"))} + {last($person.verified_as.split("@"))}
{/if}
- - {#if person?.petnames} + + {#if $person?.petnames}