From 7ed121f5609711a439bbb41569b9124692cfe9dc Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Thu, 22 Dec 2022 06:01:43 -0800 Subject: [PATCH] Fix scrolling bug, figure out pattern for listen/load --- README.md | 2 +- src/App.svelte | 6 ++- src/relay/index.js | 72 +++++++++---------------------- src/relay/pool.js | 78 ++++++++++++++++++++-------------- src/routes/Login.svelte | 10 +++-- src/routes/Notes.svelte | 63 ++------------------------- src/util/misc.js | 29 +++++++++---- src/views/Network.svelte | 63 --------------------------- src/views/Note.svelte | 20 +++++++++ src/views/NoteDetail.svelte | 3 +- src/views/notes/Global.svelte | 59 +++++++++++++++++++++++++ src/views/notes/Network.svelte | 63 +++++++++++++++++++++++++++ 12 files changed, 248 insertions(+), 220 deletions(-) delete mode 100644 src/views/Network.svelte create mode 100644 src/views/notes/Global.svelte create mode 100644 src/views/notes/Network.svelte diff --git a/README.md b/README.md index ccabcdca..eadd9c48 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg # Current update - [ ] Re-implement muffle + - Don't store muffled events, when muffle changes delete them - [ ] Delete old events - [ ] Sync account updates to user for e.g. muffle settings -- [ ] Test nos2x - [ ] Make sure login/out, no user usage works - [ ] Add a re-sync/clear cache button - https://vitejs.dev/guide/features.html#web-workers diff --git a/src/App.svelte b/src/App.svelte index d74d2d84..cf80cfd0 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -12,7 +12,7 @@ import {hasParent} from 'src/util/html' import {timedelta} from 'src/util/misc' import {store as toast} from "src/state/toast" - import {modal, alerts} from "src/state/app" + import {modal, alerts, settings} from "src/state/app" import relay, {user, connections} from 'src/relay' import Anchor from 'src/partials/Anchor.svelte' import NoteDetail from "src/views/NoteDetail.svelte" @@ -53,6 +53,7 @@ // Give any animations a moment to finish setTimeout(() => { const $connections = get(connections) + const $settings = get(settings) localStorage.clear() @@ -60,8 +61,9 @@ relay.db.events.clear() relay.db.tags.clear() - // Remember the user's relay selection + // Remember the user's relay selection and settings connections.set($connections) + settings.set($settings) // Do a hard refresh so everything gets totally cleared window.location = '/login' diff --git a/src/relay/index.js b/src/relay/index.js index f6a44297..531f0977 100644 --- a/src/relay/index.js +++ b/src/relay/index.js @@ -8,6 +8,7 @@ import {db} from 'src/relay/db' import pool from 'src/relay/pool' // Livequery appears to swallow errors + const lq = f => liveQuery(async () => { try { return await f() @@ -31,36 +32,7 @@ export const buildNoteContextFilter = async (note, extra = {}) => { return filter } -// Context getters attempt to retrieve from the db and fall back to the network - -const ensurePerson = async ({pubkey}) => { - await pool.syncPersonInfo({...prop(pubkey, get(db.people)), pubkey}) -} - -const ensureContext = async events => { - const promises = [] - const people = uniq(pluck('pubkey', events)).map(objOf('pubkey')) - const ids = events.flatMap(e => filterTags({tag: "e"}, e).concat(e.id)) - - if (people.length > 0) { - for (const p of people.map(ensurePerson)) { - promises.push(p) - } - } - - if (ids.length > 0) { - promises.push( - pool.loadEvents([ - {kinds: [1, 5, 7], '#e': ids}, - {kinds: [1, 5], ids}, - ]) - ) - } - - await Promise.all(promises) -} - -// Utils for qurying dexie +// Utils for querying dexie - these return collections, not arrays const prefilterEvents = filter => { if (filter.ids) { @@ -78,7 +50,7 @@ const prefilterEvents = filter => { return db.events } -// Utils for filtering db +// Utils for filtering db - nothing below should load events from the network const filterEvents = filter => { return prefilterEvents(filter) @@ -110,22 +82,6 @@ const filterReactions = async (id, filter) => { return reactions } -const findReaction = async (id, filter) => - first(await filterReactions(id, filter)) - -const countReactions = async (id, filter) => - (await filterReactions(id, filter)).length - -const getOrLoadNote = async (id, {showEntire = false} = {}) => { - const note = await db.events.get(id) - - if (!note) { - return first(await pool.loadEvents({kinds: [1], ids: [id]})) - } - - return note -} - const findNote = async (id, {showEntire = false} = {}) => { const note = await db.events.get(id) @@ -251,6 +207,19 @@ const unfollow = async pubkey => { db.network.update($network => $network.concat(pubkey)) } +// Methods that wil attempt to load from the database and fall back to the network. +// This is intended only for bootstrapping listeners + +const getOrLoadNote = async (id, {showEntire = false} = {}) => { + const note = await db.events.get(id) + + if (!note) { + return first(await pool.loadEvents({kinds: [1], ids: [id]})) + } + + return note +} + // Initialization db.user.subscribe($user => { @@ -277,14 +246,15 @@ db.connections.subscribe($connections => { } }) +// Export stores on their own for convenience + export const user = db.user export const people = db.people export const network = db.network export const connections = db.connections export default { - db, pool, lq, buildNoteContextFilter, ensurePerson, ensureContext, filterEvents, - filterReactions, getOrLoadNote, - countReactions, findReaction, filterReplies, findNote, annotateChunk, renderNote, - filterAlerts, login, addRelay, removeRelay, follow, unfollow, + db, pool, lq, buildNoteContextFilter, filterEvents, getOrLoadNote, + filterReplies, findNote, annotateChunk, renderNote, filterAlerts, + login, addRelay, removeRelay, follow, unfollow, } diff --git a/src/relay/pool.js b/src/relay/pool.js index 5b182f3c..a9980293 100644 --- a/src/relay/pool.js +++ b/src/relay/pool.js @@ -1,8 +1,7 @@ -import {uniqBy, prop, uniq} from 'ramda' +import {uniqBy, find, propEq, prop, uniq} from 'ramda' import {get} from 'svelte/store' import {relayPool, getPublicKey} from 'nostr-tools' -import {noop, range} from 'hurdak/lib/hurdak' -import {now, timedelta, randomChoice, getLocalJson, setLocalJson} from "src/util/misc" +import {noop, range, sleep} from 'hurdak/lib/hurdak' import {getTagValues, filterTags} from "src/util/nostr" import {db} from 'src/relay/db' @@ -14,9 +13,15 @@ const pool = relayPool() class Channel { constructor(name) { this.name = name - this.p = Promise.resolve() + this.status = 'idle' } - async sub(filter, onEvent, onEose = noop, timeout = 30000) { + claim() { + this.status = 'busy' + } + release() { + this.status = 'idle' + } + sub(filter, onEvent, onEose = noop, opts = {}) { // If we don't have any relays, we'll wait forever for an eose, but // we already know we're done. Use a timeout since callers are // expecting this to be async and we run into errors otherwise. @@ -26,17 +31,6 @@ class Channel { return {unsub: noop} } - // Grab our spot in the queue, save resolve for later - let resolve - let p = this.p - this.p = new Promise(r => { - resolve = r - }) - - // Make sure callers have to wait for the previous sub to be done - // before they can get a new one. - await p - // Start our subscription, wait for only one relay to eose before // calling it done. We were waiting for all before, but that made // the slowest relay a bottleneck @@ -45,22 +39,22 @@ class Channel { const done = () => { sub.unsub() - resolve() + this.release() } // If the relay takes to long, just give up - if (timeout) { - setTimeout(done, 1000) + if (opts.timeout) { + setTimeout(done, opts.timeout) } return {unsub: done} } - all(filter) { + all(filter, opts = {}) { /* eslint no-async-promise-executor: 0 */ return new Promise(async resolve => { const result = [] - const sub = await this.sub( + const sub = this.sub( filter, e => result.push(e), r => { @@ -68,6 +62,7 @@ class Channel { resolve(uniqBy(prop('id'), result)) }, + {timeout: 30000, ...opts}, ) }) } @@ -75,8 +70,25 @@ class Channel { export const channels = range(0, 10).map(i => new Channel(i.toString())) -const req = (...args) => randomChoice(channels).all(...args) -const sub = (...args) => randomChoice(channels).sub(...args) +const getChannel = async () => { + /*eslint no-constant-condition: 0*/ + + // Find a channel that isn't busy, or wait for one to become available + while (true) { + const channel = find(propEq('status', 'idle'), channels) + + if (channel) { + channel.claim() + + return channel + } + + await sleep(300) + } +} + +const req = async (...args) => (await getChannel()).all(...args) +const sub = async (...args) => (await getChannel()).sub(...args) const getPubkey = () => { return pool._pubkey || getPublicKey(pool._privkey) @@ -122,14 +134,12 @@ const loadEvents = async filter => { return events } -const subs = {} - const listenForEvents = async (key, filter, onEvent) => { - if (subs[key]) { - subs[key].unsub() + if (listenForEvents.subs[key]) { + listenForEvents.subs[key].unsub() } - subs[key] = await sub(filter, e => { + listenForEvents.subs[key] = await sub(filter, e => { db.events.process(e) if (onEvent) { @@ -138,8 +148,14 @@ const listenForEvents = async (key, filter, onEvent) => { }) } -const loadPeople = pubkeys => { - return pubkeys.length ? loadEvents({kinds: [0, 3, 12165], authors: pubkeys}) : [] +listenForEvents.subs = {} + +const loadPeople = (pubkeys, opts = {}) => { + if (pubkeys.length === 0) { + return [] + } + + return loadEvents({kinds: [0, 3, 12165], authors: pubkeys}, opts) } const syncNetwork = async () => { @@ -148,7 +164,7 @@ const syncNetwork = async () => { let pubkeys = [] if ($user) { // Get this user's profile to start with - await loadPeople([$user.pubkey]) + await loadPeople([$user.pubkey], {timeout: null}) // Get our refreshed person const people = get(db.people) diff --git a/src/routes/Login.svelte b/src/routes/Login.svelte index d48124cd..9e208e8d 100644 --- a/src/routes/Login.svelte +++ b/src/routes/Login.svelte @@ -7,7 +7,7 @@ import Anchor from "src/partials/Anchor.svelte" import Input from "src/partials/Input.svelte" import toast from "src/state/toast" - import relay from 'src/relay' + import relay, {connections} from 'src/relay' let privkey = '' let hasExtension = false @@ -30,10 +30,14 @@ toast.show("info", "Your private key has been re-generated.") } - const logIn = ({privkey, pubkey}) => { + const logIn = async ({privkey, pubkey}) => { relay.login({privkey, pubkey}) - navigate('/relays') + if ($connections.length === 0) { + navigate('/relays') + } else { + navigate('/notes/network') + } } const logInWithExtension = async () => { diff --git a/src/routes/Notes.svelte b/src/routes/Notes.svelte index e8d19215..b098ed00 100644 --- a/src/routes/Notes.svelte +++ b/src/routes/Notes.svelte @@ -4,69 +4,14 @@ import {findReply} from 'src/util/nostr' import Anchor from "src/partials/Anchor.svelte" import Tabs from "src/partials/Tabs.svelte" - import Notes from "src/views/Notes.svelte" + import Network from "src/views/notes/Network.svelte" + import Global from "src/views/notes/Global.svelte" import {now, timedelta} from 'src/util/misc' - import relay, {network, connections} from 'src/relay' + import relay, {connections} from 'src/relay' export let activeTab - let sub - let delta = timedelta(1, 'minutes') - let since = now() - delta - - onMount(async () => { - sub = await subscribe(now()) - }) - - onDestroy(() => { - if (sub) { - sub.unsub() - } - }) - const setActiveTab = tab => navigate(`/notes/${tab}`) - - const subscribe = until => - relay.pool.listenForEvents( - 'routes/Notes', - [{kinds: [1, 5, 7], since, until}], - async e => { - if (e.kind === 1) { - const filter = await relay.buildNoteContextFilter(e, {since}) - - await relay.pool.loadEvents(filter) - } - - if (e.kind === 7) { - const replyId = findReply(e) - - if (replyId && !await relay.db.events.get(replyId)) { - await relay.pool.loadEvents({kinds: [1], ids: [replyId]}) - } - - } - } - ) - - const loadNetworkNotes = async limit => { - const filter = {kinds: [1], authors: $network} - const notes = await relay.filterEvents(filter).reverse().sortBy('created_at') - - return relay.annotateChunk(notes.slice(0, limit)) - } - - const loadGlobalNotes = async limit => { - const filter = {kinds: [1], since} - const notes = await relay.filterEvents(filter).reverse().sortBy('created_at') - - if (notes.length < limit) { - since -= delta - - sub = await subscribe(since + delta) - } - - return relay.annotateChunk(notes.slice(0, limit)) - } {#if $connections.length === 0} @@ -82,7 +27,7 @@ {#if activeTab === 'network'} {:else} - + {/if}
{ export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) export const createScroller = loadMore => { - /* eslint no-constant-condition: 0 */ - let done = false - + let didLoad = false const check = async () => { // While we have empty space, fill it const {scrollY, innerHeight} = window const {scrollHeight} = document.body + const shouldLoad = scrollY + innerHeight + 300 > scrollHeight - if (scrollY + innerHeight + 2000 > scrollHeight) { + // Only trigger loading the first time we reach the threshhold + if (shouldLoad && !didLoad) { await loadMore() } - // This is a gross hack, basically, keep loading if the user doesn't scroll again, - // but wait a long time because otherwise we'll send off multiple concurrent requests - // that will clog up our channels and stall the app. - await sleep(30000) + didLoad = shouldLoad + + // No need to check all that often + await sleep(300) if (!done) { requestAnimationFrame(check) @@ -91,3 +91,16 @@ export const getLastSync = (k, fallback) => { return lastSync } + +export class Cursor { + constructor(since, delta) { + this.since = since || now() - delta, + this.delta = delta + } + step() { + const until = this.since + this.since -= this.delta + + return [this.since, until] + } +} diff --git a/src/views/Network.svelte b/src/views/Network.svelte deleted file mode 100644 index d75e969e..00000000 --- a/src/views/Network.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - diff --git a/src/views/Note.svelte b/src/views/Note.svelte index 938c24d6..1aae24da 100644 --- a/src/views/Note.svelte +++ b/src/views/Note.svelte @@ -50,10 +50,30 @@ } else { navigate('/login') } + + if (content === '+') { + like = true + likes += 1 + } + + if (content === '-') { + flag = true + flags += 1 + } } const deleteReaction = e => { dispatch('event/delete', [e.id]) + + if (e.content === '+') { + like = false + likes -= 1 + } + + if (e.content === '-') { + flag = false + flags -= 1 + } } const startReply = () => { diff --git a/src/views/NoteDetail.svelte b/src/views/NoteDetail.svelte index 88a2aa5d..9619bf72 100644 --- a/src/views/NoteDetail.svelte +++ b/src/views/NoteDetail.svelte @@ -8,7 +8,6 @@ export let note let observable, sub - let since = getLastSync(['NoteDetail', note.id]) onMount(async () => { note = await relay.getOrLoadNote(note.id) @@ -16,7 +15,7 @@ if (note) { sub = await relay.pool.listenForEvents( 'routes/NoteDetail', - await relay.buildNoteContextFilter(note, {since}) + await relay.buildNoteContextFilter(note) ) } }) diff --git a/src/views/notes/Global.svelte b/src/views/notes/Global.svelte new file mode 100644 index 00000000..04dcf05b --- /dev/null +++ b/src/views/notes/Global.svelte @@ -0,0 +1,59 @@ + + + diff --git a/src/views/notes/Network.svelte b/src/views/notes/Network.svelte new file mode 100644 index 00000000..691343d5 --- /dev/null +++ b/src/views/notes/Network.svelte @@ -0,0 +1,63 @@ + + +