diff options
Diffstat (limited to 'ui/routes/(app)')
| -rw-r--r-- | ui/routes/(app)/+layout.js | 2 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 40 | ||||
| -rw-r--r-- | ui/routes/(app)/ch/[channel]/+page.svelte | 45 |
3 files changed, 78 insertions, 9 deletions
diff --git a/ui/routes/(app)/+layout.js b/ui/routes/(app)/+layout.js index 651bc8c..9c0afa8 100644 --- a/ui/routes/(app)/+layout.js +++ b/ui/routes/(app)/+layout.js @@ -1,8 +1,10 @@ import * as session from '$lib/session.svelte.js'; +import { Outbox } from '$lib/outbox.svelte.js'; export async function load() { let s = await session.boot(); return { + outbox: Outbox.empty(), session: s }; } diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index 1ba3fa9..a4ae442 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -14,7 +14,7 @@ let gesture = null; const { data, children } = $props(); - const { session } = data; + const { session, outbox } = data; onMount(session.begin.bind(session)); onDestroy(session.end.bind(session)); @@ -87,10 +87,46 @@ }); async function createChannel(name) { - await api.createChannel(name); + outbox.createChannel(name); + } + + function onbeforeunload(event) { + if (outbox.pending.length > 0) { + // Prompt the user that they have unsaved (unsent) messages that may be lost. + event.preventDefault(); + } } </script> +<!-- + In theory, we [should be][bfcache-why] using an ephemeral event handler for this, rather than + leaving it hooked up at all times. Some browsers decide whether a page is eligible for + back/forward caching based on whether it has a beforeunload handler (among other factors), and + having the event handler registered can slow down navigation on those browsers by forcing a + network reload when the page could have been restored from memory. + + Most browsers _apparently_ no longer use that criterion, but I would have been inclined to follow + the advice regardless as I don't feel up to the task of cataloguing which browsers it applies to. + Unfortunately, it wouldn't matter if we did: SvelteKit itself registers beforeunload handlers, so + (at least as of this writing) any work we do to try to dynamically register or unregister + beforeunload handlers dynamically state would be wasted effort. + + For posterity, though, the appropriate code for doing so based on outbox state looks like + + $effect(() => { + if (outbox.pending.length > 0) { + window.addEventListener('beforeunload', onbeforeunload); + } + + return () => { + window.removeEventListener('beforeunload', onbeforeunload); + }; + }); + + [bfcache-why]: https://web.dev/articles/bfcache#beforeunload-caution +--> +<svelte:window {onbeforeunload} /> + <svelte:head> <!-- TODO: unread count? --> <title>pilcrow</title> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index c8507cc..33a9bdf 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -1,18 +1,23 @@ <script> import { DateTime } from 'luxon'; import { page } from '$app/state'; - import ActiveChannel from '$lib/components/ActiveChannel.svelte'; import MessageInput from '$lib/components/MessageInput.svelte'; + import MessageRun from '$lib/components/MessageRun.svelte'; + import Message from '$lib/components/Message.svelte'; import { runs } from '$lib/runs.js'; - import * as api from '$lib/apiServer'; const { data } = $props(); - const { session } = data; + const { session, outbox } = 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)); + const unsent = $derived(outbox.messages.filter((message) => message.channel === channel)); + const deleted = $derived(outbox.deleted.map((message) => message.messageId)); + const unsentSkeletons = $derived( + unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))) + ); + const messageRuns = $derived(runs(messages.concat(unsentSkeletons), session.currentUser)); function inView(parentElement, element) { const parRect = parentElement.getBoundingClientRect(); @@ -51,6 +56,12 @@ setLastRead(); }); + $effect(() => { + // This is just to force it to track messageRuns. + const _ = messageRuns; + document.querySelector('.message-run:last-child .message:last-child')?.scrollIntoView(); + }); + function handleKeydown(event) { if (event.key === 'Escape') { setLastRead(); // TODO: pass in "last message DT"? @@ -65,18 +76,38 @@ } async function sendMessage(message) { - await api.postToChannel(channel, message); + outbox.postToChannel(channel, message); } async function deleteMessage(id) { - await api.deleteMessage(id); + outbox.deleteMessage(id); } </script> <svelte:window onkeydown={handleKeydown} /> <div class="active-channel" {onscroll} bind:this={activeChannel}> - <ActiveChannel {messageRuns} {deleteMessage} /> + {#each messageRuns as { sender, ownMessage, messages }} + <MessageRun + {sender} + class={{ + ['own-message']: ownMessage, + ['other-message']: !ownMessage + }} + > + {#each messages as message} + <Message + {...message} + editable={ownMessage} + {deleteMessage} + class={{ + unsent: !message.id, + deleted: deleted.includes(message.id) + }} + /> + {/each} + </MessageRun> + {/each} </div> <div class="create-message"> <MessageInput {sendMessage} /> |
