diff options
| author | Kit La Touche <kit@transneptune.net> | 2024-11-09 22:55:22 -0500 |
|---|---|---|
| committer | Kit La Touche <kit@transneptune.net> | 2024-11-09 22:55:22 -0500 |
| commit | 24eb775ba77f5a6a78a299d9fdffb34f8f167f8d (patch) | |
| tree | 32ab5163d55688dd90dc796aa44d94fec0b35c81 /ui/lib | |
| parent | 91ce856f63bd1d7a188488476bdbec60b5bd58ff (diff) | |
| parent | a417c62edd4d3c07ba37b01835e89ed650489e09 (diff) | |
Merge branch 'main' into wip/touch-events
Diffstat (limited to 'ui/lib')
| -rw-r--r-- | ui/lib/apiServer.js | 6 | ||||
| -rw-r--r-- | ui/lib/components/ActiveChannel.svelte | 43 | ||||
| -rw-r--r-- | ui/lib/components/ChangePassword.svelte | 60 | ||||
| -rw-r--r-- | ui/lib/components/Invite.svelte | 5 | ||||
| -rw-r--r-- | ui/lib/components/Invites.svelte | 13 | ||||
| -rw-r--r-- | ui/lib/components/LogOut.svelte | 18 | ||||
| -rw-r--r-- | ui/lib/components/MessageInput.svelte | 5 | ||||
| -rw-r--r-- | ui/lib/components/MessageRun.svelte | 2 | ||||
| -rw-r--r-- | ui/lib/store.js | 2 | ||||
| -rw-r--r-- | ui/lib/store/messages.js | 40 | ||||
| -rw-r--r-- | ui/lib/store/messages.svelte.js | 74 |
11 files changed, 176 insertions, 92 deletions
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..ba62d6c 100644 --- a/ui/lib/components/ActiveChannel.svelte +++ b/ui/lib/components/ActiveChannel.svelte @@ -1,42 +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]} - <div> - <MessageRun {sender} {messages} /> - </div> - {/each} -</div> - -<style> - .container { - overflow: auto; - } -</style> +{#each messageRuns as { sender, messages }} + <div> + <MessageRun {sender} {messages} /> + </div> +{/each} diff --git a/ui/lib/components/ChangePassword.svelte b/ui/lib/components/ChangePassword.svelte new file mode 100644 index 0000000..1e48bee --- /dev/null +++ b/ui/lib/components/ChangePassword.svelte @@ -0,0 +1,60 @@ +<script> + import { changePassword } from '$lib/apiServer.js'; + + let currentPassword = $state(''), + newPassword = $state(''), + confirmPassword = $state(''), + pending = $state(false), + form; + let valid = $derived(newPassword === confirmPassword && newPassword !== currentPassword); + let disabled = $derived(pending || !valid); + + async function onsubmit(event) { + event.preventDefault(); + pending = true; + let response = await changePassword(currentPassword, newPassword); + switch (response.status) { + case 200: + form.reset(); + break; + } + pending = false; + } +</script> + +<form {onsubmit} bind:this={form}> + <label + >current password + <input + class="input" + name="currentPassword" + type="password" + placeholder="password" + bind:value={currentPassword} + /> + </label> + + <label + >new password + <input + class="input" + name="newPassword" + type="password" + placeholder="password" + bind:value={newPassword} + /> + </label> + + <label + >confirm new password + <input + class="input" + name="confirmPassword" + type="password" + placeholder="password" + bind:value={confirmPassword} + /> + </label> + + <button class="btn bg-orange-500 mt-4" type="submit" {disabled}>change password</button> +</form> diff --git a/ui/lib/components/Invite.svelte b/ui/lib/components/Invite.svelte index 35e00b4..937c911 100644 --- a/ui/lib/components/Invite.svelte +++ b/ui/lib/components/Invite.svelte @@ -5,8 +5,5 @@ let inviteUrl = $derived(new URL(`/invite/${id}`, document.location)); </script> -<button - class="border-slate-500 border-solid border-2 font-bold p-1 rounded" - use:clipboard={inviteUrl}>Copy</button -> +<button class="btn bg-secondary-500" use:clipboard={inviteUrl}>copy</button> <span data-clipboard="inviteUrl">{inviteUrl}</span> diff --git a/ui/lib/components/Invites.svelte b/ui/lib/components/Invites.svelte index cc14f3b..493bf1c 100644 --- a/ui/lib/components/Invites.svelte +++ b/ui/lib/components/Invites.svelte @@ -4,7 +4,7 @@ let invites = $state([]); - async function onSubmit(event) { + async function onsubmit(event) { event.preventDefault(); let response = await createInvite(); if (response.status == 200) { @@ -13,11 +13,12 @@ } </script> -<ul> +<form {onsubmit}> + <button class="btn bg-primary-500" type="submit">create invitation</button> +</form> + +<ul class="mt-4"> {#each invites as invite} - <li><Invite id={invite.id} /></li> + <li class="my-1"><Invite id={invite.id} /></li> {/each} </ul> -<form onsubmit={onSubmit}> - <button class="btn variant-filled" type="submit"> Create Invitation </button> -</form> diff --git a/ui/lib/components/LogOut.svelte b/ui/lib/components/LogOut.svelte new file mode 100644 index 0000000..25dd5e9 --- /dev/null +++ b/ui/lib/components/LogOut.svelte @@ -0,0 +1,18 @@ +<script> + import { goto } from '$app/navigation'; + import { logOut } from '$lib/apiServer.js'; + import { currentUser } from '$lib/store'; + + async function onsubmit(event) { + event.preventDefault(); + const response = await logOut(); + if (200 <= response.status && response.status < 300) { + currentUser.update(() => null); + goto('/login'); + } + } +</script> + +<form {onsubmit}> + <button class="btn bg-orange-400" type="submit">log out</button> +</form> diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte index 907391c..26521e1 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); } } @@ -30,7 +31,7 @@ bind:value {disabled} type="search" - class="flex-auto h-6 input rounded-r-none" + class="flex-auto h-6 py-0 input rounded-r-none text-nowrap" ></textarea> <button color="primary variant-filled-secondary" diff --git a/ui/lib/components/MessageRun.svelte b/ui/lib/components/MessageRun.svelte index b71e972..2e8c613 100644 --- a/ui/lib/components/MessageRun.svelte +++ b/ui/lib/components/MessageRun.svelte @@ -9,7 +9,7 @@ </script> <div - class="card m-4 px-4 py-1 relative" + class="card my-4 px-4 py-1 relative" class:own-message={ownMessage} class:other-message={!ownMessage} > 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..c0db71b --- /dev/null +++ b/ui/lib/store/messages.svelte.js @@ -0,0 +1,74 @@ +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 }; + + // You might be thinking, can't this be + // + // let runs = (this.channels[channel] ||= []); + // + // Let me tell you, I thought that too. Javascript's semantics allow it. It + // didn't work - the first message in each channel was getting lost as the + // update to `this.channels` wasn't actually happening. I suspect this is + // due to the implementation of Svelte's `$state` rune, but I don't know it + // for sure. + // + // In any case, splitting the read and write up like this has the same + // semantics, and _works_. (This time, for sure!) + 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); + } + } + + this.channels[channel] = runs; + + 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; + } +} |
