mirror of
https://github.com/coracle-social/coracle.git
synced 2024-10-06 11:43:30 +00:00
Switch to lokijs
This commit is contained in:
parent
08d6195880
commit
f2a76b319f
@ -1,9 +1,8 @@
|
|||||||
# Current
|
# Current
|
||||||
|
|
||||||
- [ ] Refactor
|
- [ ] Refactor
|
||||||
|
- Split out Note pieces
|
||||||
- Move global modals to child components?
|
- Move global modals to child components?
|
||||||
- Consolidate person search/list, other person components
|
|
||||||
- Consolidate relay components
|
|
||||||
|
|
||||||
- [ ] Relays bounty
|
- [ ] Relays bounty
|
||||||
- [ ] Ability to create custom feeds
|
- [ ] Ability to create custom feeds
|
||||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -41,8 +41,7 @@
|
|||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"hurdak": "github:ConsignCloud/hurdak",
|
"hurdak": "github:ConsignCloud/hurdak",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"localforage": "^1.10.0",
|
"lokijs": "^1.5.12",
|
||||||
"localforage-memoryStorageDriver": "^0.9.2",
|
|
||||||
"lru-cache": "^7.18.3",
|
"lru-cache": "^7.18.3",
|
||||||
"nostr-tools": "^1.7.4",
|
"nostr-tools": "^1.7.4",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
@ -4,7 +4,7 @@ import {doPipe} from "hurdak/lib/hurdak"
|
|||||||
import {parseContent} from "src/util/html"
|
import {parseContent} from "src/util/html"
|
||||||
import {Tags, roomAttrs, displayPerson, findReplyId, findRootId} from "src/util/nostr"
|
import {Tags, roomAttrs, displayPerson, findReplyId, findRootId} from "src/util/nostr"
|
||||||
import {getRelayForPersonHint} from "src/agent/relays"
|
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 pool from "src/agent/pool"
|
||||||
import sync from "src/agent/sync"
|
import sync from "src/agent/sync"
|
||||||
import keys from "src/agent/keys"
|
import keys from "src/agent/keys"
|
||||||
|
200
src/agent/db.ts
Normal file
200
src/agent/db.ts
Normal file
@ -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<string, Table>
|
||||||
|
|
||||||
|
class Table {
|
||||||
|
name: string
|
||||||
|
pk: string
|
||||||
|
_max: number
|
||||||
|
_sort: (xs: Array<Record<string, any>>) => Array<Record<string, any>>
|
||||||
|
_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>) {
|
||||||
|
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<T> = Writable<T> & {
|
||||||
|
refresh: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const watch = (names, f) => {
|
||||||
|
names = ensurePlural(names)
|
||||||
|
|
||||||
|
const store = writable(null) as WatchStore<any>
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -10,7 +10,7 @@ import {
|
|||||||
getRelaysForEventChildren,
|
getRelaysForEventChildren,
|
||||||
sampleRelays,
|
sampleRelays,
|
||||||
} from "src/agent/relays"
|
} from "src/agent/relays"
|
||||||
import {people} from "src/agent/tables"
|
import {people} from "src/agent/db"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import sync from "src/agent/sync"
|
import sync from "src/agent/sync"
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} fr
|
|||||||
import {first} from "hurdak/lib/hurdak"
|
import {first} from "hurdak/lib/hurdak"
|
||||||
import {Tags, isRelay, findReplyId} from "src/util/nostr"
|
import {Tags, isRelay, findReplyId} from "src/util/nostr"
|
||||||
import {shuffle, fetchJson} from "src/util/misc"
|
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 pool from "src/agent/pool"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ import user from "src/agent/user"
|
|||||||
|
|
||||||
export const initializeRelayList = async () => {
|
export const initializeRelayList = async () => {
|
||||||
// Throw some hardcoded defaults in there
|
// Throw some hardcoded defaults in there
|
||||||
await relays.bulkPatch([
|
await relays.patch([
|
||||||
{url: "wss://brb.io"},
|
{url: "wss://brb.io"},
|
||||||
{url: "wss://nostr.zebedee.cloud"},
|
{url: "wss://nostr.zebedee.cloud"},
|
||||||
{url: "wss://nostr-pub.wellorder.net"},
|
{url: "wss://nostr-pub.wellorder.net"},
|
||||||
@ -40,7 +40,7 @@ export const initializeRelayList = async () => {
|
|||||||
const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay"
|
const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay"
|
||||||
const json = await fetchJson(url)
|
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) {
|
} catch (e) {
|
||||||
warn("Failed to fetch relays list", 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) => {
|
export const getAllPubkeyRelays = (pubkeys, mode = null) => {
|
||||||
// As an optimization, filter the database once and group by pubkey
|
// 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))
|
const routesByPubkey = groupBy(prop("pubkey"), routes.all(filter))
|
||||||
|
|
||||||
return aggregateScores(
|
return aggregateScores(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {uniq, without} from "ramda"
|
import {uniq, without} from "ramda"
|
||||||
import {Tags} from "src/util/nostr"
|
import {Tags} from "src/util/nostr"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
export const getFollows = pubkey =>
|
export const getFollows = pubkey =>
|
||||||
|
@ -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<Message> => {
|
|
||||||
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<Array<CacheEntry>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const registry = {} as Record<string, Table>
|
|
||||||
|
|
||||||
export class Table {
|
|
||||||
name: string
|
|
||||||
pk: string
|
|
||||||
opts: TableOpts
|
|
||||||
cache: Cache
|
|
||||||
listeners: Array<(Table) => void>
|
|
||||||
ready: Writable<boolean>
|
|
||||||
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>) {
|
|
||||||
table.subscribe(() => listeners.forEach(f => f(table.name)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subscribe: f => {
|
|
||||||
listeners.push(f)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
listeners = without([f], listeners)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
type WatchStore<T> = Writable<T> & {
|
|
||||||
refresh: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const watch = (names, f) => {
|
|
||||||
names = ensurePlural(names)
|
|
||||||
|
|
||||||
const store = writable(null) as WatchStore<any>
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ import {
|
|||||||
hash,
|
hash,
|
||||||
} from "src/util/misc"
|
} from "src/util/misc"
|
||||||
import {Tags, roomAttrs, isRelay, isShareableRelay, normalizeRelayUrl} from "src/util/nostr"
|
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 {uniqByUrl} from "src/agent/relays"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ const processEvents = async events => {
|
|||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
for (const event of chunks[i]) {
|
for (const event of chunks[i]) {
|
||||||
if (event.pubkey === userPubkey) {
|
if (event.pubkey === userPubkey) {
|
||||||
userEvents.put(event)
|
userEvents.patch(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const handler of handlers[event.kind] || []) {
|
for (const handler of handlers[event.kind] || []) {
|
||||||
@ -57,15 +57,15 @@ const updatePerson = (pubkey, data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyNip05 = (pubkey, as) =>
|
const verifyNip05 = (pubkey, alias) =>
|
||||||
nip05.queryProfile(as).then(result => {
|
nip05.queryProfile(alias).then(result => {
|
||||||
if (result?.pubkey === pubkey) {
|
if (result?.pubkey === pubkey) {
|
||||||
updatePerson(pubkey, {verified_as: as})
|
updatePerson(pubkey, {verified_as: alias})
|
||||||
|
|
||||||
if (result.relays?.length > 0) {
|
if (result.relays?.length > 0) {
|
||||||
const urls = result.relays.filter(isRelay)
|
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 => {
|
urls.forEach(url => {
|
||||||
addRoute(pubkey, url, "nip05", "write", now())
|
addRoute(pubkey, url, "nip05", "write", now())
|
||||||
@ -299,7 +299,7 @@ const addRoute = (pubkey, rawUrl, type, mode, created_at) => {
|
|||||||
url: route.url,
|
url: route.url,
|
||||||
})
|
})
|
||||||
|
|
||||||
routes.put({
|
routes.patch({
|
||||||
...route,
|
...route,
|
||||||
count: newCount,
|
count: newCount,
|
||||||
score: newTotalScore / newCount,
|
score: newTotalScore / newCount,
|
||||||
|
@ -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())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -42,7 +42,7 @@ const profile = synced("agent/user/profile", {
|
|||||||
const settings = derived(profile, prop("settings"))
|
const settings = derived(profile, prop("settings"))
|
||||||
const petnames = derived(profile, prop("petnames"))
|
const petnames = derived(profile, prop("petnames"))
|
||||||
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
|
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
|
||||||
const mutes = derived(profile, prop("mutes"))
|
const mutes = derived(profile, prop("mutes")) as Readable<Array<[string, string]>>
|
||||||
|
|
||||||
const canPublish = derived(
|
const canPublish = derived(
|
||||||
[keys.pubkey, relays],
|
[keys.pubkey, relays],
|
||||||
|
@ -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)
|
|
@ -4,8 +4,8 @@ import {doPipe} from "hurdak/lib/hurdak"
|
|||||||
import {synced, now, timedelta} from "src/util/misc"
|
import {synced, now, timedelta} from "src/util/misc"
|
||||||
import {Tags, isNotification} from "src/util/nostr"
|
import {Tags, isNotification} from "src/util/nostr"
|
||||||
import {getUserReadRelays} from "src/agent/relays"
|
import {getUserReadRelays} from "src/agent/relays"
|
||||||
import {notifications, userEvents, contacts, rooms} from "src/agent/tables"
|
import {notifications, userEvents, contacts, rooms} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export const newChatMessages = derived(
|
|||||||
// Synchronization from events to state
|
// Synchronization from events to state
|
||||||
|
|
||||||
const processNotifications = async (pubkey, events) => {
|
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) => {
|
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
|
// Include an offset so we don't miss notifications on one relay but not another
|
||||||
const since = now() - timedelta(30, "days")
|
const since = now() - timedelta(30, "days")
|
||||||
const roomIds = pluck("id", rooms.all({joined: true}))
|
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),
|
sortBy(e => -e.created_at),
|
||||||
slice(0, 256),
|
slice(0, 256),
|
||||||
pluck("id"),
|
pluck("id"),
|
||||||
|
@ -10,13 +10,13 @@
|
|||||||
import {warn} from "src/util/logger"
|
import {warn} from "src/util/logger"
|
||||||
import {timedelta, hexToBech32, bech32ToHex, shuffle, now} from "src/util/misc"
|
import {timedelta, hexToBech32, bech32ToHex, shuffle, now} from "src/util/misc"
|
||||||
import cmd from "src/agent/cmd"
|
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 keys from "src/agent/keys"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import {initializeRelayList} from "src/agent/relays"
|
import {initializeRelayList} from "src/agent/relays"
|
||||||
import sync from "src/agent/sync"
|
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 user from "src/agent/user"
|
||||||
import {loadAppData} from "src/app"
|
import {loadAppData} from "src/app"
|
||||||
import {theme, getThemeVariables} from "src/app/ui"
|
import {theme, getThemeVariables} from "src/app/ui"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
import Modal from "src/app2/Modal.svelte"
|
import Modal from "src/app2/Modal.svelte"
|
||||||
import ForegroundButtons from "src/app2/ForegroundButtons.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
|
export let pathname = location.pathname
|
||||||
|
|
||||||
@ -117,11 +117,8 @@
|
|||||||
|
|
||||||
// Find relays with old/missing metadata and refresh them. Only pick a
|
// Find relays with old/missing metadata and refresh them. Only pick a
|
||||||
// few so we're not sending too many concurrent http requests
|
// few so we're not sending too many concurrent http requests
|
||||||
const staleRelays = shuffle(
|
const query = {refreshed_at: {$lt: now() - timedelta(7, "days")}}
|
||||||
await relays.all({
|
const staleRelays = shuffle(relays.all(query)).slice(0, 10)
|
||||||
"refreshed_at:lt": now() - timedelta(7, "days"),
|
|
||||||
})
|
|
||||||
).slice(0, 10)
|
|
||||||
|
|
||||||
const freshRelays = await Promise.all(
|
const freshRelays = await Promise.all(
|
||||||
staleRelays.map(async ({url}) => {
|
staleRelays.map(async ({url}) => {
|
||||||
@ -145,7 +142,7 @@
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
relays.bulkPatch(freshRelays.filter(identity))
|
relays.patch(freshRelays.filter(identity))
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import RelaySearch from "src/app2/shared/RelaySearch.svelte"
|
import RelaySearch from "src/app2/shared/RelaySearch.svelte"
|
||||||
import RelayCard from "src/app2/shared/RelayCard.svelte"
|
import RelayCard from "src/app2/shared/RelayCard.svelte"
|
||||||
import PersonSearch from "src/app2/shared/PersonSearch.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"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
export let enforceRelays = true
|
export let enforceRelays = true
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Route} from "svelte-routing"
|
import {Route} from "svelte-routing"
|
||||||
import {onReady} from "src/agent/tables"
|
import {onReady} from "src/agent/db"
|
||||||
import EnsureData from "src/app2/EnsureData.svelte"
|
import EnsureData from "src/app2/EnsureData.svelte"
|
||||||
import Notifications from "src/app2/views/Notifications.svelte"
|
import Notifications from "src/app2/views/Notifications.svelte"
|
||||||
import Bech32Entity from "src/app2/views/Bech32Entity.svelte"
|
import Bech32Entity from "src/app2/views/Bech32Entity.svelte"
|
||||||
|
@ -54,8 +54,8 @@
|
|||||||
import keys from "src/agent/keys"
|
import keys from "src/agent/keys"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
|
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import cmd from "src/agent/cmd"
|
import cmd from "src/agent/cmd"
|
||||||
import {routes} from "src/app/ui"
|
import {routes} from "src/app/ui"
|
||||||
import {publishWithToast} from "src/app"
|
import {publishWithToast} from "src/app"
|
||||||
@ -666,7 +666,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="staatliches text-2xl">Relays</h1>
|
<h1 class="staatliches text-2xl">Relays</h1>
|
||||||
<p>This note was found on the {quantify(note.seen_on.length, "relay")} below.</p>
|
<p>This note was found on {quantify(note.seen_on.length, "relay")} below.</p>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each note.seen_on as url}
|
{#each note.seen_on as url}
|
||||||
<RelayCard theme="black" relay={{url}} />
|
<RelayCard theme="black" relay={{url}} />
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
import {sampleRelays} from "src/agent/relays"
|
import {sampleRelays} from "src/agent/relays"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {routes, modal} from "src/app/ui"
|
import {routes, modal} from "src/app/ui"
|
||||||
|
|
||||||
export let note
|
export let note
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import {parseContent} from "src/util/html"
|
import {parseContent} from "src/util/html"
|
||||||
import {displayPerson} from "src/util/nostr"
|
import {displayPerson} from "src/util/nostr"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
export let person
|
export let person
|
||||||
export let truncate = false
|
export let truncate = false
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
||||||
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
|
|
||||||
export let type
|
export let type
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
||||||
import {getUserReadRelays} from "src/agent/relays"
|
import {getUserReadRelays} from "src/agent/relays"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
const {petnamePubkeys} = user
|
const {petnamePubkeys} = user
|
||||||
const search = watch("people", t =>
|
const search = watch("people", t =>
|
||||||
fuzzy(t.all({"kind0.name:!nil": null}), {
|
fuzzy(t.all({"kind0.name": {$type: "string"}}), {
|
||||||
keys: ["kind0.name", "kind0.about", "pubkey"],
|
keys: ["kind0.name", "kind0.about", "pubkey"],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -1,35 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {last} from "ramda"
|
import {last, nth} from "ramda"
|
||||||
import {navigate} from "svelte-routing"
|
import {navigate} from "svelte-routing"
|
||||||
import {displayPerson} from "src/util/nostr"
|
import {displayPerson} from "src/util/nostr"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import {routes} from "src/app/ui"
|
import {routes} from "src/app/ui"
|
||||||
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
||||||
import PersonAbout from "src/app2/shared/PersonAbout.svelte"
|
import PersonAbout from "src/app2/shared/PersonAbout.svelte"
|
||||||
|
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
|
||||||
const {petnamePubkeys, canPublish} = user
|
const {petnamePubkeys, mutes, canPublish} = user
|
||||||
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
|
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
|
||||||
const person = watch("people", () => getPersonWithFallback(pubkey))
|
const person = watch("people", () => getPersonWithFallback(pubkey))
|
||||||
|
|
||||||
let following = false
|
|
||||||
|
|
||||||
$: following = $petnamePubkeys.includes(pubkey)
|
$: following = $petnamePubkeys.includes(pubkey)
|
||||||
|
$: muted = $mutes.map(nth(1)).includes(pubkey)
|
||||||
|
|
||||||
const follow = async () => {
|
const follow = () => {
|
||||||
const [{url}] = getRelays()
|
const [{url}] = getRelays()
|
||||||
|
|
||||||
user.addPetname(pubkey, url, displayPerson($person))
|
user.addPetname(pubkey, url, displayPerson($person))
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollow = async () => {
|
const unfollow = () => user.removePetname(pubkey)
|
||||||
user.removePetname(pubkey)
|
|
||||||
}
|
const unmute = () => user.removeMute(pubkey)
|
||||||
|
|
||||||
|
const mute = () => user.addMute("p", pubkey)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex flex-col gap-4 py-2 px-3">
|
<div class="relative flex flex-col gap-4 py-2 px-3">
|
||||||
@ -49,16 +50,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-4 py-2 text-lg">
|
||||||
{#if $canPublish}
|
{#if $canPublish}
|
||||||
{#if following}
|
{#if muted}
|
||||||
<Anchor type="button-circle" on:click={unfollow}>
|
<i
|
||||||
<i class="fa fa-user-minus" />
|
title="Unmute"
|
||||||
</Anchor>
|
class="fa fa-microphone-slash w-6 cursor-pointer text-center"
|
||||||
|
on:click={unmute} />
|
||||||
{:else}
|
{:else}
|
||||||
<Anchor type="button-circle" on:click={follow}>
|
<i title="Mute" class="fa fa-microphone w-6 cursor-pointer text-center" on:click={mute} />
|
||||||
<i class="fa fa-user-plus" />
|
{/if}
|
||||||
</Anchor>
|
{#if following}
|
||||||
|
<i
|
||||||
|
title="Unfollow"
|
||||||
|
class="fa fa-user-minus w-6 cursor-pointer text-center"
|
||||||
|
on:click={unfollow} />
|
||||||
|
{:else}
|
||||||
|
<i
|
||||||
|
title="Follow"
|
||||||
|
class="fa fa-user-plus w-6 cursor-pointer text-center"
|
||||||
|
on:click={follow} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import Popover from "src/partials/Popover.svelte"
|
import Popover from "src/partials/Popover.svelte"
|
||||||
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
import OverflowMenu from "src/partials/OverflowMenu.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {getRelayWithFallback} from "src/agent/tables"
|
import {getRelayWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
export let relay
|
export let relay
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import {fuzzy} from "src/util/misc"
|
import {fuzzy} from "src/util/misc"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
import RelayCard from "src/app2/shared/RelayCard.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"
|
import user from "src/agent/user"
|
||||||
|
|
||||||
export let q = ""
|
export let q = ""
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
|
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
|
||||||
import network from "src/agent/network"
|
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 cmd from "src/agent/cmd"
|
||||||
import {modal} from "src/app/ui"
|
import {modal} from "src/app/ui"
|
||||||
import {lastChecked} from "src/app/listener"
|
import {lastChecked} from "src/app/listener"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Textarea from "src/partials/Textarea.svelte"
|
import Textarea from "src/partials/Textarea.svelte"
|
||||||
import Button from "src/partials/Button.svelte"
|
import Button from "src/partials/Button.svelte"
|
||||||
import {getUserWriteRelays} from "src/agent/relays"
|
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 cmd from "src/agent/cmd"
|
||||||
import {toast, modal} from "src/app/ui"
|
import {toast, modal} from "src/app/ui"
|
||||||
import {publishWithToast} from "src/app"
|
import {publishWithToast} from "src/app"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import ChatListItem from "src/app2/views/ChatListItem.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 network from "src/agent/network"
|
||||||
import {getUserReadRelays} from "src/agent/relays"
|
import {getUserReadRelays} from "src/agent/relays"
|
||||||
import {modal} from "src/app/ui"
|
import {modal} from "src/app/ui"
|
||||||
@ -15,7 +15,7 @@
|
|||||||
let results = []
|
let results = []
|
||||||
|
|
||||||
const userRooms = watch("rooms", t => t.all({joined: true}))
|
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"]})
|
$: search = fuzzy($otherRooms, {keys: ["name", "about"]})
|
||||||
$: results = search(q).slice(0, 50)
|
$: results = search(q).slice(0, 50)
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {ellipsize} from "hurdak/lib/hurdak"
|
import {ellipsize} from "hurdak/lib/hurdak"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import {rooms} from "src/agent/tables"
|
import {rooms} from "src/agent/db"
|
||||||
|
|
||||||
export let room
|
export let room
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
import RelayCard from "src/app2/shared/RelayCard.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 network from "src/agent/network"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import {dropAll} from "src/agent/storage"
|
import {dropAll} from "src/agent/db"
|
||||||
|
|
||||||
let confirmed = false
|
let confirmed = false
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import NoteContent from "src/app2/shared/NoteContent.svelte"
|
import NoteContent from "src/app2/shared/NoteContent.svelte"
|
||||||
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
|
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
import keys from "src/agent/keys"
|
import keys from "src/agent/keys"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import Tabs from "src/partials/Tabs.svelte"
|
import Tabs from "src/partials/Tabs.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import MessagesListItem from "src/app2/views/MessagesListItem.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 activeTab = "messages"
|
||||||
let contacts = []
|
let contacts = []
|
||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
const accepted = watch("contacts", t => sortBy(e => -e.lastMessage, t.all({accepted: true})))
|
const accepted = watch("contacts", t => sortBy(e => -e.lastMessage, t.all({accepted: true})))
|
||||||
const requests = watch("contacts", t =>
|
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 => ({
|
const getDisplay = tab => ({
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import {navigate} from "svelte-routing"
|
import {navigate} from "svelte-routing"
|
||||||
import {ellipsize} from "hurdak/lib/hurdak"
|
import {ellipsize} from "hurdak/lib/hurdak"
|
||||||
import {displayPerson} from "src/util/nostr"
|
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 {lastChecked} from "src/app/listener"
|
||||||
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
||||||
import Card from "src/partials/Card.svelte"
|
import Card from "src/partials/Card.svelte"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import RelayCard from "src/app2/shared/RelayCard.svelte"
|
import RelayCard from "src/app2/shared/RelayCard.svelte"
|
||||||
import RelaySearch from "src/app2/shared/RelaySearch.svelte"
|
import RelaySearch from "src/app2/shared/RelaySearch.svelte"
|
||||||
import {getUserWriteRelays} from "src/agent/relays"
|
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 cmd from "src/agent/cmd"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {toast, modal} from "src/app/ui"
|
import {toast, modal} from "src/app/ui"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Popover from "src/partials/Popover.svelte"
|
import Popover from "src/partials/Popover.svelte"
|
||||||
import NoteContent from "src/app2/shared/NoteContent.svelte"
|
import NoteContent from "src/app2/shared/NoteContent.svelte"
|
||||||
import NotificationSection from "src/app2/views/NotificationSection.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"
|
import {modal} from "src/app/ui"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Badge from "src/partials/Badge.svelte"
|
import Badge from "src/partials/Badge.svelte"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
|
|
||||||
export let pubkeys
|
export let pubkeys
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Notification from "src/app2/views/Notification.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 user from "src/agent/user"
|
||||||
import {userEvents} from "src/agent/tables"
|
import {userEvents} from "src/agent/db"
|
||||||
import {lastChecked} from "src/app/listener"
|
import {lastChecked} from "src/app/listener"
|
||||||
|
|
||||||
let limit = 0
|
let limit = 0
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
import OnboardingComplete from "src/app2/views/OnboardingComplete.svelte"
|
import OnboardingComplete from "src/app2/views/OnboardingComplete.svelte"
|
||||||
import {getFollows} from "src/agent/social"
|
import {getFollows} from "src/agent/social"
|
||||||
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
|
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 network from "src/agent/network"
|
||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import {modal} from "src/app/ui"
|
import {modal} from "src/app/ui"
|
||||||
|
|
||||||
export let follows
|
export let follows
|
||||||
@ -15,7 +15,7 @@
|
|||||||
let q = ""
|
let q = ""
|
||||||
let search
|
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(
|
$: search = fuzzy(
|
||||||
$knownPeople.filter(p => !follows.includes(p.pubkey)),
|
$knownPeople.filter(p => !follows.includes(p.pubkey)),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import Heading from "src/partials/Heading.svelte"
|
import Heading from "src/partials/Heading.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import RelayCard from "src/app2/shared/RelayCard.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"
|
import {modal} from "src/app/ui"
|
||||||
|
|
||||||
export let relays
|
export let relays
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import pool from "src/agent/pool"
|
import pool from "src/agent/pool"
|
||||||
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
||||||
import network from "src/agent/network"
|
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 {routes, modal, theme, getThemeColor} from "src/app/ui"
|
||||||
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
||||||
import PersonAbout from "src/app2/shared/PersonAbout.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 interpolate = (a, b) => t => a + Math.round((b - a) * t)
|
||||||
const {petnamePubkeys, canPublish, mutes} = user
|
const {petnamePubkeys, canPublish, mutes} = user
|
||||||
const tabs = ["notes", "likes", pool.forceUrls.length === 0 && "relays"].filter(identity)
|
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 following = false
|
||||||
let muted = false
|
let muted = false
|
||||||
let followersCount = tweened(0, {interpolate, duration: 1000})
|
let followersCount = tweened(0, {interpolate, duration: 1000})
|
||||||
let person = getPersonWithFallback(pubkey)
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let actions = []
|
let actions = []
|
||||||
let rgb, rgba
|
let rgb, rgba
|
||||||
@ -95,14 +95,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
log("Person", npub, person)
|
log("Person", npub, $person)
|
||||||
|
|
||||||
document.title = displayPerson(person)
|
document.title = displayPerson($person)
|
||||||
|
|
||||||
// Refresh our person
|
// Refresh our person
|
||||||
network.loadPeople([pubkey], {relays, force: true}).then(() => {
|
network.loadPeople([pubkey], {relays, force: true}).then(() => {
|
||||||
ownRelays = getPubkeyWriteRelays(pubkey)
|
ownRelays = getPubkeyWriteRelays(pubkey)
|
||||||
person = getPersonWithFallback(pubkey)
|
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -142,7 +141,7 @@
|
|||||||
const follow = async () => {
|
const follow = async () => {
|
||||||
const [{url}] = relays
|
const [{url}] = relays
|
||||||
|
|
||||||
user.addPetname(pubkey, url, displayPerson(person))
|
user.addPetname(pubkey, url, displayPerson($person))
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollow = async () => {
|
const unfollow = async () => {
|
||||||
@ -158,11 +157,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openProfileInfo = () => {
|
const openProfileInfo = () => {
|
||||||
modal.set({type: "person/info", person})
|
modal.set({type: "person/info", $person})
|
||||||
}
|
}
|
||||||
|
|
||||||
const share = () => {
|
const share = () => {
|
||||||
modal.set({type: "person/share", person})
|
modal.set({type: "person/share", $person})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -172,31 +171,31 @@
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to bottom, {rgba}, {rgb}),
|
linear-gradient(to bottom, {rgba}, {rgb}),
|
||||||
url('{person.kind0?.banner}')" />
|
url('{$person.kind0?.banner}')" />
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
<div class="flex gap-4 text-gray-1">
|
<div class="flex gap-4 text-gray-1">
|
||||||
<PersonCircle {person} size={16} class="sm:h-32 sm:w-32" />
|
<PersonCircle person={$person} size={16} class="sm:h-32 sm:w-32" />
|
||||||
<div class="flex flex-grow flex-col gap-4">
|
<div class="flex flex-grow flex-col gap-4">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex flex-grow flex-col gap-2">
|
<div class="flex flex-grow flex-col gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-2xl">{displayPerson(person)}</h1>
|
<h1 class="text-2xl">{displayPerson($person)}</h1>
|
||||||
</div>
|
</div>
|
||||||
{#if person.verified_as}
|
{#if $person.verified_as}
|
||||||
<div class="flex gap-1 text-sm">
|
<div class="flex gap-1 text-sm">
|
||||||
<i class="fa fa-user-check text-accent" />
|
<i class="fa fa-user-check text-accent" />
|
||||||
<span class="text-gray-1">{last(person.verified_as.split("@"))}</span>
|
<span class="text-gray-1">{last($person.verified_as.split("@"))}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<OverflowMenu {actions} />
|
<OverflowMenu {actions} />
|
||||||
</div>
|
</div>
|
||||||
<PersonAbout {person} />
|
<PersonAbout person={$person} />
|
||||||
{#if person?.petnames}
|
{#if $person?.petnames}
|
||||||
<div class="flex gap-8" in:fly={{y: 20}}>
|
<div class="flex gap-8" in:fly={{y: 20}}>
|
||||||
<button on:click={showFollows}>
|
<button on:click={showFollows}>
|
||||||
<strong>{person.petnames.length}</strong> following
|
<strong>{$person.petnames.length}</strong> following
|
||||||
</button>
|
</button>
|
||||||
<button on:click={showFollowers}>
|
<button on:click={showFollowers}>
|
||||||
<strong>{numberFmt.format($followersCount)}</strong> followers
|
<strong>{numberFmt.format($followersCount)}</strong> followers
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import Feed from "src/app2/shared/Feed.svelte"
|
import Feed from "src/app2/shared/Feed.svelte"
|
||||||
import RelayTitle from "src/app2/shared/RelayTitle.svelte"
|
import RelayTitle from "src/app2/shared/RelayTitle.svelte"
|
||||||
import RelayActions from "src/app2/shared/RelayActions.svelte"
|
import RelayActions from "src/app2/shared/RelayActions.svelte"
|
||||||
import {relays} from "src/agent/tables"
|
import {relays} from "src/agent/db"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import {sleep, createScroller, Cursor} from "src/util/misc"
|
import {sleep, createScroller, Cursor} from "src/util/misc"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {getPersonWithFallback} from "src/agent/tables"
|
import {getPersonWithFallback} from "src/agent/db"
|
||||||
import network from "src/agent/network"
|
import network from "src/agent/network"
|
||||||
|
|
||||||
export let loadMessages
|
export let loadMessages
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import Badge from "src/partials/Badge.svelte"
|
import Badge from "src/partials/Badge.svelte"
|
||||||
import ContentEditable from "src/partials/ContentEditable.svelte"
|
import ContentEditable from "src/partials/ContentEditable.svelte"
|
||||||
import Suggestions from "src/partials/Suggestions.svelte"
|
import Suggestions from "src/partials/Suggestions.svelte"
|
||||||
import {watch} from "src/agent/storage"
|
import {watch} from "src/agent/db"
|
||||||
import {getPubkeyWriteRelays} from "src/agent/relays"
|
import {getPubkeyWriteRelays} from "src/agent/relays"
|
||||||
|
|
||||||
export let onSubmit
|
export let onSubmit
|
||||||
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchPeople = watch("people", t => {
|
const searchPeople = watch("people", t => {
|
||||||
return fuzzy(t.all({"kind0.name:!nil": null}), {keys: ["kind0.name", "pubkey"]})
|
return fuzzy(t.all({"kind0.name": {$type: "string"}}), {keys: ["kind0.name", "pubkey"]})
|
||||||
})
|
})
|
||||||
|
|
||||||
const applySearch = word => {
|
const applySearch = word => {
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
import {sortBy, nth} from "ramda"
|
|
||||||
|
|
||||||
type CacheEntry = [string, {value: any; lru: number}]
|
|
||||||
|
|
||||||
type SortFn = (xs: Array<CacheEntry>) => Array<CacheEntry>
|
|
||||||
|
|
||||||
type CacheOptions = {
|
|
||||||
max?: number
|
|
||||||
sort?: SortFn
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortByLru = sortBy(([k, x]) => x.lru)
|
|
||||||
|
|
||||||
export default class Cache {
|
|
||||||
max: number
|
|
||||||
sort: SortFn
|
|
||||||
data: Map<string, any>
|
|
||||||
lru: Map<string, number>
|
|
||||||
constructor({max = 1000, sort = sortByLru}: CacheOptions = {}) {
|
|
||||||
this.max = max
|
|
||||||
this.sort = sort
|
|
||||||
this.data = new Map()
|
|
||||||
this.lru = new Map()
|
|
||||||
}
|
|
||||||
set(k, v) {
|
|
||||||
this.data.set(k, v)
|
|
||||||
this.lru.set(k, Date.now())
|
|
||||||
}
|
|
||||||
delete(k) {
|
|
||||||
this.data.delete(k)
|
|
||||||
this.lru.delete(k)
|
|
||||||
}
|
|
||||||
get(k) {
|
|
||||||
return this.data.get(k)
|
|
||||||
}
|
|
||||||
size() {
|
|
||||||
return this.data.size
|
|
||||||
}
|
|
||||||
entries() {
|
|
||||||
return this.data.entries()
|
|
||||||
}
|
|
||||||
keys() {
|
|
||||||
return this.data.keys()
|
|
||||||
}
|
|
||||||
values() {
|
|
||||||
return this.data.values()
|
|
||||||
}
|
|
||||||
find(f) {
|
|
||||||
for (const v of this.data.values()) {
|
|
||||||
if (f(v)) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
load(entries) {
|
|
||||||
for (const [k, {value, lru}] of entries) {
|
|
||||||
this.data.set(k, value)
|
|
||||||
this.lru.set(k, lru)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dump(): Array<CacheEntry> {
|
|
||||||
return Array.from(this.data.keys()).map(k => [
|
|
||||||
k,
|
|
||||||
{value: this.data.get(k), lru: this.lru.get(k)},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
prune() {
|
|
||||||
if (this.data.size <= this.max) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k of this.sort(this.dump()).map(nth(0)).slice(this.max)) {
|
|
||||||
this.delete(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clear() {
|
|
||||||
this.data.clear()
|
|
||||||
this.lru.clear()
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,12 +9,7 @@ import {
|
|||||||
mergeDeepRight,
|
mergeDeepRight,
|
||||||
aperture,
|
aperture,
|
||||||
filter,
|
filter,
|
||||||
path as getPath,
|
|
||||||
allPass,
|
|
||||||
pipe,
|
|
||||||
isNil,
|
isNil,
|
||||||
complement,
|
|
||||||
equals,
|
|
||||||
is,
|
is,
|
||||||
pluck,
|
pluck,
|
||||||
sum,
|
sum,
|
||||||
@ -173,7 +168,7 @@ export class Cursor {
|
|||||||
until: number
|
until: number
|
||||||
limit: number
|
limit: number
|
||||||
count: number
|
count: number
|
||||||
constructor({limit = 50, delta = undefined} = {}) {
|
constructor({limit = 20, delta = undefined} = {}) {
|
||||||
this.delta = delta
|
this.delta = delta
|
||||||
this.since = delta ? now() - delta : undefined
|
this.since = delta ? now() - delta : undefined
|
||||||
this.until = now()
|
this.until = now()
|
||||||
@ -266,42 +261,6 @@ export const defer = (): Deferred<any> => {
|
|||||||
|
|
||||||
export const avg = xs => sum(xs) / xs.length
|
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(".")
|
|
||||||
|
|
||||||
if (operator.startsWith("!")) {
|
|
||||||
operator = operator.slice(1)
|
|
||||||
modifier = complement
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operator === "eq" && is(Array, value)) {
|
|
||||||
test = v => (value as Array<any>).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))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/21682946
|
// https://stackoverflow.com/a/21682946
|
||||||
export const stringToHue = value => {
|
export const stringToHue = value => {
|
||||||
let hash = 0
|
let hash = 0
|
||||||
|
Loading…
Reference in New Issue
Block a user