Fix loading and scrolling

This commit is contained in:
Jon Staab
2025-01-02 15:08:16 -08:00
parent 9e96d5e483
commit f5dced433a
8 changed files with 108 additions and 65 deletions

View File

@@ -22,7 +22,7 @@
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
</script> </script>
<Link external href={url} class="my-2 flex"> <Link external href={url} class="my-2 inline-block">
<div class="overflow-hidden rounded-box leading-[0]"> <div class="overflow-hidden rounded-box leading-[0]">
{#if url.match(/\.(mov|webm|mp4)$/)} {#if url.match(/\.(mov|webm|mp4)$/)}
<video controls src={url} class="max-h-96 object-contain object-center"> <video controls src={url} class="max-h-96 object-contain object-center">

View File

@@ -27,7 +27,7 @@
</p> </p>
</div> </div>
{:else} {:else}
<p class="-mb-3 h-0 text-end text-xs opacity-75"> <p class="mb-3 h-0 text-xs opacity-75">
{formatTimestamp(event.created_at)} {formatTimestamp(event.created_at)}
</p> </p>
{/if} {/if}

View File

@@ -1,9 +1,9 @@
import {get} from "svelte/store" import {get} from "svelte/store"
import {partition, assoc, now, ago, MONTH} from "@welshman/lib" import {partition, assoc, now} from "@welshman/lib"
import {MESSAGE, DELETE, THREAD, COMMENT} from "@welshman/util" import {MESSAGE, THREAD, COMMENT} from "@welshman/util"
import type {Subscription} from "@welshman/net" import type {Subscription} from "@welshman/net"
import type {AppSyncOpts} from "@welshman/app" import type {AppSyncOpts} from "@welshman/app"
import {subscribe, repository, pull, hasNegentropy} from "@welshman/app" import {subscribe, load, repository, pull, hasNegentropy} from "@welshman/app"
import {userRoomsByUrl, getUrlsForEvent} from "@app/state" import {userRoomsByUrl, getUrlsForEvent} from "@app/state"
// Utils // Utils
@@ -35,12 +35,12 @@ export const listenForNotifications = () => {
const subs: Subscription[] = [] const subs: Subscription[] = []
for (const [url, rooms] of userRoomsByUrl.get()) { for (const [url, rooms] of userRoomsByUrl.get()) {
pullConservatively({ load({
relays: [url], relays: [url],
filters: [ filters: [
{kinds: [THREAD, DELETE], since: ago(MONTH)}, {kinds: [THREAD], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], since: ago(MONTH)}, {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], since: ago(MONTH)})), ...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
], ],
}) })

View File

