From 447c112d21198dcc26c74368576b416e2556e906 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Sat, 17 Dec 2022 12:30:18 -0800 Subject: [PATCH] Working on scrolling stuff --- README.md | 2 ++ src/relay/db.js | 8 ++++--- src/relay/index.js | 45 ++++++++++++++++++++++++++++-------- src/relay/pool.js | 31 +++++++++++-------------- src/routes/UserDetail.svelte | 5 +++- src/util/misc.js | 35 +++++++++++++++------------- src/views/Note.svelte | 15 ++++++------ src/views/NoteDetail.svelte | 2 +- src/views/Notes.svelte | 36 +++++++++++------------------ 9 files changed, 101 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index a28e7c47..72abd863 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg - [ ] Optimistically load events the user publishes (e.g. to reduce reflow for reactions/replies). - Essentially, we can pretend to be our own in-memory relay. - This allows us to keep a copy of all user data, and possibly user likes/reply parents +- [ ] Support invoices https://twitter.com/jb55/status/1604131336247476224 # Bugs @@ -44,6 +45,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg - [ ] Make user a livequery instead of a store - [ ] Figure out if multiple relays congest response times because we wait for all eose - [ ] Set default relay when storage is empty +- [ ] Are connections closed when a relay is removed? - https://vitejs.dev/guide/features.html#web-workers - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers - https://web.dev/module-workers/ diff --git a/src/relay/db.js b/src/relay/db.js index 9398346d..75a810c6 100644 --- a/src/relay/db.js +++ b/src/relay/db.js @@ -1,6 +1,7 @@ import Dexie from 'dexie' import {groupBy, prop, flatten, pick} from 'ramda' import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak' +import {now} from 'src/util/misc' import {filterTags, findReply, findRoot} from 'src/util/nostr' export const db = new Dexie('coracle/relay') @@ -59,11 +60,12 @@ db.events.process = async events => { for (const event of profileUpdates) { const {pubkey, kind, content, tags} = event const user = await db.users.where('pubkey').equals(pubkey).first() + const putUser = data => db.users.put({...user, ...data, pubkey, updated_at: now()}) await switcherFn(kind, { - 0: () => db.users.put({...user, ...JSON.parse(content), pubkey}), - 3: () => db.users.put({...user, petnames: tags, pubkey}), - 12165: () => db.users.put({...user, muffle: tags, pubkey}), + 0: () => putUser(JSON.parse(content)), + 3: () => putUser({petnames: tags}), + 12165: () => putUser({muffle: tags}), default: () => { console.log(`Received unsupported event type ${event.kind}`) }, diff --git a/src/relay/index.js b/src/relay/index.js index bf4b01be..29e8d0f4 100644 --- a/src/relay/index.js +++ b/src/relay/index.js @@ -1,5 +1,5 @@ import {liveQuery} from 'dexie' -import {pluck, isNil} from 'ramda' +import {pluck, uniq, objOf, isNil} from 'ramda' import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak' import {now, timedelta} from 'src/util/misc' import {escapeHtml} from 'src/util/html' @@ -16,16 +16,26 @@ const lq = f => liveQuery(async () => { } }) -const ensureContext = async e => { - const user = await db.users.where('pubkey').equals(e.pubkey).first() +const ensurePerson = async ({pubkey}) => { + const user = await db.users.where('pubkey').equals(pubkey).first() // Throttle updates for users if (!user || user.updated_at < now() - timedelta(1, 'hours')) { - await pool.syncUserInfo({pubkey: e.pubkey, ...user}) + await pool.syncUserInfo({pubkey, ...user}) } +} - // TODO optimize this like user above so we're not double-fetching - await pool.fetchContext(e) +const ensureContext = async events => { + const ids = events.flatMap(e => filterTags({tag: "e"}, e).concat(e.id)) + const people = uniq(pluck('pubkey', events)).map(objOf('pubkey')) + + await Promise.all([ + people.map(ensurePerson), + pool.fetchEvents([ + {kinds: [1, 5, 7], '#e': ids}, + {kinds: [1, 5], ids}, + ]), + ]) } const prefilterEvents = filter => { @@ -50,6 +60,8 @@ const filterEvents = filter => { if (filter.ids && !filter.ids.includes(e.id)) return false if (filter.authors && !filter.authors.includes(e.pubkey)) return false if (filter.kinds && !filter.kinds.includes(e.kind)) return false + if (filter.since && filter.since > e.created_at) return false + if (filter.until && filter.until < e.created_at) return false if (!isNil(filter.content) && filter.content !== e.content) return false return true @@ -78,12 +90,27 @@ const findReaction = async (id, filter) => const countReactions = async (id, filter) => (await filterReactions(id, filter)).length -const findNote = async id => { +const findNote = async (id, giveUp = false) => { const [note, children] = await Promise.all([ db.events.get(id), db.events.where('reply').equals(id), ]) + // If we don't have it, try to retrieve it + if (!note) { + console.warning(`Failed to find context for note ${id}`) + + if (giveUp) { + return null + } + + await ensureContext([ + await pool.fetchEvents({ids: [id]}), + ]) + + return findNote(id, true) + } + const [replies, reactions, user, html] = await Promise.all([ children.clone().filter(e => e.kind === 1).toArray(), children.clone().filter(e => e.kind === 7).toArray(), @@ -130,6 +157,6 @@ const filterAlerts = async (user, filter) => { } export default { - db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions, - findReaction, filterReplies, findNote, renderNote, filterAlerts, + db, pool, lq, ensurePerson, ensureContext, filterEvents, filterReactions, + countReactions, findReaction, filterReplies, findNote, renderNote, filterAlerts, } diff --git a/src/relay/pool.js b/src/relay/pool.js index 21d3f9c9..762d0d8c 100644 --- a/src/relay/pool.js +++ b/src/relay/pool.js @@ -1,8 +1,8 @@ import {uniqBy, prop} from 'ramda' import {relayPool, getPublicKey} from 'nostr-tools' -import {noop} from 'hurdak/lib/hurdak' +import {noop, range} from 'hurdak/lib/hurdak' import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc" -import {filterTags, getTagValues} from "src/util/nostr" +import {getTagValues} from "src/util/nostr" import {db} from 'src/relay/db' // ============================================================================ @@ -73,11 +73,7 @@ class Channel { } } -export const channels = [ - new Channel('a'), - new Channel('b'), - new Channel('c'), -] +export const channels = range(0, 10).map(i => new Channel(i.toString())) const req = filter => randomChoice(channels).all(filter) @@ -117,7 +113,9 @@ const publishEvent = event => { const loadEvents = async filter => { const events = await req(filter) - db.events.process(events) + await db.events.process(events) + + return events } const syncUserInfo = async user => { @@ -137,16 +135,12 @@ const syncUserInfo = async user => { return person } -const fetchContext = async event => { - const events = await req([ - {kinds: [5, 7], '#e': [event.id]}, - {kinds: [5], 'ids': filterTags({tag: "e"}, event)}, - ]) - - db.events.process(events) +const fetchEvents = async filter => { + db.events.process(await req(filter)) } let syncSub = null +let syncChan = new Channel('sync') const sync = async user => { if (syncSub) { @@ -155,7 +149,10 @@ const sync = async user => { if (!user) return + // Get user info right away const {petnames, pubkey} = await syncUserInfo(user) + + // Don't grab nothing, but don't grab everything either const since = Math.max( now() - timedelta(3, 'days'), Math.min( @@ -167,7 +164,7 @@ const sync = async user => { setLocalJson('pool/lastSync', now()) // Populate recent activity in network so the user has something to look at right away - syncSub = randomChoice(channels).sub( + syncSub = syncChan.sub( [{since, authors: getTagValues(petnames).concat(pubkey)}, {since, '#p': [pubkey]}], db.events.process @@ -176,5 +173,5 @@ const sync = async user => { export default { getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey, - publishEvent, loadEvents, syncUserInfo, fetchContext, sync, + publishEvent, loadEvents, syncUserInfo, fetchEvents, sync, } diff --git a/src/routes/UserDetail.svelte b/src/routes/UserDetail.svelte index 35037f85..12055b74 100644 --- a/src/routes/UserDetail.svelte +++ b/src/routes/UserDetail.svelte @@ -2,6 +2,7 @@ import {find} from 'ramda' import {fly} from 'svelte/transition' import {navigate} from 'svelte-routing' + import {timedelta} from 'src/util/misc' import Tabs from "src/partials/Tabs.svelte" import Button from "src/partials/Button.svelte" import Notes from "src/views/Notes.svelte" @@ -13,6 +14,8 @@ export let pubkey export let activeTab + relay.ensurePerson({pubkey}) + const user = relay.lq(() => relay.db.users.get(pubkey)) let following = $currentUser && find(t => t[1] === pubkey, $currentUser.petnames) @@ -78,7 +81,7 @@ {#if activeTab === 'notes'} - + {:else if activeTab === 'likes'} {:else if activeTab === 'network'} diff --git a/src/util/misc.js b/src/util/misc.js index 6cbaf290..d0da13ac 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,5 +1,4 @@ import {pluck} from "ramda" -import {debounce} from 'throttle-debounce' import Fuse from "fuse.js/dist/fuse.min.js" export const fuzzy = (data, opts = {}) => { @@ -52,27 +51,31 @@ export const formatTimestamp = ts => { export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) export const createScroller = loadMore => { - const onScroll = debounce(1000, async () => { - /* eslint no-constant-condition: 0 */ + /* eslint no-constant-condition: 0 */ - while (true) { - // While we have empty space, fill it - const {scrollY, innerHeight} = window - const {scrollHeight} = document.body + let done = false - if (scrollY + innerHeight + 600 < scrollHeight) { - break - } + const check = async () => { + // While we have empty space, fill it + const {scrollY, innerHeight} = window + const {scrollHeight} = document.body - loadMore() - - await sleep(1000) + if (scrollY + innerHeight + 600 > scrollHeight) { + await loadMore() } - }) - onScroll() + await sleep(300) - return onScroll + if (!done) { + requestAnimationFrame(check) + } + } + + requestAnimationFrame(check) + + return () => { + done = true + } } export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)] diff --git a/src/views/Note.svelte b/src/views/Note.svelte index e094c93a..396a4aeb 100644 --- a/src/views/Note.svelte +++ b/src/views/Note.svelte @@ -7,7 +7,6 @@ import {findReply} from "src/util/nostr" import Preview from 'src/partials/Preview.svelte' import Anchor from 'src/partials/Anchor.svelte' - import relay from 'src/relay' import {dispatch} from "src/state/dispatch" import {settings, user, modal} from "src/state/app" import {formatTimestamp} from 'src/util/misc' @@ -34,8 +33,6 @@ flag = find(whereEq({pubkey: $user?.pubkey}), flags) } - relay.ensureContext(note) - const onClick = e => { if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) { modal.set({note}) @@ -112,14 +109,16 @@ deleteReaction(flag)}>Unflag

{:else} -

- {@html note.html} +

+

{@html note.html}

{#if link} -
e.stopPropagation()}> - +
+
e.stopPropagation()}> + +
{/if} -

+
relay.findNote(note, {showEntire: true})) + const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true})) {#if $observable} diff --git a/src/views/Notes.svelte b/src/views/Notes.svelte index 8f7e0891..0eadc226 100644 --- a/src/views/Notes.svelte +++ b/src/views/Notes.svelte @@ -1,35 +1,35 @@ - -
    {#each ($notes || []) as n (n.id)} -
  • +
  • {/each}
-{#if $notes?.length === 0} -
-
- No notes found. -
-
-{:else} -{/if}