Working on scrolling stuff

This commit is contained in:
Jonathan Staab 2022-12-17 12:30:18 -08:00
parent eaf2e45e46
commit 447c112d21
9 changed files with 101 additions and 78 deletions

View File

@ -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). - [ ] 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. - 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 - 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 # 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 - [ ] Make user a livequery instead of a store
- [ ] Figure out if multiple relays congest response times because we wait for all eose - [ ] Figure out if multiple relays congest response times because we wait for all eose
- [ ] Set default relay when storage is empty - [ ] 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

@ -1,6 +1,7 @@
import Dexie from 'dexie' import Dexie from 'dexie'
import {groupBy, prop, flatten, pick} from 'ramda' import {groupBy, prop, flatten, pick} from 'ramda'
import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak' import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {now} from 'src/util/misc'
import {filterTags, findReply, findRoot} from 'src/util/nostr' import {filterTags, findReply, findRoot} from 'src/util/nostr'
export const db = new Dexie('coracle/relay') export const db = new Dexie('coracle/relay')
@ -59,11 +60,12 @@ db.events.process = async events => {
for (const event of profileUpdates) { for (const event of profileUpdates) {
const {pubkey, kind, content, tags} = event const {pubkey, kind, content, tags} = event
const user = await db.users.where('pubkey').equals(pubkey).first() 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, { await switcherFn(kind, {
0: () => db.users.put({...user, ...JSON.parse(content), pubkey}), 0: () => putUser(JSON.parse(content)),
3: () => db.users.put({...user, petnames: tags, pubkey}), 3: () => putUser({petnames: tags}),
12165: () => db.users.put({...user, muffle: tags, pubkey}), 12165: () => putUser({muffle: tags}),
default: () => { default: () => {
console.log(`Received unsupported event type ${event.kind}`) console.log(`Received unsupported event type ${event.kind}`)
}, },

View File

@ -1,5 +1,5 @@
import {liveQuery} from 'dexie' 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 {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak'
import {now, timedelta} from 'src/util/misc' import {now, timedelta} from 'src/util/misc'
import {escapeHtml} from 'src/util/html' import {escapeHtml} from 'src/util/html'
@ -16,16 +16,26 @@ const lq = f => liveQuery(async () => {
} }
}) })
const ensureContext = async e => { const ensurePerson = async ({pubkey}) => {
const user = await db.users.where('pubkey').equals(e.pubkey).first() const user = await db.users.where('pubkey').equals(pubkey).first()
// Throttle updates for users // Throttle updates for users
if (!user || user.updated_at < now() - timedelta(1, 'hours')) { 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 const ensureContext = async events => {
await pool.fetchContext(e) 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 => { const prefilterEvents = filter => {
@ -50,6 +60,8 @@ const filterEvents = filter => {
if (filter.ids && !filter.ids.includes(e.id)) return false if (filter.ids && !filter.ids.includes(e.id)) return false
if (filter.authors && !filter.authors.includes(e.pubkey)) return false if (filter.authors && !filter.authors.includes(e.pubkey)) return false
if (filter.kinds && !filter.kinds.includes(e.kind)) 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 if (!isNil(filter.content) && filter.content !== e.content) return false
return true return true
@ -78,12 +90,27 @@ const findReaction = async (id, filter) =>
const countReactions = async (id, filter) => const countReactions = async (id, filter) =>
(await filterReactions(id, filter)).length (await filterReactions(id, filter)).length
const findNote = async id => { const findNote = async (id, giveUp = false) => {
const [note, children] = await Promise.all([ const [note, children] = await Promise.all([
db.events.get(id), db.events.get(id),
db.events.where('reply').equals(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([ const [replies, reactions, user, html] = await Promise.all([
children.clone().filter(e => e.kind === 1).toArray(), children.clone().filter(e => e.kind === 1).toArray(),
children.clone().filter(e => e.kind === 7).toArray(), children.clone().filter(e => e.kind === 7).toArray(),
@ -130,6 +157,6 @@ const filterAlerts = async (user, filter) => {
} }
export default { export default {
db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions, db, pool, lq, ensurePerson, ensureContext, filterEvents, filterReactions,
findReaction, filterReplies, findNote, renderNote, filterAlerts, countReactions, findReaction, filterReplies, findNote, renderNote, filterAlerts,
} }

View File

@ -1,8 +1,8 @@
import {uniqBy, prop} from 'ramda' import {uniqBy, prop} from 'ramda'
import {relayPool, getPublicKey} from 'nostr-tools' 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 {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' import {db} from 'src/relay/db'
// ============================================================================ // ============================================================================
@ -73,11 +73,7 @@ class Channel {
} }
} }
export const channels = [ export const channels = range(0, 10).map(i => new Channel(i.toString()))
new Channel('a'),
new Channel('b'),
new Channel('c'),
]
const req = filter => randomChoice(channels).all(filter) const req = filter => randomChoice(channels).all(filter)
@ -117,7 +113,9 @@ const publishEvent = event => {
const loadEvents = async filter => { const loadEvents = async filter => {
const events = await req(filter) const events = await req(filter)
db.events.process(events) await db.events.process(events)
return events
} }
const syncUserInfo = async user => { const syncUserInfo = async user => {
@ -137,16 +135,12 @@ const syncUserInfo = async user => {
return person return person
} }
const fetchContext = async event => { const fetchEvents = async filter => {
const events = await req([ db.events.process(await req(filter))
{kinds: [5, 7], '#e': [event.id]},
{kinds: [5], 'ids': filterTags({tag: "e"}, event)},
])
db.events.process(events)
} }
let syncSub = null let syncSub = null
let syncChan = new Channel('sync')
const sync = async user => { const sync = async user => {
if (syncSub) { if (syncSub) {
@ -155,7 +149,10 @@ const sync = async user => {
if (!user) return if (!user) return
// Get user info right away
const {petnames, pubkey} = await syncUserInfo(user) const {petnames, pubkey} = await syncUserInfo(user)
// Don't grab nothing, but don't grab everything either
const since = Math.max( const since = Math.max(
now() - timedelta(3, 'days'), now() - timedelta(3, 'days'),
Math.min( Math.min(
@ -167,7 +164,7 @@ const sync = async user => {
setLocalJson('pool/lastSync', now()) setLocalJson('pool/lastSync', now())
// Populate recent activity in network so the user has something to look at right away // 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, authors: getTagValues(petnames).concat(pubkey)},
{since, '#p': [pubkey]}], {since, '#p': [pubkey]}],
db.events.process db.events.process
@ -176,5 +173,5 @@ const sync = async user => {
export default { export default {
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey, getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, loadEvents, syncUserInfo, fetchContext, sync, publishEvent, loadEvents, syncUserInfo, fetchEvents, sync,
} }

View File

@ -2,6 +2,7 @@
import {find} from 'ramda' import {find} from 'ramda'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import {timedelta} from 'src/util/misc'
import Tabs from "src/partials/Tabs.svelte" import Tabs from "src/partials/Tabs.svelte"
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import Notes from "src/views/Notes.svelte" import Notes from "src/views/Notes.svelte"
@ -13,6 +14,8 @@
export let pubkey export let pubkey
export let activeTab export let activeTab
relay.ensurePerson({pubkey})
const user = relay.lq(() => relay.db.users.get(pubkey)) const user = relay.lq(() => relay.db.users.get(pubkey))
let following = $currentUser && find(t => t[1] === pubkey, $currentUser.petnames) let following = $currentUser && find(t => t[1] === pubkey, $currentUser.petnames)
@ -78,7 +81,7 @@
<Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} /> <Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'} {#if activeTab === 'notes'}
<Notes filter={{kinds: [1], authors: [pubkey]}} /> <Notes showParent delta={timedelta(1, 'days')} filter={{kinds: [1], authors: [pubkey]}} />
{:else if activeTab === 'likes'} {:else if activeTab === 'likes'}
<Likes author={pubkey} /> <Likes author={pubkey} />
{:else if activeTab === 'network'} {:else if activeTab === 'network'}

View File

@ -1,5 +1,4 @@
import {pluck} from "ramda" import {pluck} from "ramda"
import {debounce} from 'throttle-debounce'
import Fuse from "fuse.js/dist/fuse.min.js" import Fuse from "fuse.js/dist/fuse.min.js"
export const fuzzy = (data, opts = {}) => { 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 sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
export const createScroller = loadMore => { export const createScroller = loadMore => {
const onScroll = debounce(1000, async () => { /* eslint no-constant-condition: 0 */
/* eslint no-constant-condition: 0 */
while (true) { let done = false
// While we have empty space, fill it
const {scrollY, innerHeight} = window
const {scrollHeight} = document.body
if (scrollY + innerHeight + 600 < scrollHeight) { const check = async () => {
break // While we have empty space, fill it
} const {scrollY, innerHeight} = window
const {scrollHeight} = document.body
loadMore() if (scrollY + innerHeight + 600 > scrollHeight) {
await loadMore()
await sleep(1000)
} }
})
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)] export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)]

View File

@ -7,7 +7,6 @@
import {findReply} from "src/util/nostr" import {findReply} from "src/util/nostr"
import Preview from 'src/partials/Preview.svelte' import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import relay from 'src/relay'
import {dispatch} from "src/state/dispatch" import {dispatch} from "src/state/dispatch"
import {settings, user, modal} from "src/state/app" import {settings, user, modal} from "src/state/app"
import {formatTimestamp} from 'src/util/misc' import {formatTimestamp} from 'src/util/misc'
@ -34,8 +33,6 @@
flag = find(whereEq({pubkey: $user?.pubkey}), flags) flag = find(whereEq({pubkey: $user?.pubkey}), flags)
} }
relay.ensureContext(note)
const onClick = e => { const onClick = e => {
if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) { if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) {
modal.set({note}) modal.set({note})
@ -112,14 +109,16 @@
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor> <Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
</p> </p>
{:else} {:else}
<p class="text-ellipsis overflow-hidden"> <div class="text-ellipsis overflow-hidden flex flex-col gap-2">
{@html note.html} <p>{@html note.html}</p>
{#if link} {#if link}
<div class="mt-2" on:click={e => e.stopPropagation()}> <div>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} /> <div class="inline-block" on:click={e => e.stopPropagation()}>
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
</div>
</div> </div>
{/if} {/if}
</p> </div>
<div class="flex gap-6 text-light"> <div class="flex gap-6 text-light">
<div> <div>
<i <i

View File

@ -4,7 +4,7 @@
export let note export let note
const observable = relay.lq(() => relay.findNote(note, {showEntire: true})) const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true}))
</script> </script>
{#if $observable} {#if $observable}

View File

@ -1,35 +1,35 @@
<script> <script>
import {onDestroy} from 'svelte'
import {prop, identity, concat, uniqBy, groupBy} from 'ramda' import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
import {createMap} from 'hurdak/lib/hurdak' import {createMap} from 'hurdak/lib/hurdak'
import {now, timedelta} from 'src/util/misc' import {now, timedelta} from 'src/util/misc'
import {findReply, findRoot} from 'src/util/nostr' import {findReply, findRoot} from 'src/util/nostr'
import {fly} from 'svelte/transition'
import {createScroller} from 'src/util/misc' import {createScroller} from 'src/util/misc'
import Spinner from 'src/partials/Spinner.svelte' import Spinner from 'src/partials/Spinner.svelte'
import Note from "src/views/Note.svelte" import Note from "src/views/Note.svelte"
import relay from 'src/relay' import relay from 'src/relay'
export let filter export let filter
export let showParent = false
export let shouldMuffle = false export let shouldMuffle = false
export let delta = timedelta(10, 'minutes')
let limit = 10, init = now(), offset = 0, notes let since = now() - delta, until = now(), notes
const onScroll = createScroller(async () => { const done = createScroller(async () => {
limit += 10 since -= delta
offset += 1 until -= delta
const delta = timedelta(1, 'minutes') await relay.ensureContext(
const since = init - delta * offset await relay.pool.loadEvents({...filter, since, until})
const until = init - delta * (offset - 1) )
await relay.pool.loadEvents({...filter, since, until})
createNotesObservable() createNotesObservable()
}) })
const createNotesObservable = () => { const createNotesObservable = () => {
notes = relay.lq(async () => { notes = relay.lq(async () => {
const notes = await relay.filterEvents(filter).limit(limit).reverse().sortBy('created_at') const notes = await relay.filterEvents({...filter, since}).reverse().sortBy('created_at')
const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity) const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity)
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray() const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()
@ -58,23 +58,13 @@
}) })
} }
createNotesObservable() onDestroy(done)
</script> </script>
<svelte:window on:scroll={onScroll} />
<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 note={n} depth={2} /></li> <li><Note interactive note={n} depth={2} {showParent} /></li>
{/each} {/each}
</ul> </ul>
{#if $notes?.length === 0}
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
No notes found.
</div>
</div>
{:else}
<Spinner /> <Spinner />
{/if}