From 5243c91d3a7c325088845cef198c2eb8906babc0 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 6 May 2025 00:46:01 -0400 Subject: Use a more targetted selector to control link text colours inside of messages. Using a wildcard selector here makes this rule surprisingly hard to override, which will be a problem for styling unsent messages. --- ui/styles/messages.css | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'ui/styles/messages.css') diff --git a/ui/styles/messages.css b/ui/styles/messages.css index 4890f2c..5890c1a 100644 --- a/ui/styles/messages.css +++ b/ui/styles/messages.css @@ -8,26 +8,22 @@ overflow: hidden; } -.own-message { +.own-message, +.own-message :link { background-color: var(--colour-message-run-self-bg); + color: var(--colour-message-run-self-text); border: 1px solid var(--colour-message-run-self-border); margin-left: 1rem; } -.own-message * { - color: var(--colour-message-run-self-text); -} - -.other-message { +.other-message, +.other-message :link { background-color: var(--colour-message-run-other-bg); + color: var(--colour-message-run-other-text); border: 1px solid var(--colour-message-run-other-border); margin-right: 1rem; } -.other-message * { - color: var(--colour-message-run-other-text); -} - .message-run > .username { background-color: var(--colour-message-run-username-bg); color: var(--colour-message-run-username-text); -- cgit v1.2.3 From e4273ffd945f16d6f74e9c64431808ea36148880 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 6 May 2025 01:07:54 -0400 Subject: Render "ghost" messages for unsent messages. There is a subtle race conditon in this code, which is likely not fixable without a protocol change: * Ghost messages can disappear before their "real" message replacement shows up, if the client finishes sending (i.e., receives an HTTP response on the POST) before the server delivers the real message. * Ghost messages can be duplicated briefly, if the client receives the real message before the client finishes sending. Both happen in practice; we make no ordering guarantees between requests. To aviod this, we'd to give clients a way to correlate pending sends with received messages. This would require fundamentally the same capabilities, like per-operation nonces, that preventing duplicate operations will require. --- ui/lib/components/Message.svelte | 11 +++++++++-- ui/lib/outbox.svelte.js | 16 ++++++++++++++++ ui/lib/runs.js | 10 +++++++++- ui/lib/state/remote/messages.svelte.js | 2 +- ui/routes/(app)/ch/[channel]/+page.svelte | 6 +++++- ui/styles/messages.css | 4 ++++ ui/styles/variables.css | 1 + 7 files changed, 45 insertions(+), 5 deletions(-) (limited to 'ui/styles/messages.css') diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte index edd9d79..ea90414 100644 --- a/ui/lib/components/Message.svelte +++ b/ui/lib/components/Message.svelte @@ -20,10 +20,17 @@ } -
+
{atFormatted} - {#if editable} + {#if editable && id} {/if}
diff --git a/ui/lib/outbox.svelte.js b/ui/lib/outbox.svelte.js index fd7fdba..0e4cf29 100644 --- a/ui/lib/outbox.svelte.js +++ b/ui/lib/outbox.svelte.js @@ -1,3 +1,6 @@ +import { DateTime } from 'luxon'; +import * as msg from './state/remote/messages.svelte.js'; + import * as api from './apiServer.js'; import * as md from './markdown.js'; @@ -5,9 +8,21 @@ class PostToChannel { constructor(channel, body) { this.channel = channel; this.body = body; + this.at = DateTime.now(); this.renderedBody = md.render(body); } + toSkeleton(sender) { + return { + id: null, + at: this.at, + channel: this.channel, + sender, + body: this.body, + renderedBody: this.renderedBody + }; + } + async send() { return await api.retry(() => api.postToChannel(this.channel, this.body)); } @@ -35,6 +50,7 @@ class CreateChannel { export class Outbox { pending = $state([]); + messages = $derived(this.pending.filter((operation) => operation instanceof PostToChannel)); static empty() { return new Outbox([]); diff --git a/ui/lib/runs.js b/ui/lib/runs.js index f4e90be..e3d4c20 100644 --- a/ui/lib/runs.js +++ b/ui/lib/runs.js @@ -21,5 +21,13 @@ function runKey(message) { } function continueRun([lastSender, lastAt], [newSender, newAt]) { - return lastSender === newSender && newAt - lastAt < RUN_COALESCE_MAX_INTERVAL; + const { id: lastId, name: lastName } = lastSender; + const { id: newId, name: newName } = newSender; + if (lastId !== newId) { + return false; + } + if (lastName !== newName) { + return false; + } + return newAt - lastAt < RUN_COALESCE_MAX_INTERVAL; } diff --git a/ui/lib/state/remote/messages.svelte.js b/ui/lib/state/remote/messages.svelte.js index 576a74e..c6d31f0 100644 --- a/ui/lib/state/remote/messages.svelte.js +++ b/ui/lib/state/remote/messages.svelte.js @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import { render } from '$lib/markdown.js'; -class Message { +export class Message { static boot({ id, at, channel, sender, body }) { return new Message({ id, diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index ccf455c..50b6a7d 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -12,7 +12,11 @@ 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 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(); diff --git a/ui/styles/messages.css b/ui/styles/messages.css index 5890c1a..67a9517 100644 --- a/ui/styles/messages.css +++ b/ui/styles/messages.css @@ -39,6 +39,10 @@ position: relative; } +.message.unsent { + color: var(--colour-message-run-unsent-text); +} + .message.delete-armed, .message.delete-armed:hover { background-color: var(--colour-warn); diff --git a/ui/styles/variables.css b/ui/styles/variables.css index c43832c..01efc19 100644 --- a/ui/styles/variables.css +++ b/ui/styles/variables.css @@ -56,6 +56,7 @@ ); --colour-message-run-self-text: var(--dark-text); --colour-message-run-other-text: var(--dark-text); + --colour-message-run-unsent-text: var(--light-text); --colour-message-run-username-bg: color-mix(in srgb, var(--colour-base) 70%, white); --colour-message-run-username-border: color-mix(in srgb, var(--colour-base) 50%, black); -- cgit v1.2.3 From a01b516844c2a89af9446864b3eccccdd5afb9dc Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 6 May 2025 01:28:28 -0400 Subject: Render messages as ghosts when there's a pending delete, too. --- ui/lib/components/Message.svelte | 20 ++++++++++++++++---- ui/lib/outbox.svelte.js | 1 + ui/routes/(app)/ch/[channel]/+page.svelte | 11 ++++++++++- ui/styles/messages.css | 3 ++- 4 files changed, 29 insertions(+), 6 deletions(-) (limited to 'ui/styles/messages.css') diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte index ea90414..5d15d17 100644 --- a/ui/lib/components/Message.svelte +++ b/ui/lib/components/Message.svelte @@ -1,7 +1,15 @@
operation instanceof PostToChannel)); + deleted = $derived(this.pending.filter((operation) => operation instanceof DeleteMessage)); static empty() { return new Outbox([]); diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 50b6a7d..33a9bdf 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -13,6 +13,7 @@ const channel = $derived(page.params.channel); const messages = $derived(session.messages.filter((message) => message.channel === channel)); 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))) ); @@ -95,7 +96,15 @@ }} > {#each messages as message} - + {/each} {/each} diff --git a/ui/styles/messages.css b/ui/styles/messages.css index 67a9517..30f6db9 100644 --- a/ui/styles/messages.css +++ b/ui/styles/messages.css @@ -39,7 +39,8 @@ position: relative; } -.message.unsent { +.message.unsent, +.message.deleted { color: var(--colour-message-run-unsent-text); } -- cgit v1.2.3