@@ -607,8 +607,7 @@ export const userSettingValues = withGetter(
derived(userSettings, $s => $s?.values || defaultSettings), derived(userSettings, $s => $s?.values || defaultSettings),
) )
export const getSetting = <T = any>(key: keyof Settings["values"]) => export const getSetting = <T>(key: keyof Settings["values"]) => userSettingValues.get()[key] as T
userSettingValues.get()[key] as T
export const userMembership = withGetter( export const userMembership = withGetter(
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => { derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {

View File

@@ -5,20 +5,12 @@
import {get, derived} from "svelte/store" import {get, derived} from "svelte/store"
import {dev} from "$app/environment" import {dev} from "$app/environment"
import {bytesToHex, hexToBytes} from "@noble/hashes/utils" import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
import { import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
identity,
sleep,
take,
sortBy,
ago,
now,
HOUR,
WEEK,
Worker,
} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import { import {
MESSAGE,
PROFILE, PROFILE,
DELETE,
REACTION, REACTION,
ZAP_RESPONSE, ZAP_RESPONSE,
FOLLOWS, FOLLOWS,
@@ -111,13 +103,13 @@
events: storageAdapters.fromRepositoryAndTracker(repository, tracker, { events: storageAdapters.fromRepositoryAndTracker(repository, tracker, {
throttle: 3000, throttle: 3000,
migrate: (events: TrustedEvent[]) => { migrate: (events: TrustedEvent[]) => {
if (events.length < 50_000) { if (events.length < 15_000) {
return events return events
} }
const NEVER_KEEP = 0 const NEVER_KEEP = 0
const ALWAYS_KEEP = Infinity const ALWAYS_KEEP = Infinity
const reactionKinds = [REACTION, ZAP_RESPONSE] const reactionKinds = [REACTION, ZAP_RESPONSE, DELETE]
const metaKinds = [PROFILE, FOLLOWS, RELAYS, INBOX_RELAYS] const metaKinds = [PROFILE, FOLLOWS, RELAYS, INBOX_RELAYS]
const $sessionKeys = new Set(Object.keys(app.sessions.get())) const $sessionKeys = new Set(Object.keys(app.sessions.get()))
const $userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows)))) const $userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows))))
@@ -129,6 +121,9 @@
// No need to keep a record of everyone who follows the current user // No need to keep a record of everyone who follows the current user
if (e.kind === FOLLOWS && !isFollowing) return NEVER_KEEP if (e.kind === FOLLOWS && !isFollowing) return NEVER_KEEP
// Drop room messages after a month, re-load on demand
if (e.kind === MESSAGE && e.created_at < ago(MONTH)) return NEVER_KEEP
// Always keep stuff by or tagging a signed in user // Always keep stuff by or tagging a signed in user
if ($sessionKeys.has(e.pubkey)) return ALWAYS_KEEP if ($sessionKeys.has(e.pubkey)) return ALWAYS_KEEP
if (e.tags.some(t => $sessionKeys.has(t[1]))) return ALWAYS_KEEP if (e.tags.some(t => $sessionKeys.has(t[1]))) return ALWAYS_KEEP
@@ -148,7 +143,7 @@
} }
return take( return take(
30_000, 10_000,
sortBy(e => -scoreEvent(e), events), sortBy(e => -scoreEvent(e), events),
) )
}, },
@@ -193,11 +188,11 @@
// Listen for chats, populate chat-based notifications // Listen for chats, populate chat-based notifications
let chatsSub: any let chatsSub: any
derived([pubkey, userInboxRelaySelections], identity).subscribe( derived([pubkey, canDecrypt, userInboxRelaySelections], identity).subscribe(
([$pubkey, $userInboxRelaySelections]) => { ([$pubkey, $canDecrypt, $userInboxRelaySelections]) => {
chatsSub?.close() chatsSub?.close()
if ($pubkey) { if ($pubkey && $canDecrypt) {
chatsSub = subscribe({ chatsSub = subscribe({
filters: [ filters: [
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)}, {kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {now} from "@welshman/lib" import {ago, WEEK} from "@welshman/lib"
import {GROUPS, MESSAGE, DELETE} from "@welshman/util"
import {subscribe} from "@welshman/app" import {subscribe} from "@welshman/app"
import {DELETE, REACTION, GROUPS} from "@welshman/util"
import Page from "@lib/components/Page.svelte" import Page from "@lib/components/Page.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte" import MenuSpace from "@app/components/MenuSpace.svelte"
@@ -12,11 +12,14 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands" import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands"
import {decodeRelay} from "@app/state" import {decodeRelay, userRoomsByUrl, THREAD_FILTER, COMMENT_FILTER} from "@app/state"
import {pullConservatively} from "@app/requests"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const rooms = Array.from($userRoomsByUrl.get(url) || [])
const checkConnection = async () => { const checkConnection = async () => {
const connectionError = await checkRelayConnection(url) const connectionError = await checkRelayConnection(url)
@@ -43,11 +46,27 @@
onMount(() => { onMount(() => {
checkConnection() checkConnection()
const sub = subscribe({ const relays = [url]
relays: [url], const since = ago(WEEK)
filters: [{kinds: [GROUPS]}, {kinds: [DELETE, REACTION], since: now()}],
// Load all groups for this space to populate navigation
pullConservatively({relays, filters: [{kinds: [GROUPS]}]})
// Load threads and comments
pullConservatively({
relays,
filters: [
{...THREAD_FILTER, since},
{...COMMENT_FILTER, since},
],
}) })
// Load recent messages for user rooms to help with a quick page transition
pullConservatively({relays, filters: rooms.map(r => ({kinds: [MESSAGE], "#h": [r], since}))})
// Listen for deletes that would apply to messages we already have
const sub = subscribe({relays, filters: [{kinds: [DELETE], since}]})
return () => { return () => {
sub.close() sub.close()
} }

View File

@@ -1,13 +1,20 @@
<script lang="ts"> <script lang="ts">
import {nip19} from "nostr-tools" import {nip19} from "nostr-tools"
import {onDestroy} from "svelte" import {onMount} from "svelte"
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sleep, ctx} from "@welshman/lib" import {sleep, now, ctx} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {throttled} from "@welshman/store" import {throttled} from "@welshman/store"
import {createEvent, MESSAGE} from "@welshman/util" import {feedsFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {formatTimestampAsDate, publishThunk, deriveRelay} from "@welshman/app" import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
import {
formatTimestampAsDate,
createFeedController,
subscribe,
publishThunk,
deriveRelay,
} from "@welshman/app"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import {createScroller, type Scroller} from "@lib/html" import {createScroller, type Scroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -41,6 +48,8 @@
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const relay = deriveRelay(url) const relay = deriveRelay(url)
const legacyRoom = room === GENERAL ? "general" : room const legacyRoom = room === GENERAL ? "general" : room
const feeds = feedsFromFilter({kinds: [MESSAGE], "#h": [room]})
const events = throttled( const events = throttled(
300, 300,
deriveEventsForUrl(url, [ deriveEventsForUrl(url, [
@@ -49,6 +58,14 @@
]), ]),
) )
const ctrl = createFeedController({
useWindowing: true,
feed: makeIntersectionFeed(makeRelayFeed(url), ...feeds),
onExhausted: () => {
loading = false
},
})
const assertEvent = (e: any) => e as TrustedEvent const assertEvent = (e: any) => e as TrustedEvent
const joinRoom = async () => { const joinRoom = async () => {
@@ -87,8 +104,9 @@
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
let limit = 30 let limit = 100
let loading = sleep(5000) let loading = true
let unmounted = false
let element: HTMLElement let element: HTMLElement
let scroller: Scroller let scroller: Scroller
let editor: ReturnType<typeof getEditor> let editor: ReturnType<typeof getEditor>
@@ -118,27 +136,39 @@
previousPubkey = pubkey previousPubkey = pubkey
} }
return $elements.reverse().slice(0, limit) return $elements.reverse()
}) })
// Sveltekit doesn't set element in onMount for some reason onMount(() => {
$: { // Element is frequently not defined. I don't know why
if (element) { sleep(1000).then(() => {
scroller = createScroller({ if (!unmounted) {
element, scroller = createScroller({
delay: 300, element,
threshold: 3000, delay: 300,
onScroll: () => { threshold: 10_000,
limit += 30 onScroll: () => {
loading = sleep(5000) limit += 100
},
})
}
}
onDestroy(() => { if ($events.length - limit < 100) {
setChecked($page.url.pathname) ctrl.load(200)
scroller?.stop() }
},
})
}
})
const sub = subscribe({
relays: [url],
filters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
})
return () => {
unmounted = true
setChecked($page.url.pathname)
scroller?.stop()
sub.close()
}
}) })
</script> </script>
@@ -170,7 +200,7 @@
<div <div
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-auto py-2" class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-auto py-2"
bind:this={element}> bind:this={element}>
{#each $elements as { type, id, value, showPubkey } (id)} {#each $elements.slice(0, limit) as { type, id, value, showPubkey } (id)}
{#if type === "date"} {#if type === "date"}
<Divider>{value}</Divider> <Divider>{value}</Divider>
{:else} {:else}
@@ -180,11 +210,11 @@
{/if} {/if}
{/each} {/each}
<p class="flex h-10 items-center justify-center py-20"> <p class="flex h-10 items-center justify-center py-20">
{#await loading} {#if loading}
<Spinner loading>Looking for messages...</Spinner> <Spinner loading>Looking for messages...</Spinner>
{:then} {:else}
<Spinner>End of message history</Spinner> <Spinner>End of message history</Spinner>
{/await} {/if}
</p> </p>
</div> </div>
<ChannelCompose bind:editor {content} {onSubmit} /> <ChannelCompose bind:editor {content} {onSubmit} />

View File

@@ -5,7 +5,7 @@
import {sortBy, min, nthEq, sleep} from "@welshman/lib" import {sortBy, min, nthEq, sleep} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util" import {getListTags, getPubkeyTagValues} from "@welshman/util"
import {throttled} from "@welshman/store" import {throttled} from "@welshman/store"
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {feedFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {createFeedController, userMutes} from "@welshman/app" import {createFeedController, userMutes} from "@welshman/app"
import {createScroller, type Scroller} from "@lib/html" import {createScroller, type Scroller} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
@@ -21,7 +21,7 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const feeds = feedsFromFilters([THREAD_FILTER, COMMENT_FILTER]) const feed = feedFromFilters([THREAD_FILTER, COMMENT_FILTER])
const threads = deriveEventsForUrl(url, [THREAD_FILTER]) const threads = deriveEventsForUrl(url, [THREAD_FILTER])
const comments = deriveEventsForUrl(url, [COMMENT_FILTER]) const comments = deriveEventsForUrl(url, [COMMENT_FILTER])
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes)) const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
@@ -50,7 +50,7 @@
const ctrl = createFeedController({ const ctrl = createFeedController({
useWindowing: true, useWindowing: true,
feed: makeIntersectionFeed(makeRelayFeed(url), feeds), feed: makeIntersectionFeed(makeRelayFeed(url), feed),
onExhausted: () => { onExhausted: () => {
loading = false loading = false
}, },