mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-28 16:00:52 +00:00
Add new database built on localforage, apply to people
This commit is contained in:
parent
c1569a3903
commit
3ccec4d5b5
@ -54,6 +54,7 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
|
||||
# Maintenance
|
||||
|
||||
- [ ] Normalize relay urls (lowercase, strip trailing slash)
|
||||
- [ ] Use nip 56 for reporting
|
||||
- https://github.com/nostr-protocol/nips/pull/205#issuecomment-1419234230
|
||||
- [ ] Change network tab to list relays the user is connected to
|
||||
@ -65,7 +66,8 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
|
||||
# Current
|
||||
|
||||
- [ ] Make threshold it a setting, or maybe avoid `load` altogether
|
||||
- [ ] Switch to localforage
|
||||
- Check that firefox private mode works (it won't work in dev)
|
||||
- [ ] Add modal for follows/followers
|
||||
- [ ] Implement gossip model https://bountsr.org/code/2023/02/03/gossip-model.html
|
||||
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
|
||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -36,6 +36,7 @@
|
||||
"fuse.js": "^6.6.2",
|
||||
"hurdak": "github:ConsignCloud/hurdak",
|
||||
"husky": "^8.0.3",
|
||||
"localforage": "^1.10.0",
|
||||
"nostr-tools": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ramda": "^0.28.0",
|
||||
|
@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@fortawesome/fontawesome-free/css/fontawesome.css"
|
||||
import "@fortawesome/fontawesome-free/css/solid.css"
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
import Chat from "src/routes/Chat.svelte"
|
||||
import ChatRoom from "src/routes/ChatRoom.svelte"
|
||||
import Messages from "src/routes/Messages.svelte"
|
||||
import _db from 'src/agent/database'
|
||||
|
||||
export let url = ""
|
||||
|
||||
@ -133,7 +134,9 @@
|
||||
|
||||
return {...await res.json(), url, refreshed_at: now()}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
if (!e.toString().includes('Failed to fetch')) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
return {url, refreshed_at: now()}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import Dexie, {liveQuery} from 'dexie'
|
||||
import {pick, isEmpty} from 'ramda'
|
||||
import {nip05} from 'nostr-tools'
|
||||
import {writable} from 'svelte/store'
|
||||
import {noop, ensurePlural, createMap, switcherFn} from 'hurdak/lib/hurdak'
|
||||
import {noop, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
|
||||
import {now} from 'src/util/misc'
|
||||
import {personKinds, Tags, roomAttrs, isRelay} from 'src/util/nostr'
|
||||
import database from 'src/agent/database'
|
||||
|
||||
export const lq = cb => liveQuery(async () => {
|
||||
try {
|
||||
@ -20,38 +20,11 @@ db.version(13).stores({
|
||||
relays: '++url, name',
|
||||
alerts: '++id, created_at',
|
||||
messages: '++id, pubkey, recipient',
|
||||
people: '++pubkey',
|
||||
rooms: '++id, joined',
|
||||
})
|
||||
|
||||
// A flag for hiding things that rely on people being loaded initially
|
||||
export const ready = writable(false)
|
||||
|
||||
// Some things work better as observables than database tables
|
||||
export const people = writable([])
|
||||
|
||||
// Bootstrap our people observable
|
||||
db.table('people').toArray().then($p => {
|
||||
people.set(createMap('pubkey', $p))
|
||||
ready.set(true)
|
||||
})
|
||||
|
||||
// Sync to a regular object so we have a synchronous interface
|
||||
let $people = {}
|
||||
people.subscribe($p => {
|
||||
$people = $p
|
||||
})
|
||||
|
||||
// Our synchronous interface
|
||||
export const getPerson = (pubkey, fallback = false) =>
|
||||
$people[pubkey] || (fallback ? {pubkey} : null)
|
||||
|
||||
export const updatePeople = async updates => {
|
||||
// Sync to our in memory copy
|
||||
people.update($people => ({...$people, ...updates}))
|
||||
|
||||
// Sync to our database
|
||||
await db.table('people').bulkPut(Object.values(updates))
|
||||
await database.people.bulkPut(updates)
|
||||
}
|
||||
|
||||
// Hooks
|
||||
@ -70,7 +43,7 @@ const processProfileEvents = async events => {
|
||||
|
||||
const updates = {}
|
||||
for (const e of profileEvents) {
|
||||
const person = getPerson(e.pubkey, true)
|
||||
const person = database.getPersonWithFallback(e.pubkey)
|
||||
|
||||
updates[e.pubkey] = {
|
||||
...person,
|
||||
@ -92,8 +65,10 @@ const processProfileEvents = async events => {
|
||||
},
|
||||
2: () => {
|
||||
if (e.created_at > (person.relays_updated_at || 0)) {
|
||||
const {relays = []} = database.getPersonWithFallback(e.pubkey)
|
||||
|
||||
return {
|
||||
relays: ($people[e.pubkey]?.relays || []).concat({url: e.content}),
|
||||
relays: relays.concat({url: e.content}),
|
||||
relays_updated_at: e.created_at,
|
||||
}
|
||||
}
|
||||
@ -200,7 +175,7 @@ const processMessages = async events => {
|
||||
const verifyNip05 = (pubkey, as) =>
|
||||
nip05.queryProfile(as).then(result => {
|
||||
if (result?.pubkey === pubkey) {
|
||||
const person = getPerson(pubkey, true)
|
||||
const person = database.getPersonWithFallback(pubkey)
|
||||
|
||||
updatePeople({[pubkey]: {...person, verified_as: as}})
|
||||
}
|
||||
|
238
src/agent/database.ts
Normal file
238
src/agent/database.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import {is, prop, without} from 'ramda'
|
||||
import {writable} from 'svelte/store'
|
||||
import {switcherFn, ensurePlural, first} from 'hurdak/lib/hurdak'
|
||||
import {defer, asyncIterableToArray} from 'src/util/misc'
|
||||
|
||||
// Types
|
||||
|
||||
type Message = {
|
||||
topic: string
|
||||
payload: object
|
||||
}
|
||||
|
||||
type Table = {
|
||||
name: string
|
||||
subscribe: (subscription: (value: any) => void) => (() => void)
|
||||
bulkPut: (data: object) => void
|
||||
all: (where: object) => Promise<any>
|
||||
get: (key: string) => any
|
||||
}
|
||||
|
||||
// Plumbing
|
||||
|
||||
const worker = new Worker('/src/workers/database.js', {type: 'module'})
|
||||
|
||||
worker.addEventListener('error', e => console.error(e))
|
||||
|
||||
class Channel {
|
||||
id: string
|
||||
onMessage: (e: MessageEvent) => void
|
||||
constructor({onMessage}) {
|
||||
this.id = Math.random().toString().slice(2)
|
||||
this.onMessage = e => 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 callLocalforage = async (storeName, method, ...args) => {
|
||||
const message = await call('localforage.call', {storeName, method, args})
|
||||
|
||||
if (message.topic !== 'localforage.return') {
|
||||
throw new Error(`callLocalforage received invalid response: ${message}`)
|
||||
}
|
||||
|
||||
return message.payload
|
||||
}
|
||||
|
||||
|
||||
// Methods that proxy localforage
|
||||
|
||||
const getItem = (storeName, ...args) => callLocalforage(storeName, 'getItem', ...args)
|
||||
const setItem = (storeName, ...args) => callLocalforage(storeName, 'setItem', ...args)
|
||||
const removeItem = (storeName, ...args) => callLocalforage(storeName, 'removeItem', ...args)
|
||||
const length = (storeName) => callLocalforage(storeName, 'length')
|
||||
const clear = (storeName) => callLocalforage(storeName, 'clear')
|
||||
const keys = (storeName) => callLocalforage(storeName, 'keys')
|
||||
|
||||
const iterate = (storeName, where = {}) => ({
|
||||
[Symbol.asyncIterator]() {
|
||||
let done = false
|
||||
let promise = defer()
|
||||
const messages = []
|
||||
const channel = new Channel({
|
||||
onMessage: m => switcherFn(m.topic, {
|
||||
'localforage.item': () => {
|
||||
promise.resolve()
|
||||
messages.push(m.payload)
|
||||
},
|
||||
'localforage.iterationComplete': () => {
|
||||
done = true
|
||||
promise.resolve()
|
||||
channel.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
channel.send('localforage.iterate', {storeName, where})
|
||||
|
||||
const next = async () => {
|
||||
if (done) {
|
||||
return {done}
|
||||
}
|
||||
|
||||
const [value] = messages.splice(0, 1)
|
||||
|
||||
if (value) {
|
||||
return {done, value}
|
||||
} else {
|
||||
promise = defer()
|
||||
|
||||
await promise
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
return {next}
|
||||
}
|
||||
})
|
||||
|
||||
// Local copy of data so we can provide a sync observable interface. The worker
|
||||
// is just for storing data and processing expensive queries
|
||||
|
||||
const registry = {}
|
||||
|
||||
const defineTable = (name: string): Table => {
|
||||
let p = Promise.resolve()
|
||||
let listeners = []
|
||||
let data = {}
|
||||
|
||||
const subscribe = f => {
|
||||
listeners.push(f)
|
||||
|
||||
f(data)
|
||||
|
||||
return () => {
|
||||
listeners = without([f], listeners)
|
||||
}
|
||||
}
|
||||
|
||||
const setAndNotify = newData => {
|
||||
// Update our local copy
|
||||
data = newData
|
||||
|
||||
// Notify subscribers
|
||||
for (const f of listeners) {
|
||||
f(data)
|
||||
}
|
||||
}
|
||||
|
||||
const bulkPut = newData => {
|
||||
setAndNotify({...data, ...newData})
|
||||
|
||||
// Sync to storage, keeping updates in order
|
||||
p = p.then(() => {
|
||||
const updates = []
|
||||
for (const [k, v] of Object.entries(newData)) {
|
||||
updates.push(setItem(name, k, v))
|
||||
}
|
||||
|
||||
return Promise.all(updates)
|
||||
}) as Promise<void>
|
||||
}
|
||||
|
||||
const all = (where = {}) => asyncIterableToArray(iterate(name, where), prop('v'))
|
||||
const one = (where = {}) => first(all(where))
|
||||
const get = k => data[k]
|
||||
|
||||
// Sync from storage initially
|
||||
;(async () => {
|
||||
const initialData = {}
|
||||
for await (const {k, v} of iterate(name)) {
|
||||
initialData[k] = v
|
||||
}
|
||||
|
||||
setAndNotify(initialData)
|
||||
})()
|
||||
|
||||
registry[name] = {name, subscribe, bulkPut, all, one, get}
|
||||
|
||||
return registry[name]
|
||||
}
|
||||
|
||||
const people = defineTable('people')
|
||||
|
||||
// Helper to allow us to listen to changes of any given table
|
||||
|
||||
const listener = (() => {
|
||||
let listeners = []
|
||||
|
||||
for (const table of Object.values(registry) as Array<Table>) {
|
||||
table.subscribe(() => listeners.forEach(f => f(table.name)))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: f => {
|
||||
listeners.push(f)
|
||||
|
||||
return () => {
|
||||
listeners = without([f], listeners)
|
||||
}
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
// Helper to re-run a query every time a given table changes
|
||||
|
||||
const watch = (names, f) => {
|
||||
names = ensurePlural(names)
|
||||
|
||||
const store = writable(null)
|
||||
const tables = names.map(name => registry[name])
|
||||
|
||||
// Initialize synchronously if possible
|
||||
const initialValue = f(...tables)
|
||||
if (is(Promise, initialValue)) {
|
||||
initialValue.then(v => store.set(v))
|
||||
} else {
|
||||
store.set(initialValue)
|
||||
}
|
||||
|
||||
// Listen for changes
|
||||
listener.subscribe(async name => {
|
||||
if (names.includes(name)) {
|
||||
store.set(await f(...tables))
|
||||
}
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// Other utilities
|
||||
|
||||
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
|
||||
|
||||
export default {
|
||||
getItem, setItem, removeItem, length, clear, keys, iterate,
|
||||
watch, getPersonWithFallback, people,
|
||||
}
|
@ -1,28 +1,31 @@
|
||||
import type {Person} from 'src/util/types'
|
||||
import type {Readable} from 'svelte/store'
|
||||
import {last, uniqBy, prop} from 'ramda'
|
||||
import {derived, get} from 'svelte/store'
|
||||
import {Tags} from 'src/util/nostr'
|
||||
import pool from 'src/agent/pool'
|
||||
import keys from 'src/agent/keys'
|
||||
import defaults from 'src/agent/defaults'
|
||||
import {lq, db, people, ready, getPerson, processEvents} from 'src/agent/data'
|
||||
import database from 'src/agent/database'
|
||||
import {lq, db, processEvents} from 'src/agent/data'
|
||||
|
||||
Object.assign(window, {pool, db})
|
||||
Object.assign(window, {pool, db, database})
|
||||
|
||||
export {pool, keys, lq, db, ready, people, getPerson}
|
||||
export {pool, keys, lq, db, database}
|
||||
|
||||
export const user = derived(
|
||||
[keys.pubkey, people],
|
||||
[keys.pubkey, database.people as Readable<any>],
|
||||
([pubkey, $people]) => {
|
||||
if (!pubkey) {
|
||||
return null
|
||||
}
|
||||
|
||||
return $people[pubkey] || {pubkey}
|
||||
return ($people[pubkey] || {pubkey})
|
||||
}
|
||||
)
|
||||
) as Readable<Person>
|
||||
|
||||
export const getMuffle = () => {
|
||||
const $user = get(user)
|
||||
const $user = get(user) as Person
|
||||
|
||||
if (!$user?.muffle) {
|
||||
return []
|
||||
@ -34,16 +37,16 @@ export const getMuffle = () => {
|
||||
}
|
||||
|
||||
export const getFollows = pubkey => {
|
||||
const person = getPerson(pubkey)
|
||||
const person = database.getPersonWithFallback(pubkey)
|
||||
|
||||
return Tags.wrap(person?.petnames || defaults.petnames).values().all()
|
||||
return Tags.wrap(person.petnames || defaults.petnames).values().all()
|
||||
}
|
||||
|
||||
export const getRelays = (pubkey?: string) => {
|
||||
let relays = getPerson(pubkey)?.relays
|
||||
let relays = database.getPersonWithFallback(pubkey).relays
|
||||
|
||||
if (!relays?.length) {
|
||||
relays = getPerson(get(keys.pubkey))?.relays
|
||||
relays = database.getPersonWithFallback(get(keys.pubkey)).relays
|
||||
}
|
||||
|
||||
if (!relays?.length) {
|
||||
|
@ -183,7 +183,7 @@ const subscribe = async (relays, filters, {onEvent, onEose}: Record<string, (e:
|
||||
}
|
||||
}
|
||||
|
||||
const request = (relays, filters, {threshold = 0.5} = {}): Promise<Record<string, unknown>[]> => {
|
||||
const request = (relays, filters, {threshold = 0.1} = {}): Promise<Record<string, unknown>[]> => {
|
||||
return new Promise(async resolve => {
|
||||
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
|
||||
threshold = relays.length * threshold
|
||||
|
@ -2,7 +2,7 @@ import {prop, pick, join, uniqBy, last} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
import {Tags, isRelay, roomAttrs, displayPerson} from 'src/util/nostr'
|
||||
import {keys, publish, getRelays, getPerson} from 'src/agent'
|
||||
import {keys, publish, getRelays, database} from 'src/agent'
|
||||
|
||||
const updateUser = (relays, updates) =>
|
||||
publishEvent(relays, 0, {content: JSON.stringify(updates)})
|
||||
@ -32,7 +32,7 @@ const createDirectMessage = (relays, pubkey, content) =>
|
||||
const createNote = (relays, content, mentions = [], topics = []) => {
|
||||
mentions = mentions.map(p => {
|
||||
const {url} = first(getRelays(p))
|
||||
const name = displayPerson(getPerson(p, true))
|
||||
const name = displayPerson(database.getPersonWithFallback(p))
|
||||
|
||||
return ["p", p, url, name]
|
||||
})
|
||||
|
@ -1,10 +1,11 @@
|
||||
import type {Person} from 'src/util/types'
|
||||
import {pluck, whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {get} from 'svelte/store'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
|
||||
import {user, people, getPerson, getRelays, keys} from 'src/agent'
|
||||
import {user, database, getRelays, keys} from 'src/agent'
|
||||
import defaults from 'src/agent/defaults'
|
||||
import {toast, routes, modal, settings, logUsage} from 'src/app/ui'
|
||||
import cmd from 'src/app/cmd'
|
||||
@ -45,7 +46,7 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
|
||||
}
|
||||
|
||||
export const addRelay = async url => {
|
||||
const person = get(user)
|
||||
const person = get(user) as Person
|
||||
const modify = relays => relays.concat({url, write: '!'})
|
||||
|
||||
// Set to defaults to support anonymous usage
|
||||
@ -63,7 +64,7 @@ export const addRelay = async url => {
|
||||
}
|
||||
|
||||
export const removeRelay = async url => {
|
||||
const person = get(user)
|
||||
const person = get(user) as Person
|
||||
const modify = relays => reject(whereEq({url}), relays)
|
||||
|
||||
// Set to defaults to support anonymous usage
|
||||
@ -75,7 +76,7 @@ export const removeRelay = async url => {
|
||||
}
|
||||
|
||||
export const setRelayWriteCondition = async (url, write) => {
|
||||
const person = get(user)
|
||||
const person = get(user) as Person
|
||||
const modify = relays => relays.map(when(whereEq({url}), assoc('write', write)))
|
||||
|
||||
// Set to defaults to support anonymous usage
|
||||
@ -88,10 +89,9 @@ export const setRelayWriteCondition = async (url, write) => {
|
||||
|
||||
export const renderNote = (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const $people = get(people)
|
||||
const peopleByPubkey = createMap(
|
||||
'pubkey',
|
||||
Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity)
|
||||
Tags.from(note).type("p").values().all().map(k => database.people.get(k)).filter(identity)
|
||||
)
|
||||
|
||||
let content
|
||||
@ -126,7 +126,7 @@ export const annotate = (note, context) => {
|
||||
|
||||
return {
|
||||
...note, reactions,
|
||||
person: getPerson(note.pubkey),
|
||||
person: database.people.get(note.pubkey),
|
||||
replies: sortBy(e => e.created_at, replies).map(r => annotate(r, context)),
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ import {uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
|
||||
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
|
||||
import {findReply, personKinds, Tags} from 'src/util/nostr'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import {load, getPerson, getFollows} from 'src/agent'
|
||||
import {load, database, getFollows} from 'src/agent'
|
||||
|
||||
const getStalePubkeys = pubkeys => {
|
||||
// If we're not reloading, only get pubkeys we don't already know about
|
||||
return uniq(pubkeys).filter(pubkey => {
|
||||
const p = getPerson(pubkey)
|
||||
const p = database.people.get(pubkey)
|
||||
|
||||
return !p || p.updated_at < now() - timedelta(1, 'days')
|
||||
})
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Badge from 'src/partials/Badge.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import {user, getPerson} from 'src/agent'
|
||||
import {user, database} from 'src/agent'
|
||||
import {renderNote} from 'src/app'
|
||||
|
||||
export let name
|
||||
@ -32,7 +32,7 @@
|
||||
// Group messages so we're only showing the person once per chunk
|
||||
annotatedMessages = reverse(sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
|
||||
(mx, m) => {
|
||||
const person = getPerson(m.pubkey, true)
|
||||
const person = database.getPersonWithFallback(m.pubkey)
|
||||
const showPerson = person.pubkey !== getPath(['person', 'pubkey'], last(mx))
|
||||
|
||||
return mx.concat({...m, person, showPerson})
|
||||
|
@ -5,7 +5,7 @@
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import {fromParentOffset} from "src/util/html"
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {people} from "src/agent/data"
|
||||
import {database} from "src/agent"
|
||||
|
||||
export let onSubmit
|
||||
|
||||
@ -14,10 +14,11 @@
|
||||
let suggestions = []
|
||||
let input = null
|
||||
let prevContent = ''
|
||||
let search = fuzzy(
|
||||
Object.values($people).filter(prop('name')),
|
||||
{keys: ["name", "pubkey"]}
|
||||
)
|
||||
let search
|
||||
|
||||
database.people.all({'name:!nil': null}).then(people => {
|
||||
search = fuzzy(people, {keys: ["name", "pubkey"]})
|
||||
})
|
||||
|
||||
const getText = () => {
|
||||
const selection = document.getSelection()
|
||||
|
@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
{#if size === 'inherit'}
|
||||
<div {...$$props} class={cx($$props.class, className)}>
|
||||
<div {...$$props} class={cx($$props.class, className, "w-full")}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -5,7 +5,7 @@
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import {killEvent} from 'src/util/html'
|
||||
import {getPerson} from 'src/agent'
|
||||
import {database} from 'src/agent'
|
||||
import {modal} from 'src/app'
|
||||
|
||||
export let note
|
||||
@ -41,7 +41,7 @@
|
||||
class="absolute top-0 mt-8 py-2 px-4 rounded border border-solid border-medium
|
||||
bg-dark grid grid-cols-3 gap-y-2 gap-x-4 z-20">
|
||||
{#each uniq(note.likedBy) as pubkey}
|
||||
<Badge person={getPerson(pubkey)} />
|
||||
<Badge person={database.getPersonWithFallback(pubkey)} />
|
||||
{/each}
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -14,7 +14,7 @@
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import {user, people, getPerson, getRelays, getEventRelays} from 'src/agent'
|
||||
import {user, database, getRelays, getEventRelays} from 'src/agent'
|
||||
import cmd from 'src/app/cmd'
|
||||
|
||||
export let note
|
||||
@ -34,10 +34,9 @@
|
||||
const showEntire = anchorId === note.id
|
||||
const interactive = !anchorId || !showEntire
|
||||
const relays = getEventRelays(note)
|
||||
const person = database.watch('people', p => p.get(note.pubkey) || {pubkey: note.pubkey})
|
||||
|
||||
let likes, flags, like, flag, person
|
||||
|
||||
$: person = $people[note.pubkey] || {pubkey: note.pubkey}
|
||||
let likes, flags, like, flag
|
||||
|
||||
$: {
|
||||
likes = note.reactions.filter(n => isLike(n.content))
|
||||
@ -138,7 +137,7 @@
|
||||
|
||||
<Card on:click={onClick} {interactive} {invertColors}>
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<Badge person={person} />
|
||||
<Badge person={$person} />
|
||||
<Anchor
|
||||
href={"/" + nip19.neventEncode({id: note.id, relays: pluck('url', relays.slice(0, 5))})}
|
||||
class="text-sm text-light"
|
||||
@ -205,7 +204,7 @@
|
||||
{#each replyMentions as p}
|
||||
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
|
||||
<button class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
|
||||
{displayPerson(getPerson(p, true))}
|
||||
{displayPerson(database.getPersonWithFallback(p))}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="-mt-2" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {navigate} from "svelte-routing"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {getRelays, user, lq, getPerson, listen, db} from 'src/agent'
|
||||
import {getRelays, user, lq, database, listen, db} from 'src/agent'
|
||||
import {modal, messages} from 'src/app'
|
||||
import loaders from 'src/app/loaders'
|
||||
import Room from "src/partials/Room.svelte"
|
||||
@ -26,7 +26,7 @@
|
||||
await loaders.loadPeople(getRelays(), pubkeys)
|
||||
|
||||
return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys)
|
||||
.map(k => ({type: 'npub', id: k, ...getPerson(k, true)}))
|
||||
.map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)}))
|
||||
.concat(rooms.map(room => ({type: 'note', ...room})))
|
||||
})
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {personKinds} from 'src/util/nostr'
|
||||
import {batch, now} from 'src/util/misc'
|
||||
import Channel from 'src/partials/Channel.svelte'
|
||||
import {lq, getRelays, user, db, listen, keys} from 'src/agent'
|
||||
import {database, getRelays, user, db, listen, keys} from 'src/agent'
|
||||
import {messages} from 'src/app'
|
||||
import {routes} from 'src/app/ui'
|
||||
import cmd from 'src/app/cmd'
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
let crypt = keys.getCrypt()
|
||||
let {data: pubkey} = nip19.decode(entity) as {data: string}
|
||||
let person = lq(() => db.table('people').get(pubkey))
|
||||
let person = database.watch('people', p => p.get(pubkey))
|
||||
|
||||
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
import Notes from "src/views/person/Notes.svelte"
|
||||
import Likes from "src/views/person/Likes.svelte"
|
||||
import Network from "src/views/person/Network.svelte"
|
||||
import {getPerson, getRelays, listen, user, keys} from "src/agent"
|
||||
import {database, getRelays, listen, user, keys} from "src/agent"
|
||||
import {modal} from "src/app"
|
||||
import loaders from "src/app/loaders"
|
||||
import {routes} from "src/app/ui"
|
||||
@ -29,7 +29,7 @@
|
||||
let following = false
|
||||
let followers = new Set()
|
||||
let followersCount = 0
|
||||
let person = getPerson(pubkey, true)
|
||||
let person = database.getPersonWithFallback(pubkey)
|
||||
let loading = true
|
||||
|
||||
$: following = find(t => t[1] === pubkey, $user?.petnames || [])
|
||||
@ -37,7 +37,7 @@
|
||||
onMount(async () => {
|
||||
// Refresh our person if needed
|
||||
loaders.loadPeople(relays || getRelays(pubkey), [pubkey]).then(() => {
|
||||
person = getPerson(pubkey, true)
|
||||
person = database.getPersonWithFallback(pubkey)
|
||||
loading = false
|
||||
})
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import RelayCard from "src/partials/RelayCard.svelte"
|
||||
import {lq, pool, db, user, ready} from "src/agent"
|
||||
import {lq, pool, db, user} from "src/agent"
|
||||
import {modal, settings} from "src/app"
|
||||
import defaults from "src/agent/defaults"
|
||||
|
||||
@ -41,20 +41,18 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Attempt to connect so we can show status
|
||||
relays.forEach(relay => pool.connect(relay.url))
|
||||
return poll(1000, async () => {
|
||||
const userRelays = $user?.relays || []
|
||||
const urls = pluck('url', userRelays)
|
||||
const relaysByUrl = createMap(
|
||||
'url',
|
||||
await db.table('relays').where('url').anyOf(urls).toArray()
|
||||
)
|
||||
|
||||
return poll(300, async () => {
|
||||
if ($ready) {
|
||||
const userRelays = $user?.relays || []
|
||||
const urls = pluck('url', userRelays)
|
||||
const relaysByUrl = createMap(
|
||||
'url',
|
||||
await db.table('relays').where('url').anyOf(urls).toArray()
|
||||
)
|
||||
relays = userRelays.map(relay => ({...relaysByUrl[relay.url], ...relay}))
|
||||
|
||||
relays = userRelays.map(relay => ({...relaysByUrl[relay.url], ...relay}))
|
||||
}
|
||||
// Attempt to connect so we can show status
|
||||
relays.forEach(relay => pool.connect(relay.url))
|
||||
|
||||
status = Object.fromEntries(
|
||||
pool.getConnections().map(({url, status, stats}) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {pluck, sortBy} from "ramda"
|
||||
import {pluck, identity, sortBy} from "ramda"
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
import {writable} from 'svelte/store'
|
||||
import {isObject} from 'hurdak/lib/hurdak'
|
||||
@ -179,3 +179,22 @@ export const batch = (t, f) => {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
export const defer = () => {
|
||||
let resolve, reject
|
||||
const p = new Promise((resolve_, reject_) => {
|
||||
resolve = resolve_
|
||||
reject = reject_
|
||||
})
|
||||
|
||||
return Object.assign(p, {resolve, reject})
|
||||
}
|
||||
|
||||
export const asyncIterableToArray = async (it, f = identity) => {
|
||||
const result = []
|
||||
for await (const x of it) {
|
||||
result.push(f(x))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
11
src/util/types.ts
Normal file
11
src/util/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type Relay = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type Person = {
|
||||
pubkey: string
|
||||
picture?: string
|
||||
relays?: Array<Relay>
|
||||
muffle?: Array<Array<string>>
|
||||
petnames?: Array<Array<string>>
|
||||
}
|
@ -1,23 +1,21 @@
|
||||
<script>
|
||||
import {prop} from 'ramda'
|
||||
import {ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {renderContent} from "src/util/html"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import {user, people} from 'src/agent'
|
||||
import {user, database} from 'src/agent'
|
||||
import {routes} from "src/app/ui"
|
||||
|
||||
export let q
|
||||
|
||||
let search
|
||||
|
||||
$: search = fuzzy(
|
||||
Object.values($people).filter(prop('name')),
|
||||
{keys: ["name", "about", "pubkey"]}
|
||||
)
|
||||
database.people.all({'name:!nil': null}).then(people => {
|
||||
search = fuzzy(people, {keys: ["name", "about", "pubkey"]})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each search(q).slice(0, 30) as p (p.pubkey)}
|
||||
{#each (search ? search(q) : []).slice(0, 30) as p (p.pubkey)}
|
||||
{#if p.pubkey !== $user.pubkey}
|
||||
<a href={routes.person(p.pubkey)} class="flex gap-4">
|
||||
<div
|
||||
|
73
src/workers/database.js
Normal file
73
src/workers/database.js
Normal file
@ -0,0 +1,73 @@
|
||||
import lf from 'localforage'
|
||||
import {complement, equals, isNil, pipe, prop, identity, allPass} from 'ramda'
|
||||
import {switcherFn} from 'hurdak/lib/hurdak'
|
||||
|
||||
const stores = {}
|
||||
|
||||
const getStore = storeName => {
|
||||
if (!stores[storeName]) {
|
||||
stores[storeName] = lf.createInstance({name: 'coracle', storeName})
|
||||
}
|
||||
|
||||
return stores[storeName]
|
||||
}
|
||||
|
||||
addEventListener('message', async ({data: {topic, payload, channel}}) => {
|
||||
const reply = (topic, payload) => postMessage({channel, topic, payload})
|
||||
|
||||
switcherFn(topic, {
|
||||
'localforage.call': async () => {
|
||||
const {storeName, method, args} = payload
|
||||
const result = await getStore(storeName)[method](...args)
|
||||
|
||||
reply('localforage.return', result)
|
||||
},
|
||||
'localforage.iterate': async () => {
|
||||
const {storeName, where} = payload
|
||||
const matchesFilter = allPass(
|
||||
Object.entries(where)
|
||||
.map(([key, value]) => {
|
||||
let [field, operator = 'eq'] = key.split(':')
|
||||
let test, modifier = identity
|
||||
|
||||
if (operator.startsWith('!')) {
|
||||
operator = operator.slice(1)
|
||||
modifier = complement
|
||||
}
|
||||
|
||||
if (operator === 'eq') {
|
||||
test = equals(value)
|
||||
} if (operator === 'nil') {
|
||||
test = isNil
|
||||
} else {
|
||||
throw new Error(`Invalid operator ${operator}`)
|
||||
}
|
||||
|
||||
return pipe(prop(field), modifier(test))
|
||||
})
|
||||
)
|
||||
|
||||
getStore(storeName).iterate(
|
||||
(v, k, i) => {
|
||||
if (matchesFilter(v)) {
|
||||
reply('localforage.item', {v, k, i})
|
||||
}
|
||||
},
|
||||
() => {
|
||||
reply('localforage.iterationComplete')
|
||||
},
|
||||
)
|
||||
},
|
||||
default: () => {
|
||||
throw new Error(`invalid topic: ${topic}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
addEventListener('error', event => {
|
||||
console.error(event.error)
|
||||
})
|
||||
|
||||
addEventListener('unhandledrejection', event => {
|
||||
console.error(event)
|
||||
})
|
Loading…
Reference in New Issue
Block a user