summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKit La Touche <kit@transneptune.net>2024-11-07 10:52:16 -0500
committerKit La Touche <kit@transneptune.net>2024-11-07 10:52:16 -0500
commitf31b4d8188ac7e38b6c42380649691c0e8e1e097 (patch)
tree62644664fdce6f10243ad9581b47bac547b53dd3
parent8751155e24f020802d1c387af19318edceaa39d2 (diff)
parent104f1115286834f71013e37617f12a78b3ce5210 (diff)
Merge branch 'main' into wip/pwa
-rw-r--r--eslint.config.js24
-rw-r--r--ui/lib/apiServer.js6
-rw-r--r--ui/lib/components/ActiveChannel.svelte27
-rw-r--r--ui/lib/components/MessageInput.svelte3
-rw-r--r--ui/lib/store.js2
-rw-r--r--ui/lib/store/messages.js40
-rw-r--r--ui/lib/store/messages.svelte.js60
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte4
8 files changed, 75 insertions, 91 deletions
diff --git a/eslint.config.js b/eslint.config.js
index 2eeebcc..7d23626 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,23 +1,3 @@
-import js from '@eslint/js';
-import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
-import globals from 'globals';
-
-/** @type {import('eslint').Linter.Config[]} */
-export default [
- js.configs.recommended,
- ...svelte.configs['flat/recommended'],
- prettier,
- ...svelte.configs['flat/prettier'],
- {
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.node
- }
- }
- },
- {
- ignores: ['build/', '.svelte-kit/', 'dist/']
- }
-];
+import svelte from 'eslint-plugin-svelte';
+export default [prettier, ...svelte.configs['flat/prettier']];
diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js
index a6fdaa6..6ada0f7 100644
--- a/ui/lib/apiServer.js
+++ b/ui/lib/apiServer.js
@@ -111,7 +111,11 @@ function onMessageEvent(data) {
switch (data.event) {
case 'sent':
messages.update((value) =>
- value.addMessage(data.channel, data.id, data.at, data.sender, data.body)
+ value.addMessage(data.channel, data.id, {
+ at: data.at,
+ sender: data.sender,
+ body: data.body
+ })
);
break;
case 'deleted':
diff --git a/ui/lib/components/ActiveChannel.svelte b/ui/lib/components/ActiveChannel.svelte
index a4ccd24..f939dbd 100644
--- a/ui/lib/components/ActiveChannel.svelte
+++ b/ui/lib/components/ActiveChannel.svelte
@@ -1,34 +1,11 @@
<script>
- import { messages } from '$lib/store';
import MessageRun from './MessageRun.svelte';
- let { channel } = $props();
- let messageList = $derived(channel !== null ? $messages.inChannel(channel) : []);
-
- function* chunkBy(xs, fn) {
- let chunk;
- let key;
- for (let x of xs) {
- let newKey = fn(x);
- if (key !== newKey) {
- if (chunk !== undefined) {
- yield [key, chunk];
- }
-
- chunk = [x];
- key = newKey;
- } else {
- chunk.push(x);
- }
- }
- if (chunk !== undefined) {
- yield [key, chunk];
- }
- }
+ let { messageRuns } = $props();
</script>
<div class="container">
- {#each chunkBy(messageList, (msg) => msg.sender) as [sender, messages]}
+ {#each messageRuns as { sender, messages }}
<div>
<MessageRun {sender} {messages} />
</div>
diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte
index 907391c..c071bea 100644
--- a/ui/lib/components/MessageInput.svelte
+++ b/ui/lib/components/MessageInput.svelte
@@ -18,7 +18,8 @@
}
function onKeyDown(event) {
- if (!event.altKey && event.key === 'Enter') {
+ let modifier = event.shiftKey || event.altKey || event.ctrlKey || event.metaKey;
+ if (!modifier && event.key === 'Enter') {
onSubmit(event);
}
}
diff --git a/ui/lib/store.js b/ui/lib/store.js
index ae17ffa..3b20e05 100644
--- a/ui/lib/store.js
+++ b/ui/lib/store.js
@@ -1,6 +1,6 @@
import { writable } from 'svelte/store';
import { Channels } from '$lib/store/channels';
-import { Messages } from '$lib/store/messages';
+import { Messages } from '$lib/store/messages.svelte.js';
import { Logins } from '$lib/store/logins';
export const currentUser = writable(null);
diff --git a/ui/lib/store/messages.js b/ui/lib/store/messages.js
deleted file mode 100644
index 62c567a..0000000
--- a/ui/lib/store/messages.js
+++ /dev/null
@@ -1,40 +0,0 @@
-export class Messages {
- constructor() {
- this.channels = {};
- }
-
- inChannel(channel) {
- return (this.channels[channel] = this.channels[channel] || []);
- }
-
- addMessage(channel, id, at, sender, body) {
- this.updateChannel(channel, (messages) => [...messages, { id, at, sender, body }]);
- return this;
- }
-
- setMessages(messages) {
- this.channels = {};
- for (let { channel, id, at, sender, body } of messages) {
- this.inChannel(channel).push({ id, at, sender, body });
- }
- return this;
- }
-
- deleteMessage(message) {
- for (let channel in this.channels) {
- this.updateChannel(channel, (messages) => messages.filter((msg) => msg.id != message));
- }
- return this;
- }
-
- deleteChannel(id) {
- delete this.channels[id];
- return this;
- }
-
- updateChannel(channel, callback) {
- let messages = callback(this.inChannel(channel));
- messages.sort((a, b) => a.at - b.at);
- this.channels[channel] = messages;
- }
-}
diff --git a/ui/lib/store/messages.svelte.js b/ui/lib/store/messages.svelte.js
new file mode 100644
index 0000000..4630a40
--- /dev/null
+++ b/ui/lib/store/messages.svelte.js
@@ -0,0 +1,60 @@
+const RUN_COALESCE_MAX_INTERVAL = 10 /* min */ * 60 /* sec */ * 1000; /* ms */
+
+export class Messages {
+ channels = $state({});
+
+ inChannel(channel) {
+ return this.channels[channel];
+ }
+
+ addMessage(channel, id, { at, sender, body }) {
+ let parsedAt = new Date(at);
+ const message = { id, at: parsedAt, body };
+
+ let runs = (this.channels[channel] ||= []);
+ let currentRun = runs.slice(-1)[0];
+ if (currentRun === undefined) {
+ currentRun = { sender, messages: [message] };
+ runs.push(currentRun);
+ } else {
+ let lastMessage = currentRun.messages.slice(-1)[0];
+ let newRun =
+ currentRun.sender !== sender
+ || parsedAt - lastMessage.at > RUN_COALESCE_MAX_INTERVAL;
+
+ if (newRun) {
+ currentRun = { sender, messages: [message] };
+ runs.push(currentRun);
+ } else {
+ currentRun.messages.push(message);
+ }
+ }
+
+ return this;
+ }
+
+ setMessages(messages) {
+ this.channels = {};
+ for (let { channel, id, at, sender, body } of messages) {
+ this.addMessage(channel, id, at, sender, body);
+ }
+ return this;
+ }
+
+ deleteMessage(messageId) {
+ for (let channel in this.channels) {
+ this.channels[channel] = this.channels[channel]
+ .map(({ sender, messages }) => ({
+ sender,
+ messages: messages.filter(({ id }) => id != messageId)
+ }))
+ .filter(({ messages }) => messages.length > 0);
+ }
+ return this;
+ }
+
+ deleteChannel(id) {
+ delete this.channels[id];
+ return this;
+ }
+}
diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte
index 49c1c29..0961665 100644
--- a/ui/routes/(app)/ch/[channel]/+page.svelte
+++ b/ui/routes/(app)/ch/[channel]/+page.svelte
@@ -2,12 +2,14 @@
import { page } from '$app/stores';
import ActiveChannel from '$lib/components/ActiveChannel.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
+ import { messages } from '$lib/store';
let channel = $derived($page.params.channel);
+ let messageRuns = $derived($messages.inChannel(channel));
</script>
<div class="active-channel">
- <ActiveChannel {channel} />
+ <ActiveChannel {messageRuns} />
</div>
<div class="create-message max-h-full">
<MessageInput {channel} />