Fix all major bugs

This commit is contained in:
Jonathan Staab 2022-12-17 15:25:07 -08:00
parent d23742d33e
commit 78a85be7e5
14 changed files with 130 additions and 130 deletions

View File

@ -14,6 +14,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
- [x] Notifications - [x] Notifications
- [x] Link previews - [x] Link previews
- [x] Add notes, follows, likes tab to profile - [x] Add notes, follows, likes tab to profile
- [ ] Add a coracle relay
- [ ] Mentions - render done, now reference in compose - [ ] Mentions - render done, now reference in compose
- [ ] Image uploads - [ ] Image uploads
- [ ] An actual readme - [ ] An actual readme
@ -40,11 +41,9 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
- [ ] Check firefox - in dev it won't work, but it should in production - [ ] Check firefox - in dev it won't work, but it should in production
- [ ] Re-implement muffle - [ ] Re-implement muffle
- [ ] Move relays to db - [ ] Delete old events
- [ ] Make user a livequery instead of a store - [ ] Sync accounts to store to avoid loading jank
- [ ] Figure out if multiple relays congest response times because we wait for all eose - [ ] Sync account updates to user for e.g. muffle settings
- [ ] Set default relay when storage is empty
- [ ] Are connections closed when a relay is removed?
- https://vitejs.dev/guide/features.html#web-workers - https://vitejs.dev/guide/features.html#web-workers
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
- https://web.dev/module-workers/ - https://web.dev/module-workers/

View File

