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
|
||||
|
||||
- [ ] 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
|
||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -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",
|
||||
|
@ -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"
|
||||
|
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,
|
||||
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"
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 =>
|
||||
|
@ -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,
|
||||
} 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,
|
||||
|
@ -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 petnames = derived(profile, prop("petnames"))
|
||||
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(
|
||||
[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 {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"),
|
||||
|
@ -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 () => {
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 Notifications from "src/app2/views/Notifications.svelte"
|
||||
import Bech32Entity from "src/app2/views/Bech32Entity.svelte"
|
||||
|
@ -54,8 +54,8 @@
|
||||
import keys from "src/agent/keys"
|
||||
import network from "src/agent/network"
|
||||
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import {getPersonWithFallback} from "src/agent/db"
|
||||
import {watch} from "src/agent/db"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {routes} from "src/app/ui"
|
||||
import {publishWithToast} from "src/app"
|
||||
@ -666,7 +666,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<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">
|
||||
{#each note.seen_on as url}
|
||||
<RelayCard theme="black" relay={{url}} />
|
||||
|
@ -14,7 +14,7 @@
|
||||
import {sampleRelays} from "src/agent/relays"
|
||||
import user from "src/agent/user"
|
||||
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"
|
||||
|
||||
export let note
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {parseContent} from "src/util/html"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {getPersonWithFallback} from "src/agent/db"
|
||||
|
||||
export let person
|
||||
export let truncate = false
|
||||
|
@ -6,8 +6,8 @@
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
||||
import {sampleRelays, getPubkeyWriteRelays} 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"
|
||||
|
||||
export let type
|
||||
|
@ -5,7 +5,7 @@
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonInfo from "src/app2/shared/PersonInfo.svelte"
|
||||
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 user from "src/agent/user"
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
const {petnamePubkeys} = user
|
||||
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"],
|
||||
})
|
||||
)
|
||||
|
@ -1,35 +1,36 @@
|
||||
<script lang="ts">
|
||||
import {last} from "ramda"
|
||||
import {last, nth} from "ramda"
|
||||
import {navigate} from "svelte-routing"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import user from "src/agent/user"
|
||||
import {sampleRelays, getPubkeyWriteRelays} 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 {routes} from "src/app/ui"
|
||||
import PersonCircle from "src/app2/shared/PersonCircle.svelte"
|
||||
import PersonAbout from "src/app2/shared/PersonAbout.svelte"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const {petnamePubkeys, canPublish} = user
|
||||
const {petnamePubkeys, mutes, canPublish} = user
|
||||
const getRelays = () => sampleRelays(getPubkeyWriteRelays(pubkey))
|
||||
const person = watch("people", () => getPersonWithFallback(pubkey))
|
||||
|
||||
let following = false
|
||||
|
||||
$: following = $petnamePubkeys.includes(pubkey)
|
||||
$: muted = $mutes.map(nth(1)).includes(pubkey)
|
||||
|
||||
const follow = async () => {
|
||||
const follow = () => {
|
||||
const [{url}] = getRelays()
|
||||
|
||||
user.addPetname(pubkey, url, displayPerson($person))
|
||||
}
|
||||
|
||||
const unfollow = async () => {
|
||||
user.removePetname(pubkey)
|
||||
}
|
||||
const unfollow = () => user.removePetname(pubkey)
|
||||
|
||||
const unmute = () => user.removeMute(pubkey)
|
||||
|
||||
const mute = () => user.addMute("p", pubkey)
|
||||
</script>
|
||||
|
||||
<div class="relative flex flex-col gap-4 py-2 px-3">
|
||||
@ -49,16 +50,26 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-4 py-2 text-lg">
|
||||
{#if $canPublish}
|
||||
{#if following}
|
||||
<Anchor type="button-circle" on:click={unfollow}>
|
||||
<i class="fa fa-user-minus" />
|
||||
</Anchor>
|
||||
{#if muted}
|
||||
<i
|
||||
title="Unmute"
|
||||
class="fa fa-microphone-slash w-6 cursor-pointer text-center"
|
||||
on:click={unmute} />
|
||||
{:else}
|
||||
<Anchor type="button-circle" on:click={follow}>
|
||||
<i class="fa fa-user-plus" />
|
||||
</Anchor>
|
||||
<i title="Mute" class="fa fa-microphone w-6 cursor-pointer text-center" on:click={mute} />
|
||||
{/if}
|
||||
{#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}
|
||||
</div>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = ""
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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 => ({
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {getPersonWithFallback} from "src/agent/db"
|
||||
|
||||
export let pubkeys
|
||||
</script>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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)),
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -172,31 +171,31 @@
|
||||
background-size: cover;
|
||||
background-image:
|
||||
linear-gradient(to bottom, {rgba}, {rgb}),
|
||||
url('{person.kind0?.banner}')" />
|
||||
url('{$person.kind0?.banner}')" />
|
||||
|
||||
<Content>
|
||||
<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 items-start justify-between gap-4">
|
||||
<div class="flex flex-grow flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-2xl">{displayPerson(person)}</h1>
|
||||
<h1 class="text-2xl">{displayPerson($person)}</h1>
|
||||
</div>
|
||||
{#if person.verified_as}
|
||||
{#if $person.verified_as}
|
||||
<div class="flex gap-1 text-sm">
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
<OverflowMenu {actions} />
|
||||
</div>
|
||||
<PersonAbout {person} />
|
||||
{#if person?.petnames}
|
||||
<PersonAbout person={$person} />
|
||||
{#if $person?.petnames}
|
||||
<div class="flex gap-8" in:fly={{y: 20}}>
|
||||
<button on:click={showFollows}>
|
||||
<strong>{person.petnames.length}</strong> following
|
||||
<strong>{$person.petnames.length}</strong> following
|
||||
</button>
|
||||
<button on:click={showFollowers}>
|
||||
<strong>{numberFmt.format($followersCount)}</strong> followers
|
||||
|
@ -4,7 +4,7 @@
|
||||
import Feed from "src/app2/shared/Feed.svelte"
|
||||
import RelayTitle from "src/app2/shared/RelayTitle.svelte"
|
||||
import RelayActions from "src/app2/shared/RelayActions.svelte"
|
||||
import {relays} from "src/agent/tables"
|
||||
import {relays} from "src/agent/db"
|
||||
|
||||
export let url
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
import {sleep, createScroller, Cursor} from "src/util/misc"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import user from "src/agent/user"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {getPersonWithFallback} from "src/agent/db"
|
||||
import network from "src/agent/network"
|
||||
|
||||
export let loadMessages
|
||||
|
@ -6,7 +6,7 @@
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import ContentEditable from "src/partials/ContentEditable.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"
|
||||
|
||||
export let onSubmit
|
||||
@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
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 => {
|
||||
|
@ -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,
|
||||
aperture,
|
||||
filter,
|
||||
path as getPath,
|
||||
allPass,
|
||||
pipe,
|
||||
isNil,
|
||||
complement,
|
||||
equals,
|
||||
is,
|
||||
pluck,
|
||||
sum,
|
||||
@ -173,7 +168,7 @@ export class Cursor {
|
||||
until: number
|
||||
limit: number
|
||||
count: number
|
||||
constructor({limit = 50, delta = undefined} = {}) {
|
||||
constructor({limit = 20, delta = undefined} = {}) {
|
||||
this.delta = delta
|
||||
this.since = delta ? now() - delta : undefined
|
||||
this.until = now()
|
||||
@ -266,42 +261,6 @@ export const defer = (): Deferred<any> => {
|
||||
|
||||
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
|
||||
export const stringToHue = value => {
|
||||
let hash = 0
|
||||
|
Loading…
Reference in New Issue
Block a user