diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-02-25 00:37:33 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-02-26 01:54:44 -0500 |
| commit | f788ea84e25a4f7216ca0604aeb216346403b6ef (patch) | |
| tree | db62f5d1e15d871f8a73ce20b40cd53053d12f85 /ui/routes/(app) | |
| parent | f1b124a0423cdaf4d8a6bd62a2059722e9afdf2b (diff) | |
Track state on a per-session basis, rather than via globals.
Sorry about the thousand-line omnibus change; this is functionally a rewrite of the client's state tracking, flavoured to resemble the existing code as far as is possible, rather than something that can be parted out and committed in pieces.
Highlights:
* No more `store.writeable()`s. All state is now tracked using state runs or derivatives. State is still largely structured the way it was, but several bits of nested state have been rewritten to ensure that their properties are reactive just as much as their containers are.
* State is no longer global. `(app)/+layout` manages a stateful session, created via its load hook and started/stopped via component mount and destroy events. The session also tracks an event source for the current state, and feeds events into the state, broadly along the same lines as the previous stores-based approach.
Together these two changes fix up several rough spots integrating state with Svelte, and allow for the possibility of multiple states. This is a major step towards restartable states, and thus towards better connection management, which will require the ability to "start over" once a connection is restored.
Diffstat (limited to 'ui/routes/(app)')
| -rw-r--r-- | ui/routes/(app)/+layout.js | 8 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 125 | ||||
| -rw-r--r-- | ui/routes/(app)/ch/[channel]/+page.svelte | 25 | ||||
| -rw-r--r-- | ui/routes/(app)/me/+page.svelte | 2 |
4 files changed, 51 insertions, 109 deletions
diff --git a/ui/routes/(app)/+layout.js b/ui/routes/(app)/+layout.js new file mode 100644 index 0000000..651bc8c --- /dev/null +++ b/ui/routes/(app)/+layout.js @@ -0,0 +1,8 @@ +import * as session from '$lib/session.svelte.js'; + +export async function load() { + let s = await session.boot(); + return { + session: s + }; +} diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index 7818505..9ec5244 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -8,61 +8,42 @@ import TinyGesture from 'tinygesture'; import * as api from '$lib/apiServer.js'; - import { - channelsList, - channelsMetaList, - currentUser, - logins, - messages, - onEvent - } from '$lib/store'; import ChannelList from '$lib/components/ChannelList.svelte'; import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; - let events = null; let gesture = null; + const { data, children } = $props(); + const { session } = data; + + onMount(session.begin.bind(session)); + onDestroy(session.end.bind(session)); + let pageContext = getContext('page'); - let { children } = $props(); - let loading = $state(true); let channel = $derived(page.params.channel); - let rawChannels = $derived($channelsList.channels); - let rawChannelsMeta = $derived($channelsMetaList.channelsMeta); - let rawMessages = $derived($messages); - - let enrichedChannels = $derived.by(() => { - const channels = rawChannels; - const channelsMeta = rawChannelsMeta; - const messages = rawMessages; + let rawChannels = $derived(session.channels); + let rawChannelsMeta = $derived(session.local.all); + let rawMessages = $derived(session.messages); + function enrichChannels(channels, channelsMeta, messages) { const enrichedChannels = []; - if (channels && messages) { - for (let ch of channels) { - let runs = messages.inChannel(ch.id); - let lastRun = runs?.slice(-1)[0]; - let lastMessage = lastRun?.messages.slice(-1)[0]; - let lastMessageAt = lastMessage?.at; - let hasUnreads = lastMessageAt > channelsMeta[ch.id]?.lastReadAt; - enrichedChannels.push({ - ...ch, - hasUnreads - }); - } + for (const ch of channels.values()) { + const channelMessages = messages.filter((message) => message.channel === ch.id); + const lastMessage = channelMessages.slice(-1)[0]; + const lastMessageAt = lastMessage?.at; + const lastReadAt = channelsMeta.get(ch.id)?.lastReadAt; + const hasUnreads = lastReadAt === null || lastMessageAt > lastReadAt; + enrichedChannels.push({ + ...ch, + hasUnreads + }); } return enrichedChannels; - }); - - function onBooted(boot) { - currentUser.set({ - id: boot.login.id, - username: boot.login.name - }); - logins.update((value) => value.setLogins(boot.logins)); - channelsList.update((value) => value.setChannels(boot.channels)); - messages.update((value) => value.setMessages(boot.messages)); } + const enrichedChannels = $derived(enrichChannels(rawChannels, rawChannelsMeta, rawMessages)); + function setUpGestures() { if (!browser) { // Meaningless if we're not in a browser, so... @@ -77,46 +58,14 @@ }); } - onMount(async () => { - let response = await api.boot(); - switch (response.status) { - case 200: - onBooted(response.data); - events = api.subscribeToEvents(response.data.resume_point); - events.onmessage = onEvent.fromMessage; - break; - case 401: - currentUser.set(null); - await goto('/login'); - break; - case 503: - currentUser.set(null); - await goto('/setup'); - break; - default: - // TODO: display error. - break; - } - setUpGestures(); - - loading = false; - }); + onMount(setUpGestures); onDestroy(async () => { - if (events !== null) { - events.close(); - } if (gesture !== null) { gesture.destroy(); } }); - function onbeforeunload(event) { - if (events !== null) { - events.close(); - } - } - const STORE_KEY_LAST_ACTIVE = 'pilcrow:lastActiveChannel'; function getLastActiveChannel() { @@ -142,25 +91,19 @@ } </script> -<svelte:window {onbeforeunload} /> - <svelte:head> <!-- TODO: unread count? --> <title>pilcrow</title> </svelte:head> -{#if loading} - <h2>Loading…</h2> -{:else} - <div id="interface"> - <nav id="sidebar" data-expanded={pageContext.showMenu}> - <ChannelList active={channel} channels={enrichedChannels} /> - <div class="create-channel"> - <CreateChannelForm {createChannel} /> - </div> - </nav> - <main> - {@render children?.()} - </main> - </div> -{/if} +<div id="interface"> + <nav id="sidebar" data-expanded={pageContext.showMenu}> + <ChannelList active={channel} channels={enrichedChannels} /> + <div class="create-channel"> + <CreateChannelForm {createChannel} /> + </div> + </nav> + <main> + {@render children?.()} + </main> +</div> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 095e66a..c8507cc 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -3,24 +3,17 @@ import { page } from '$app/state'; import ActiveChannel from '$lib/components/ActiveChannel.svelte'; import MessageInput from '$lib/components/MessageInput.svelte'; - import { channelsList, channelsMetaList, currentUser, logins, messages } from '$lib/store'; + import { runs } from '$lib/runs.js'; import * as api from '$lib/apiServer'; - let channel = $derived(page.params.channel); - let messageRuns = $derived( - $messages.inChannel(channel).map(({ sender, messages }) => { - let senderName = $derived($logins.get(sender)); - let ownMessage = $derived($currentUser !== null && $currentUser.id === sender); - - return { - sender: senderName, - ownMessage, - messages - }; - }) - ); + const { data } = $props(); + const { session } = data; let activeChannel; + const channel = $derived(page.params.channel); + const messages = $derived(session.messages.filter((message) => message.channel === channel)); + const messageRuns = $derived(runs(messages, session.currentUser)); + function inView(parentElement, element) { const parRect = parentElement.getBoundingClientRect(); const parentTop = parRect.top; @@ -49,12 +42,12 @@ const lastInView = getLastVisibleMessage(); if (lastInView) { const at = DateTime.fromISO(lastInView.dataset.at); - $channelsMetaList.updateLastReadAt(channel, at); + session.local.updateLastReadAt(channel, at); } } $effect(() => { - const _ = $messages.inChannel(channel); + const _ = session.messages; setLastRead(); }); diff --git a/ui/routes/(app)/me/+page.svelte b/ui/routes/(app)/me/+page.svelte index ab214e9..0c960c8 100644 --- a/ui/routes/(app)/me/+page.svelte +++ b/ui/routes/(app)/me/+page.svelte @@ -5,14 +5,12 @@ import { goto } from '$app/navigation'; import * as api from '$lib/apiServer.js'; - import { currentUser } from '$lib/store'; let invites = $state([]); async function logOut() { const response = await api.logOut(); if (200 <= response.status && response.status < 300) { - currentUser.set(null); await goto('/login'); } } |
