mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Split cursor and scroller up
This commit is contained in:
parent
ee04e2987a
commit
995904a2b2
@ -1,5 +1,6 @@
|
||||
Bugs
|
||||
|
||||
- [ ] Use cursor for chat rooms
|
||||
- [ ] Load/publish user preferred relays
|
||||
- [ ] 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.
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {channels, findReplyTo} from "src/state/nostr"
|
||||
import {accounts, modal, annotateNotesChunk} from "src/state/app"
|
||||
import {accounts, modal, annotateNotes} from "src/state/app"
|
||||
import {user} from "src/state/user"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
const showParent = async () => {
|
||||
const notes = await annotateNotesChunk(
|
||||
const notes = await annotateNotes(
|
||||
await channels.getter.all({kinds: [1, 5, 7], ids: [parentId]})
|
||||
)
|
||||
|
||||
|
@ -2,38 +2,30 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {find, propEq} from 'ramda'
|
||||
import {notesLoader, notesListener, modal} from "src/state/app"
|
||||
import {Cursor} from "src/state/nostr"
|
||||
import {notesListener, modal} from "src/state/app"
|
||||
import {user} from "src/state/user"
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
|
||||
export let note
|
||||
|
||||
const notes = writable([note])
|
||||
let loader
|
||||
let cursor
|
||||
let listener
|
||||
|
||||
onMount(() => {
|
||||
const opts = {isInModal: true}
|
||||
|
||||
if (note.created_at) {
|
||||
opts.since = note.created_at
|
||||
}
|
||||
cursor = new Cursor({ids: [note.id]}, note.created_at)
|
||||
|
||||
// Can't use async/await since we need to return unsubscribe functions
|
||||
Promise.all([
|
||||
notesLoader(notes, {ids: [note.id]}, opts),
|
||||
notesListener(notes, [
|
||||
{kinds: [1, 5, 7], '#e': [note.id]},
|
||||
// We can't target reaction deletes by e tag, so get them
|
||||
// all so we can support toggling like/flags for our user
|
||||
{kinds: [5], authors: $user ? [$user.pubkey] : []}
|
||||
]),
|
||||
]).then(([_loader, _listener]) => {
|
||||
loader = _loader
|
||||
notesListener(notes, [
|
||||
{kinds: [1, 5, 7], '#e': [note.id]},
|
||||
// We can't target reaction deletes by e tag, so get them
|
||||
// all so we can support toggling like/flags for our user
|
||||
{kinds: [5], authors: $user ? [$user.pubkey] : []}
|
||||
]).then(_listener => {
|
||||
listener = _listener
|
||||
|
||||
// Populate our initial empty space
|
||||
loader.onScroll()
|
||||
listener.start()
|
||||
})
|
||||
|
||||
@ -43,7 +35,7 @@
|
||||
|
||||
// Unsubscribe when modal closes so that others can re-subscribe sooner
|
||||
const unsubModal = modal.subscribe($modal => {
|
||||
loader?.stop()
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
})
|
||||
|
||||
@ -54,8 +46,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={loader?.onScroll} />
|
||||
|
||||
{#if note.pubkey}
|
||||
<Note showEntire note={note} />
|
||||
{#each note.replies as r (r.id)}
|
||||
|
@ -2,45 +2,52 @@
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {navigate} from "svelte-routing"
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {relays} from "src/state/nostr"
|
||||
import {notesLoader, notesListener, modal} from "src/state/app"
|
||||
import {relays, Cursor} from "src/state/nostr"
|
||||
import {scroller, annotateNotes, notesListener, modal} from "src/state/app"
|
||||
|
||||
const notes = writable([])
|
||||
let loader
|
||||
let cursor
|
||||
let listener
|
||||
let scroll
|
||||
|
||||
const createNote = () => {
|
||||
navigate("/notes/new")
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
loader = await notesLoader(notes, {kinds: [1]}, {showParents: true})
|
||||
cursor = new Cursor({kinds: [1]})
|
||||
listener = await notesListener(notes, {kinds: [1, 5, 7]})
|
||||
scroll = scroller(cursor, async chunk => {
|
||||
const annotated = await annotateNotes(chunk, {showParents: true})
|
||||
|
||||
notes.update($notes => uniqBy(prop('id'), $notes.concat(annotated)))
|
||||
})
|
||||
|
||||
// Populate our initial empty space
|
||||
loader.onScroll()
|
||||
scroll()
|
||||
|
||||
// When a modal opens, suspend our subscriptions
|
||||
modal.subscribe(async $modal => {
|
||||
if ($modal) {
|
||||
loader.stop()
|
||||
cursor.stop()
|
||||
listener.stop()
|
||||
} else {
|
||||
loader.start()
|
||||
cursor.start()
|
||||
listener.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
loader?.stop()
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={loader?.onScroll} />
|
||||
<svelte:window on:scroll={scroll} />
|
||||
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each (notes ? $notes : []) as n (n.id)}
|
||||
|
@ -1,46 +1,54 @@
|
||||
<script>
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {Cursor} from 'src/state/nostr'
|
||||
import {user as currentUser} from 'src/state/user'
|
||||
import {accounts, notesLoader, notesListener, modal} from "src/state/app"
|
||||
import {accounts, scroller, notesListener, modal, annotateNotes} from "src/state/app"
|
||||
|
||||
export let pubkey
|
||||
|
||||
const notes = writable([])
|
||||
let user
|
||||
let loader
|
||||
let cursor
|
||||
let listener
|
||||
let scroll
|
||||
|
||||
$: user = $accounts[pubkey]
|
||||
|
||||
onMount(async () => {
|
||||
loader = await notesLoader(notes, {kinds: [1], authors: [pubkey]}, {showParents: true})
|
||||
cursor = new Cursor({kinds: [1], authors: [pubkey]})
|
||||
listener = await notesListener(notes, {kinds: [1, 5, 7], authors: [pubkey]})
|
||||
scroll = scroller(cursor, async chunk => {
|
||||
const annotated = await annotateNotes(chunk, {showParents: true})
|
||||
|
||||
notes.update($notes => uniqBy(prop('id'), $notes.concat(annotated)))
|
||||
})
|
||||
|
||||
// Populate our initial empty space
|
||||
loader.onScroll()
|
||||
scroll()
|
||||
|
||||
// When a modal opens, suspend our subscriptions
|
||||
modal.subscribe(async $modal => {
|
||||
if ($modal) {
|
||||
loader.stop()
|
||||
cursor.stop()
|
||||
listener.stop()
|
||||
} else {
|
||||
loader.start()
|
||||
cursor.start()
|
||||
listener.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
loader?.stop()
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={loader?.onScroll} />
|
||||
<svelte:window on:scroll={scroll} />
|
||||
|
||||
{#if user}
|
||||
<div class="max-w-2xl m-auto flex flex-col gap-4 py-8 px-4">
|
||||
|
100
src/state/app.js
100
src/state/app.js
@ -5,7 +5,7 @@ import {navigate} from "svelte-routing"
|
||||
import {switcherFn, ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import {getLocalJson, setLocalJson, now, timedelta, sleep} from "src/util/misc"
|
||||
import {user} from 'src/state/user'
|
||||
import {epoch, filterMatches, Listener, Cursor, channels, relays, findReplyTo} from 'src/state/nostr'
|
||||
import {epoch, filterMatches, Listener, channels, relays, findReplyTo} from 'src/state/nostr'
|
||||
|
||||
export const modal = writable(null)
|
||||
|
||||
@ -63,7 +63,7 @@ export const ensureAccounts = async (pubkeys, {force = false} = {}) => {
|
||||
|
||||
// Notes
|
||||
|
||||
export const annotateNotesChunk = async (chunk, {showParents = false} = {}) => {
|
||||
export const annotateNotes = async (chunk, {showParents = false} = {}) => {
|
||||
const parentIds = chunk.map(findReplyTo).filter(identity)
|
||||
|
||||
if (showParents && parentIds.length) {
|
||||
@ -116,56 +116,6 @@ export const annotateNotesChunk = async (chunk, {showParents = false} = {}) => {
|
||||
return reverse(sortBy(prop('created'), chunk.map(annotate)))
|
||||
}
|
||||
|
||||
export const notesLoader = async (
|
||||
notes,
|
||||
filter,
|
||||
{
|
||||
showParents = false,
|
||||
delta = timedelta(1, 'hours'),
|
||||
isInModal = false,
|
||||
since = epoch,
|
||||
} = {}
|
||||
) => {
|
||||
const cursor = new Cursor(filter, delta)
|
||||
|
||||
const onScroll = debounce(1000, async () => {
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
// If a modal opened up, wait for them to close it
|
||||
if (!isInModal && get(modal)) {
|
||||
await sleep(1000)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// While we have empty space, fill it
|
||||
if (window.scrollY + window.innerHeight * 3 < document.body.scrollHeight) {
|
||||
break
|
||||
}
|
||||
|
||||
// Stop if we've gone back far enough
|
||||
if (cursor.since <= since) {
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = await cursor.chunk()
|
||||
const annotated = await annotateNotesChunk(chunk, {showParents})
|
||||
|
||||
notes.update($notes => uniqBy(prop('id'), $notes.concat(annotated)))
|
||||
|
||||
// If we have an empty chunk, increase our step size so we can get back to where
|
||||
// we might have old events. Once we get a chunk, knock it down to the default again
|
||||
if (annotated.length === 0) {
|
||||
cursor.delta = Math.min(timedelta(30, 'days'), cursor.delta * 2)
|
||||
} else {
|
||||
cursor.delta = delta
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Object.assign(cursor, {onScroll})
|
||||
}
|
||||
|
||||
export const notesListener = async (notes, filter) => {
|
||||
const updateNote = (id, f) =>
|
||||
notes.update($notes =>
|
||||
@ -195,11 +145,11 @@ export const notesListener = async (notes, filter) => {
|
||||
const id = findReplyTo(e)
|
||||
|
||||
if (id) {
|
||||
const [reply] = await annotateNotesChunk([e])
|
||||
const [reply] = await annotateNotes([e])
|
||||
|
||||
updateNote(id, n => ({...n, replies: n.replies.concat(reply)}))
|
||||
} else if (filterMatches(filter, e)) {
|
||||
const [note] = await annotateNotesChunk([e])
|
||||
const [note] = await annotateNotes([e])
|
||||
|
||||
notes.update($notes => uniqBy(prop('id'), [note].concat($notes)))
|
||||
}
|
||||
@ -217,3 +167,45 @@ export const notesListener = async (notes, filter) => {
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// UI
|
||||
|
||||
export const scroller = (cursor, cb, {isInModal = false, since = epoch} = {}) => {
|
||||
const startingDelta = cursor.delta
|
||||
|
||||
return debounce(1000, async () => {
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
// If a modal opened up, wait for them to close it
|
||||
if (!isInModal && get(modal)) {
|
||||
await sleep(1000)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// While we have empty space, fill it
|
||||
if (window.scrollY + window.innerHeight * 3 < document.body.scrollHeight) {
|
||||
break
|
||||
}
|
||||
|
||||
// Stop if we've gone back far enough
|
||||
if (cursor.since <= since) {
|
||||
break
|
||||
}
|
||||
|
||||
// Get our chunk
|
||||
const chunk = await cursor.chunk()
|
||||
|
||||
// Notify the caller
|
||||
await cb(chunk)
|
||||
|
||||
// If we have an empty chunk, increase our step size so we can get back to where
|
||||
// we might have old events. Once we get a chunk, knock it down to the default again
|
||||
if (chunk.length === 0) {
|
||||
cursor.delta = Math.min(timedelta(30, 'days'), cursor.delta * 2)
|
||||
} else {
|
||||
cursor.delta = startingDelta
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -108,10 +108,10 @@ export const channels = {
|
||||
// older events again for pagination. Since we have to limit channels to 3 per nip 01,
|
||||
// this requires us to unsubscribe and re-subscribe frequently
|
||||
export class Cursor {
|
||||
constructor(filter, delta = timedelta(1, 'hours')) {
|
||||
constructor(filter, delta) {
|
||||
this.filter = ensurePlural(filter)
|
||||
this.delta = delta
|
||||
this.since = now() - delta
|
||||
this.delta = delta || timedelta(1, 'hours')
|
||||
this.since = now() - this.delta
|
||||
this.until = now()
|
||||
this.sub = null
|
||||
this.q = []
|
||||
|
Loading…
Reference in New Issue
Block a user