@ -6,7 +6,8 @@ import {filterTags, findReply, findRoot} from 'src/util/nostr'
export const db = new Dexie('coracle/relay') export const db = new Dexie('coracle/relay')
db.version(3).stores({ db.version(4).stores({
relays: '++url, name',
events: '++id, pubkey, created_at, kind, content, reply, root', events: '++id, pubkey, created_at, kind, content, reply, root',
people: '++pubkey, name, about', people: '++pubkey, name, about',
tags: '++key, event, value', tags: '++key, event, value',

View File

@ -38,7 +38,7 @@ const ensureContext = async events => {
if (ids.length > 0) { if (ids.length > 0) {
promises.push( promises.push(
pool.fetchEvents([ pool.loadEvents([
{kinds: [1, 5, 7], '#e': ids}, {kinds: [1, 5, 7], '#e': ids},
{kinds: [1, 5], ids}, {kinds: [1, 5], ids},
]) ])
@ -107,6 +107,11 @@ const scroller = (filter, delta, onChunk) => {
until -= delta until -= delta
await onChunk(await getOrLoadChunk(filter, since, until)) await onChunk(await getOrLoadChunk(filter, since, until))
// Set a hard cutoff at 3 weeks back
if (since < now() - timedelta(21, 'days')) {
unsub()
}
}) })
return unsub return unsub
@ -142,15 +147,13 @@ const findNote = async (id, giveUp = false) => {
// If we don't have it, try to retrieve it // If we don't have it, try to retrieve it
if (!note) { if (!note) {
console.warning(`Failed to find context for note ${id}`) console.warn(`Failed to find context for note ${id}`)
if (giveUp) { if (giveUp) {
return null return null
} }
await ensureContext([ await ensureContext(await pool.loadEvents({ids: [id]}))
await pool.fetchEvents({ids: [id]}),
])
return findNote(id, true) return findNote(id, true)
} }

View File

@ -1,4 +1,5 @@
import {uniqBy, prop} from 'ramda' import {uniqBy, without, prop} from 'ramda'
import {writable} from 'svelte/store'
import {relayPool, getPublicKey} from 'nostr-tools' import {relayPool, getPublicKey} from 'nostr-tools'
import {noop, range} from 'hurdak/lib/hurdak' import {noop, range} from 'hurdak/lib/hurdak'
import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc" import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc"
@ -10,6 +11,16 @@ import {db} from 'src/relay/db'
const pool = relayPool() const pool = relayPool()
const relays = writable([])
const setup = () => {
for (const url of getLocalJson('pool/relays') || []) {
addRelay(url)
}
relays.subscribe($relays => setLocalJson('pool/relays', $relays))
}
class Channel { class Channel {
constructor(name) { constructor(name) {
this.name = name this.name = name
@ -77,10 +88,12 @@ const getPubkey = () => {
const addRelay = url => { const addRelay = url => {
pool.addRelay(url) pool.addRelay(url)
relays.update($r => $r.concat(url))
} }
const removeRelay = url => { const removeRelay = url => {
pool.removeRelay(url) pool.removeRelay(url)
relays.update($r => without([url], $r))
} }
const setPrivateKey = privkey => { const setPrivateKey = privkey => {
@ -127,10 +140,6 @@ const syncPersonInfo = async person => {
return await db.people.where('pubkey').equals(person.pubkey).first() return await db.people.where('pubkey').equals(person.pubkey).first()
} }
const fetchEvents = async filter => {
db.events.process(await req(filter))
}
let syncSub = null let syncSub = null
let syncChan = new Channel('sync') let syncChan = new Channel('sync')
@ -163,7 +172,9 @@ const sync = async person => {
) )
} }
setup()
export default { export default {
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey, getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, loadEvents, syncPersonInfo, fetchEvents, sync, publishEvent, loadEvents, syncPersonInfo, sync, relays,
} }

View File

@ -8,7 +8,7 @@
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import toast from "src/state/toast" import toast from "src/state/toast"
import {dispatch} from "src/state/dispatch" import {dispatch} from "src/state/dispatch"
import {relays, user} from "src/state/app" import {user} from "src/state/app"
let privkey = '' let privkey = ''
let hasExtension = false let hasExtension = false
@ -32,19 +32,11 @@
} }
const logIn = async ({privkey, pubkey}) => { const logIn = async ({privkey, pubkey}) => {
console.log(1)
const person = await dispatch("user/init", pubkey) const person = await dispatch("user/init", pubkey)
console.log(person)
user.set({...person, pubkey, privkey}) user.set({...person, pubkey, privkey})
if ($relays.length === 0) {
navigate('/relays')
} else if (user.name) {
navigate('/notes/global') navigate('/notes/global')
} else {
navigate('/profile')
}
} }
const logInWithExtension = async () => { const logInWithExtension = async () => {

View File

@ -4,11 +4,13 @@
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Tabs from "src/partials/Tabs.svelte" import Tabs from "src/partials/Tabs.svelte"
import Notes from "src/views/Notes.svelte" import Notes from "src/views/Notes.svelte"
import {user, relays} from "src/state/app" import {user} from "src/state/app"
import {timedelta} from 'src/util/misc' import {timedelta} from 'src/util/misc'
import relay from 'src/relay'
export let activeTab export let activeTab
const relays = relay.pool.relays
const authors = $user ? $user.petnames.map(t => t[1]) : [] const authors = $user ? $user.petnames.map(t => t[1]) : []
const setActiveTab = tab => navigate(`/notes/${tab}`) const setActiveTab = tab => navigate(`/notes/${tab}`)
</script> </script>

View File

@ -1,20 +1,20 @@
<script> <script>
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {find, identity, whereEq, reject} from 'ramda'
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 {dispatch} from "src/state/dispatch" import {modal} from "src/state/app"
import {modal, relays, knownRelays} from "src/state/app" import relay from 'src/relay'
let q = "" let q = ""
let search let search
let data
$: data = reject(r => $relays.includes(r.url), $knownRelays || []) const relays = relay.pool.relays
$: search = fuzzy(data, {keys: ["name", "description", "url"]}) const knownRelays = relay.lq(() => relay.db.relays.toArray())
const join = url => dispatch("relay/join", url) $: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
const leave = url => dispatch("relay/leave", url)
const join = url => relay.pool.addRelay(url)
const leave = url => relay.pool.removeRelay(url)
</script> </script>
<div class="flex justify-center py-8 px-4" in:fly={{y: 20}}> <div class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
@ -30,27 +30,29 @@
<i slot="before" class="fa-solid fa-search" /> <i slot="before" class="fa-solid fa-search" />
</Input> </Input>
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6"> <div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
{#each $relays.map(url => find(whereEq({url}), $knownRelays)).filter(identity) as relay} {#each ($knownRelays || []) as r}
{#if $relays.includes(r.url)}
<div class="flex gap-2 justify-between"> <div class="flex gap-2 justify-between">
<div> <div>
<strong>{relay.name || relay.url}</strong> <strong>{r.name || r.url}</strong>
<p class="text-light">{relay.description || ''}</p> <p class="text-light">{r.description || ''}</p>
</div> </div>
<a class="underline cursor-pointer" on:click={() => leave(relay.url)}> <a class="underline cursor-pointer" on:click={() => leave(r.url)}>
Leave Leave
</a> </a>
</div> </div>
{/if}
{/each} {/each}
{#if $relays.length > 0} {#if ($knownRelays || []).length > 0}
<div class="pt-2 mb-2 border-b border-solid border-medium" /> <div class="pt-2 mb-2 border-b border-solid border-medium" />
{/if} {/if}
{#each search(q).slice(0, 10) as relay} {#each (search(q) || []).slice(0, 10) as r}
<div class="flex gap-2 justify-between"> <div class="flex gap-2 justify-between">
<div> <div>
<strong>{relay.name || relay.url}</strong> <strong>{r.name || r.url}</strong>
<p class="text-light">{relay.description || ''}</p> <p class="text-light">{r.description || ''}</p>
</div> </div>
<a class="underline cursor-pointer" on:click={() => join(relay.url)}> <a class="underline cursor-pointer" on:click={() => join(r.url)}>
Join Join
</a> </a>
</div> </div>

View File

@ -1,4 +1,3 @@
import {uniqBy, prop} from 'ramda'
import {writable, get} from 'svelte/store' import {writable, get} from 'svelte/store'
import {navigate} from "svelte-routing" import {navigate} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history" import {globalHistory} from "svelte-routing/src/history"
@ -32,46 +31,6 @@ userLq.subscribe(person => {
user.update($user => $user ? ({...$user, ...person}) : null) user.update($user => $user ? ({...$user, ...person}) : null)
}) })
// Keep track of known relays
export const knownRelays = writable((getLocalJson("coracle/knownRelays") || [
{url: "wss://nostr.zebedee.cloud"},
{url: "wss://nostr-pub.wellorder.net"},
{url: "wss://nostr.rocks"},
{url: "wss://nostr-pub.semisol.dev"},
{url: "wss://nostr.drss.io"},
{url: "wss://relay.damus.io"},
{url: "wss://nostr.openchain.fr"},
{url: "wss://nostr.delo.software"},
{url: "wss://relay.nostr.info"},
{url: "wss://nostr.ono.re"},
{url: "wss://relay.grunch.dev"},
{url: "wss://nostr.sandwich.farm"},
{url: "wss://relay.nostr.ch"},
{url: "wss://nostr-relay.wlvs.space"},
]).filter(x => x.url))
knownRelays.subscribe($knownRelays => {
setLocalJson("coracle/knownRelays", $knownRelays)
})
export const registerRelay = async url => {
let json
try {
const res = await fetch(url.replace(/^ws/, 'http'), {
headers: {
Accept: 'application/nostr_json',
},
})
json = await res.json()
} catch (e) {
json = {}
}
knownRelays.update($xs => uniqBy(prop('url'), $xs.concat({...json, url})))
}
// Keep track of which relays we're subscribed to // Keep track of which relays we're subscribed to
export const relays = writable(getLocalJson("coracle/relays") || []) export const relays = writable(getLocalJson("coracle/relays") || [])
@ -139,3 +98,40 @@ export const alerts = writable({
alerts.subscribe($alerts => { alerts.subscribe($alerts => {
setLocalJson("coracle/alerts", $alerts) setLocalJson("coracle/alerts", $alerts)
}) })
// Relays
const defaultRelays = [
"wss://nostr.zebedee.cloud",
"wss://nostr-pub.wellorder.net",
"wss://relay.damus.io",
"wss://relay.grunch.dev",
"wss://nostr.sandwich.farm",
"wss://relay.nostr.ch",
"wss://nostr-relay.wlvs.space",
]
export const registerRelay = async url => {
const {dufflepudUrl} = get(settings)
let json
try {
const res = await fetch(dufflepudUrl + '/relay/info', {
method: 'POST',
body: JSON.stringify({url}),
headers: {
'Content-Type': 'application/json',
},
})
json = await res.json()
} catch (e) {
json = {}
}
relay.db.relays.put({...json, url})
}
for (const url of defaultRelays) {
registerRelay(url)
}

View File

@ -1,8 +1,6 @@
import {identity, isNil, uniqBy, last, without} from 'ramda' import {identity, isNil, uniqBy, last} from 'ramda'
import {get} from 'svelte/store'
import {first, defmulti} from "hurdak/lib/hurdak" import {first, defmulti} from "hurdak/lib/hurdak"
import relay from 'src/relay' import relay from 'src/relay'
import {relays} from 'src/state/app'
// Commands are processed in two layers: // Commands are processed in two layers:
// - App-oriented commands are created via dispatch // - App-oriented commands are created via dispatch
@ -12,7 +10,10 @@ import {relays} from 'src/state/app'
export const dispatch = defmulti("dispatch", identity) export const dispatch = defmulti("dispatch", identity)
dispatch.addMethod("user/init", (topic, pubkey) => { dispatch.addMethod("user/init", (topic, pubkey) => {
return relay.pool.syncUserInfo({pubkey}) // Hardcode one to get them started
relay.pool.addRelay("wss://nostr.zebedee.cloud")
return relay.pool.syncPersonInfo({pubkey})
}) })
dispatch.addMethod("user/update", async (topic, updates) => { dispatch.addMethod("user/update", async (topic, updates) => {
@ -27,14 +28,6 @@ dispatch.addMethod("user/muffle", async (topic, muffle) => {
await relay.pool.publishEvent(makeEvent(12165, '', muffle)) await relay.pool.publishEvent(makeEvent(12165, '', muffle))
}) })
dispatch.addMethod("relay/join", async (topic, url) => {
relays.update(r => r.concat(url))
})
dispatch.addMethod("relay/leave", (topic, url) => {
relays.update(r => without([url], r))
})
dispatch.addMethod("room/create", async (topic, room) => { dispatch.addMethod("room/create", async (topic, room) => {
const event = makeEvent(40, JSON.stringify(room)) const event = makeEvent(40, JSON.stringify(room))
@ -104,7 +97,7 @@ export const copyTags = (e, newTags = []) => {
} }
export const t = (type, content, marker) => { export const t = (type, content, marker) => {
const tag = [type, content, first(get(relays))] const tag = [type, content, first(Object.keys(relay.pool.relays))]
if (!isNil(marker)) { if (!isNil(marker)) {
tag.push(marker) tag.push(marker)

View File

@ -64,7 +64,7 @@ export const createScroller = loadMore => {
await loadMore() await loadMore()
} }
await sleep(100) await sleep(1000)
if (!done) { if (!done) {
requestAnimationFrame(check) requestAnimationFrame(check)

View File

@ -1,26 +1,35 @@
<script> <script>
import {onDestroy} from 'svelte'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {propEq} from 'ramda' import {uniqBy, identity, prop} from 'ramda'
import {timedelta} from 'src/util/misc'
import Note from "src/views/Note.svelte" import Note from "src/views/Note.svelte"
import {findReply} from 'src/util/nostr' import {findReply} from 'src/util/nostr'
import relay from 'src/relay' import relay from 'src/relay'
export let author export let author
const notes = relay.lq(async () => { const filter = {kinds: [7], authors: [author]}
const reactions = await relay.db.events const delta = timedelta(1, 'days')
.where('pubkey').equals(author).filter(propEq('kind', 7)).toArray()
return Promise.all(reactions.map(r => relay.findNote(findReply(r)))) let notes
onDestroy(relay.scroller(filter, delta, async chunk => {
notes = relay.lq(async () => {
const notes = await Promise.all(chunk.map(r => relay.findNote(findReply(r))))
return uniqBy(prop('id'), notes.filter(identity))
}) })
}))
</script> </script>
{#if $notes} {#if $notes}
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto"> <ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
{#each $notes as n (n.id)} {#each $notes as n (n.id)}
<li><Note interactive noReply note={n} depth={1} /></li> <li><Note note={n} depth={1} /></li>
{:else} {:else}
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li> <li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
{/each} {/each}
</ul> </ul>
{/if} {/if}

View File

@ -12,7 +12,7 @@
const values = { const values = {
// Scale up to integers for each choice we have // Scale up to integers for each choice we have
muffle: switcher(Math.round(getMuffleValue($modal.user) * 3), muffleOptions), muffle: switcher(Math.round(getMuffleValue($modal.person) * 3), muffleOptions),
} }
const save = async e => { const save = async e => {
@ -21,8 +21,8 @@
// Scale back down to a decimal based on string value // Scale back down to a decimal based on string value
const muffleValue = muffleOptions.indexOf(values.muffle) / 3 const muffleValue = muffleOptions.indexOf(values.muffle) / 3
const muffle = $user.muffle const muffle = $user.muffle
.filter(x => x[1] !== $modal.user.pubkey) .filter(x => x[1] !== $modal.person.pubkey)
.concat([t("p", $modal.user.pubkey, muffleValue.toString())]) .concat([t("p", $modal.person.pubkey, muffleValue.toString())])
.filter(x => last(x) !== "1") .filter(x => last(x) !== "1")
dispatch('user/muffle', muffle) dispatch('user/muffle', muffle)

View File

@ -27,7 +27,7 @@
</script> </script>
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto"> <ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
{#each results as e (e.id)} {#each results.slice(0, 50) as e (e.id)}
<li in:fly={{y: 20}}> <li in:fly={{y: 20}}>
<Note interactive note={e} /> <Note interactive note={e} />
</li> </li>

View File

@ -6,37 +6,29 @@
export let q export let q
let results = [] let search
const search = relay.lq(async () => { const people = relay.lq(() => relay.db.people.toArray())
return fuzzy(await relay.db.people.toArray(), {keys: ["name", "about", "pubkey"]})
})
$: { $: search = fuzzy($people || [], {keys: ["name", "about", "pubkey"]})
if ($search) {
Promise.all(
$search(q).map(n => relay.findNote(n.id))
).then(notes => {
results = notes
})
}
}
</script> </script>
{#if search}
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto"> <ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
{#each results as e (e.pubkey)} {#each search(q) as p (p.pubkey)}
{#if e.pubkey !== $user.pubkey} {#if p.pubkey !== $user.pubkey}
<li in:fly={{y: 20}}> <li in:fly={{y: 20}}>
<a href="/people/{e.pubkey}/notes" class="flex gap-4 my-4"> <a href="/people/{p.pubkey}/notes" class="flex gap-4 my-4">
<div <div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white" class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({e.picture})" /> style="background-image: url({p.picture})" />
<div class="flex-grow"> <div class="flex-grow">
<h1 class="text-2xl">{e.name || e.pubkey.slice(0, 8)}</h1> <h1 class="text-2xl">{p.name || p.pubkey.slice(0, 8)}</h1>
<p>{e.about || ''}</p> <p>{p.about || ''}</p>
</div> </div>
</a> </a>
<li> <li>
{/if} {/if}
{/each} {/each}
</ul> </ul>
{/if}