forked from coracle/flotilla
Add forward scrolling to makeMakeFeed
This commit is contained in:
@@ -20,7 +20,7 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
|
||||
|
||||
## Development
|
||||
|
||||
See [./CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
See [CONTRIBUTING.md](AGENTS.md).
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
+80
-40
@@ -9,9 +9,11 @@ import {
|
||||
sortBy,
|
||||
now,
|
||||
on,
|
||||
between,
|
||||
isDefined,
|
||||
filterVals,
|
||||
fromPairs,
|
||||
HOUR,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
EVENT_TIME,
|
||||
@@ -23,9 +25,8 @@ import {
|
||||
getRelaysFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, Filter, List} from "@welshman/util"
|
||||
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
|
||||
import {load, request} from "@welshman/net"
|
||||
import {repository, makeFeedController, loadRelay, tracker} from "@welshman/app"
|
||||
import {repository, loadRelay, tracker} from "@welshman/app"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import {getEventsForUrl} from "@app/core/state"
|
||||
@@ -36,55 +37,61 @@ export const makeFeed = ({
|
||||
url,
|
||||
filters,
|
||||
element,
|
||||
onExhausted,
|
||||
onBackwardExhausted,
|
||||
onForwardExhausted,
|
||||
at = now(),
|
||||
}: {
|
||||
url: string
|
||||
filters: Filter[]
|
||||
element: HTMLElement
|
||||
onExhausted?: () => void
|
||||
onBackwardExhausted?: () => void
|
||||
onForwardExhausted?: () => void
|
||||
at?: number
|
||||
}) => {
|
||||
const seen = new Set<string>()
|
||||
const interval = int(12, HOUR)
|
||||
const controller = new AbortController()
|
||||
const buffer = writable<TrustedEvent[]>([])
|
||||
const events = writable<TrustedEvent[]>([])
|
||||
|
||||
let buffer: TrustedEvent[] = []
|
||||
let backwardWindow = [at - interval, at]
|
||||
let forwardWindow = [at, at + interval]
|
||||
|
||||
const insertEvent = (event: TrustedEvent) => {
|
||||
let handled = false
|
||||
|
||||
if (seen.has(event.id)) {
|
||||
return
|
||||
}
|
||||
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
|
||||
const $events = get(events)
|
||||
|
||||
events.update($events => {
|
||||
for (let i = 0; i < $events.length; i++) {
|
||||
if ($events[i].id === event.id) return $events
|
||||
if ($events[i].created_at < event.created_at) {
|
||||
if ($events[i].created_at > event.created_at) {
|
||||
events.set(insertAt(i, event, $events))
|
||||
handled = true
|
||||
return insertAt(i, event, $events)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return $events
|
||||
})
|
||||
|
||||
if (!handled) {
|
||||
buffer.update($buffer => {
|
||||
for (let i = 0; i < $buffer.length; i++) {
|
||||
if ($buffer[i].id === event.id) return $buffer
|
||||
if ($buffer[i].created_at < event.created_at) return insertAt(i, event, $buffer)
|
||||
if (!handled) {
|
||||
events.set([...$events, event])
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
if (buffer[i].created_at > event.created_at) {
|
||||
buffer.splice(i, 0, event)
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [...$buffer, event]
|
||||
})
|
||||
if (!handled) {
|
||||
buffer.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
seen.add(event.id)
|
||||
}
|
||||
|
||||
const unsubscribers = [
|
||||
on(repository, "update", ({added, removed}) => {
|
||||
if (removed.size > 0) {
|
||||
buffer.update($buffer => $buffer.filter(e => !removed.has(e.id)))
|
||||
buffer = buffer.filter(e => !removed.has(e.id))
|
||||
events.update($events => $events.filter(e => !removed.has(e.id)))
|
||||
}
|
||||
|
||||
@@ -105,24 +112,56 @@ export const makeFeed = ({
|
||||
}),
|
||||
]
|
||||
|
||||
const ctrl = makeFeedController({
|
||||
useWindowing: true,
|
||||
signal: controller.signal,
|
||||
feed: makeIntersectionFeed(makeRelayFeed(url), feedFromFilters(filters)),
|
||||
onExhausted,
|
||||
})
|
||||
const loadTimeframe = (since: number, until: number) => {
|
||||
request({
|
||||
relays: [url],
|
||||
autoClose: true,
|
||||
signal: controller.signal,
|
||||
filters: filters.map(filter => ({...filter, since, until})),
|
||||
})
|
||||
}
|
||||
|
||||
const scroller = createScroller({
|
||||
const backwardScroller = createScroller({
|
||||
element,
|
||||
delay: 300,
|
||||
threshold: 10_000,
|
||||
onScroll: async () => {
|
||||
const $buffer = get(buffer)
|
||||
threshold: 5000,
|
||||
onScroll: () => {
|
||||
const [since, until] = backwardWindow
|
||||
|
||||
events.update($events => [...$events, ...$buffer.splice(0, 30)])
|
||||
backwardWindow = [since - interval, since]
|
||||
|
||||
if ($buffer.length < 100) {
|
||||
ctrl.load(100)
|
||||
for (const event of buffer.splice(0)) {
|
||||
insertEvent(event)
|
||||
}
|
||||
|
||||
if (until > now() - int(2, YEAR)) {
|
||||
loadTimeframe(since, until)
|
||||
} else if (!buffer.some(e => e.created_at < at)) {
|
||||
backwardScroller.stop()
|
||||
onBackwardExhausted?.()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const forwardScroller = createScroller({
|
||||
element,
|
||||
reverse: true,
|
||||
delay: 300,
|
||||
threshold: 5000,
|
||||
onScroll: () => {
|
||||
const [since, until] = forwardWindow
|
||||
|
||||
forwardWindow = [until, until + interval]
|
||||
|
||||
for (const event of buffer.splice(0)) {
|
||||
insertEvent(event)
|
||||
}
|
||||
|
||||
if (until < now()) {
|
||||
loadTimeframe(since, until)
|
||||
} else if (!buffer.some(e => e.created_at > at)) {
|
||||
forwardScroller.stop()
|
||||
onForwardExhausted?.()
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -134,8 +173,9 @@ export const makeFeed = ({
|
||||
return {
|
||||
events,
|
||||
cleanup: () => {
|
||||
scroller.stop()
|
||||
controller.abort()
|
||||
forwardScroller.stop()
|
||||
backwardScroller.stop()
|
||||
unsubscribers.forEach(call)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -186,11 +186,20 @@
|
||||
const scrollToNewMessages = () =>
|
||||
document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
|
||||
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
const scrollToBottom = () => {
|
||||
if ($page.url.searchParams.get("at")) {
|
||||
at = now()
|
||||
start()
|
||||
} else {
|
||||
element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
}
|
||||
}
|
||||
|
||||
let joining = $state(false)
|
||||
let leaving = $state(false)
|
||||
let loadingEvents = $state(true)
|
||||
let loadingBackward = $state(true)
|
||||
let loadingForward = $state(true)
|
||||
let at = $state(parseInt($page.url.searchParams.get("at") || String(now())))
|
||||
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let element: HTMLElement | undefined = $state()
|
||||
@@ -221,7 +230,7 @@
|
||||
const adjustedLastChecked =
|
||||
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
||||
|
||||
for (const event of $events.toReversed()) {
|
||||
for (const event of $events) {
|
||||
if (seen.has(event.id)) {
|
||||
continue
|
||||
}
|
||||
@@ -272,11 +281,15 @@
|
||||
cleanup?.()
|
||||
|
||||
const feed = makeFeed({
|
||||
at,
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}],
|
||||
onExhausted: () => {
|
||||
loadingEvents = false
|
||||
onBackwardExhausted: () => {
|
||||
loadingBackward = false
|
||||
},
|
||||
onForwardExhausted: () => {
|
||||
loadingForward = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -319,14 +332,11 @@
|
||||
start()
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
observer.unobserve(chatCompose!)
|
||||
observer.unobserve(dynamicPadding!)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cleanup?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<PageBar>
|
||||
@@ -373,6 +383,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if loadingForward}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "new-messages"}
|
||||
<div
|
||||
@@ -405,8 +420,8 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#if loadingEvents}
|
||||
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
||||
{#if loadingBackward}
|
||||
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/if}
|
||||
|
||||
@@ -124,9 +124,18 @@
|
||||
const scrollToNewMessages = () =>
|
||||
document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"})
|
||||
|
||||
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
const scrollToBottom = () => {
|
||||
if ($page.url.searchParams.get("at")) {
|
||||
at = now()
|
||||
start()
|
||||
} else {
|
||||
element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
}
|
||||
}
|
||||
|
||||
let loadingEvents = $state(true)
|
||||
let loadingBackward = $state(true)
|
||||
let loadingForward = $state(true)
|
||||
let at = $state(parseInt($page.url.searchParams.get("at") || String(now())))
|
||||
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let element: HTMLElement | undefined = $state()
|
||||
@@ -157,7 +166,7 @@
|
||||
const adjustedLastChecked =
|
||||
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
||||
|
||||
for (const event of $events.toReversed()) {
|
||||
for (const event of $events) {
|
||||
if (seen.has(event.id)) {
|
||||
continue
|
||||
}
|
||||
@@ -204,6 +213,26 @@
|
||||
return elements
|
||||
})
|
||||
|
||||
const start = () => {
|
||||
cleanup?.()
|
||||
|
||||
const feed = makeFeed({
|
||||
at,
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [...MESSAGE_KINDS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}],
|
||||
onBackwardExhausted: () => {
|
||||
loadingBackward = false
|
||||
},
|
||||
onForwardExhausted: () => {
|
||||
loadingForward = false
|
||||
},
|
||||
})
|
||||
|
||||
events = feed.events
|
||||
cleanup = feed.cleanup
|
||||
}
|
||||
|
||||
const onEscape = () => {
|
||||
clearParent()
|
||||
clearShare()
|
||||
@@ -238,29 +267,13 @@
|
||||
|
||||
observer.observe(chatCompose!)
|
||||
observer.observe(dynamicPadding!)
|
||||
|
||||
const feed = makeFeed({
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [...MESSAGE_KINDS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}],
|
||||
onExhausted: () => {
|
||||
loadingEvents = false
|
||||
},
|
||||
})
|
||||
|
||||
events = feed.events
|
||||
cleanup = feed.cleanup
|
||||
start()
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
controller.abort()
|
||||
observer.unobserve(chatCompose!)
|
||||
observer.unobserve(dynamicPadding!)
|
||||
|
||||
// Sveltekit calls onDestroy at the beginning of the page load for some reason
|
||||
setTimeout(() => {
|
||||
setChecked($page.url.pathname)
|
||||
}, 800)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -284,6 +297,11 @@
|
||||
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
|
||||
<div bind:this={dynamicPadding}></div>
|
||||
{#if loadingForward}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "new-messages"}
|
||||
<div
|
||||
@@ -316,8 +334,8 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#if loadingEvents}
|
||||
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
||||
{#if loadingBackward}
|
||||
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/if}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [CLASSIFIED]}, makeCommentFilter([CLASSIFIED])],
|
||||
onExhausted: () => {
|
||||
onBackwardExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [ZAP_GOAL]}, makeCommentFilter([ZAP_GOAL])],
|
||||
onExhausted: () => {
|
||||
onBackwardExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [THREAD]}, makeCommentFilter([THREAD])],
|
||||
onExhausted: () => {
|
||||
onBackwardExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user