summaryrefslogtreecommitdiff
path: root/ui/routes
diff options
context:
space:
mode:
Diffstat (limited to 'ui/routes')
-rw-r--r--ui/routes/(app)/+layout.js2
-rw-r--r--ui/routes/(app)/+layout.svelte40
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte45
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} />