From 32a30bcf2140c8fbf1a739123d0378f17503efb0 Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:28:12 -0500 Subject: Add Luxon to dependencies This handles datetimes better than the browser-builtin, and is in many ways a successor to Moment, the previous standard choice. --- package-lock.json | 10 ++++++++++ package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/package-lock.json b/package-lock.json index a7aff66..7d8be51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.7.7", "dompurify": "^3.1.7", + "luxon": "^3.5.0", "marked": "^14.1.3", "tinygesture": "^3.0.0" }, @@ -2644,6 +2645,15 @@ "dev": true, "license": "ISC" }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.12", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", diff --git a/package.json b/package.json index 64b8453..961b44b 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "axios": "^1.7.7", "dompurify": "^3.1.7", + "luxon": "^3.5.0", "marked": "^14.1.3", "tinygesture": "^3.0.0" } -- cgit v1.2.3 From d36efbb1378ca1d6bf3b3c20391d711c00da4761 Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:29:12 -0500 Subject: Rename and modify channels store I tried to have a custom class for Channel objects, but Svelte's automatic proxy logic works only on bare objects, as far as I could tell. So that broke everything. I resorted to a function that would build the bare objects, but we still lack methods that I think would make life easier ("touch last read" etc). --- ui/lib/store.js | 2 +- ui/lib/store/channels.js | 36 ------------------------ ui/lib/store/channels.svelte.js | 62 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 37 deletions(-) delete mode 100644 ui/lib/store/channels.js create mode 100644 ui/lib/store/channels.svelte.js diff --git a/ui/lib/store.js b/ui/lib/store.js index 3b20e05..47ebbc2 100644 --- a/ui/lib/store.js +++ b/ui/lib/store.js @@ -1,5 +1,5 @@ import { writable } from 'svelte/store'; -import { Channels } from '$lib/store/channels'; +import { Channels } from '$lib/store/channels.svelte.js'; import { Messages } from '$lib/store/messages.svelte.js'; import { Logins } from '$lib/store/logins'; diff --git a/ui/lib/store/channels.js b/ui/lib/store/channels.js deleted file mode 100644 index 37dc673..0000000 --- a/ui/lib/store/channels.js +++ /dev/null @@ -1,36 +0,0 @@ -export class Channels { - constructor() { - this.channels = []; - } - - setChannels(channels) { - this.channels = [...channels]; - this.sort(); - return this; - } - - addChannel(id, name) { - this.channels = [...this.channels, { id, name }]; - this.sort(); - return this; - } - - deleteChannel(id) { - const channelIndex = this.channels.map((e) => e.id).indexOf(id); - if (channelIndex !== -1) { - this.channels.splice(channelIndex, 1); - } - return this; - } - - sort() { - this.channels.sort((a, b) => { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } - return 0; - }); - } -} diff --git a/ui/lib/store/channels.svelte.js b/ui/lib/store/channels.svelte.js new file mode 100644 index 0000000..8919be0 --- /dev/null +++ b/ui/lib/store/channels.svelte.js @@ -0,0 +1,62 @@ +import { DateTime } from 'luxon'; +const EPOCH_STRING = "1970-01-01T00:00:00Z"; + +// For reasons unclear to me, a straight up class definition with a constructor +// doesn't seem to work, reactively. So we resort to this. +// Owen suggests that this sentence in the Svelte docs should make the reason +// clear: +// > If $state is used with an array or a simple object, the result is a deeply +// > reactive state proxy. +// Emphasis on "simple object". +// --Kit +function makeChannelObject({ id, name, draft = '', lastReadAt = null, scrollPosition = null }) { + return { + id, + name, + lastReadAt: lastReadAt || DateTime.fromISO(EPOCH_STRING), + draft, + scrollPosition, + }; +} + +export class Channels { + channels = $state([]); + + getChannel(channelId) { + return this.channels.filter((ch) => ch.id === channelId)[0] || null; + } + + setChannels(channels) { + this.channels = channels.map(makeChannelObject); + this.sort(); + return this; + } + + addChannel(id, name) { + this.channels = [ + ...this.channels, + makeChannelObject({ id, name }), + ]; + this.sort(); + return this; + } + + deleteChannel(id) { + const channelIndex = this.channels.map((e) => e.id).indexOf(id); + if (channelIndex !== -1) { + this.channels.splice(channelIndex, 1); + } + return this; + } + + sort() { + this.channels.sort((a, b) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + return 0; + }); + } +} -- cgit v1.2.3 From b85dcad22f89de7ec8d07ab1776fa2f51a08ae24 Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:32:00 -0500 Subject: Use Luxon dates on Message store and component This includes jamming the "at" of a message into a data- attribute on the Message component, so that it can later be used by parent components via Plain Old Javascript and the .dataset attribute of an HTML node. --- ui/lib/components/Message.svelte | 12 ++++++++++-- ui/lib/store/messages.svelte.js | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte index 1663696..5673248 100644 --- a/ui/lib/components/Message.svelte +++ b/ui/lib/components/Message.svelte @@ -1,4 +1,5 @@ -
+
- {at} + {atFormatted} {#if editable} {/if} diff --git a/ui/lib/store/messages.svelte.js b/ui/lib/store/messages.svelte.js index 0ceba54..ba4c895 100644 --- a/ui/lib/store/messages.svelte.js +++ b/ui/lib/store/messages.svelte.js @@ -1,17 +1,18 @@ +import { DateTime } from 'luxon'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; const RUN_COALESCE_MAX_INTERVAL = 10 /* min */ * 60 /* sec */ * 1000; /* ms */ export class Messages { - channels = $state({}); + channels = $state({}); // Mapping inChannel(channel) { - return this.channels[channel]; + return this.channels[channel] || []; } addMessage(channel, id, { at, sender, body }) { - let parsedAt = new Date(at); + let parsedAt = DateTime.fromISO(at); let renderedBody = DOMPurify.sanitize(marked.parse(body, { breaks: true })); const message = { id, at: parsedAt, body, renderedBody }; -- cgit v1.2.3 From 9c4f37d3853dbc114c305dbbb45f2cb23cf8f4e0 Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:33:13 -0500 Subject: Style Channels differently when they have unreads I dunno, I like the fleuron. Maybe it's too twee? --- ui/lib/components/Channel.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte index e84c6d0..01b1c87 100644 --- a/ui/lib/components/Channel.svelte +++ b/ui/lib/components/Channel.svelte @@ -1,10 +1,14 @@
  • - # + {#if hasUnreads} + + {:else} + + {/if} {name}
  • -- cgit v1.2.3 From f8ecf97e66b999882bfa8750af111e9cf73ea89c Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:33:52 -0500 Subject: Prefer camelCase to snake_case in argument names Even when they get mapped to snake_case searchParams. --- ui/lib/apiServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js index fee1a81..e52daff 100644 --- a/ui/lib/apiServer.js +++ b/ui/lib/apiServer.js @@ -54,9 +54,9 @@ export async function acceptInvite(inviteId, username, password) { return apiServer.post(`/invite/${inviteId}`, data); } -export function subscribeToEvents(resume_point) { +export function subscribeToEvents(resumePoint) { const eventsUrl = new URL('/api/events', window.location); - eventsUrl.searchParams.append('resume_point', resume_point); + eventsUrl.searchParams.append('resume_point', resumePoint); const evtSource = new EventSource(eventsUrl.toString()); // TODO: this should process all incoming events and store them. // TODO: eventually we'll need to handle expiring old info, so as not to use -- cgit v1.2.3 From e0d93f326722609a0ba50da106ace9b7a43bbc7b Mon Sep 17 00:00:00 2001 From: Kit La Touche Date: Fri, 29 Nov 2024 14:36:04 -0500 Subject: Enrich channels with hasUnread attribute This requires both the channels and messages stores to be available, so we do it in the page file where we've got those stores handy. It's a bit inefficient (it recreates the entire enrichedChannels array every time one of them changes), but it'll do for now. There's also a TODO hinting that we might want an idea of unread-count. I think that would require a distinct idea of mentions, because we just want "fact of unread" for general-availability channels, not "this many messages since you were last here". --- ui/routes/(app)/+layout.svelte | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index ad8227e..81324fd 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -19,6 +19,35 @@ let loading = $state(true); let channel = $derived($page.params.channel); + let rawChannels; + channelsList.subscribe((val) => { + rawChannels = val.channels; + }); + let rawMessages; + messages.subscribe((val) => { + rawMessages = val; + }); + + let enrichedChannels = $derived.by(() => { + const channels = rawChannels; + const messages = rawMessages; + 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 > ch.lastReadAt; + enrichedChannels.push({ + ...ch, + hasUnreads, + }); + } + } + return enrichedChannels; + }); + function onBooted(boot) { currentUser.set({ id: boot.login.id, @@ -78,6 +107,7 @@ + pilcrow @@ -87,7 +117,7 @@