summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/app.html18
-rw-r--r--ui/lib/apiServer.js148
-rw-r--r--ui/lib/components/ActiveChannel.svelte62
-rw-r--r--ui/lib/components/Channel.svelte10
-rw-r--r--ui/lib/components/ChannelList.svelte14
-rw-r--r--ui/lib/components/CreateChannelForm.svelte42
-rw-r--r--ui/lib/components/CurrentUser.svelte34
-rw-r--r--ui/lib/components/Invite.svelte10
-rw-r--r--ui/lib/components/Invites.svelte28
-rw-r--r--ui/lib/components/LogIn.svelte68
-rw-r--r--ui/lib/components/Message.svelte44
-rw-r--r--ui/lib/components/MessageInput.svelte60
-rw-r--r--ui/lib/components/MessageRun.svelte28
-rw-r--r--ui/lib/store/channels.js60
-rw-r--r--ui/lib/store/logins.js34
-rw-r--r--ui/lib/store/messages.js64
-rw-r--r--ui/routes/(app)/+layout.svelte202
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte24
-rw-r--r--ui/routes/(app)/me/+page.svelte102
-rw-r--r--ui/routes/(login)/invite/[invite]/+page.js32
-rw-r--r--ui/routes/(login)/invite/[invite]/+page.svelte52
-rw-r--r--ui/routes/(login)/login/+page.svelte36
-rw-r--r--ui/routes/(login)/setup/+page.svelte36
-rw-r--r--ui/routes/+layout.svelte66
24 files changed, 637 insertions, 637 deletions
diff --git a/ui/app.html b/ui/app.html
index 63eb917..51a6780 100644
--- a/ui/app.html
+++ b/ui/app.html
@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en" class="dark">
- <head>
- <meta charset="utf-8" />
- <link rel="icon" href="%sveltekit.assets%/favicon.png" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- %sveltekit.head%
- </head>
- <body data-sveltekit-preload-data="hover" data-theme="skeleton">
- <div style="display: contents">%sveltekit.body%</div>
- </body>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ %sveltekit.head%
+ </head>
+ <body data-sveltekit-preload-data="hover" data-theme="skeleton">
+ <div style="display: contents">%sveltekit.body%</div>
+ </body>
</html>
diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js
index e537abc..a6fdaa6 100644
--- a/ui/lib/apiServer.js
+++ b/ui/lib/apiServer.js
@@ -2,120 +2,120 @@ import axios from 'axios';
import { channelsList, logins, messages } from '$lib/store';
export const apiServer = axios.create({
- baseURL: '/api/',
- validateStatus: () => true
+ baseURL: '/api/',
+ validateStatus: () => true
});
export async function boot() {
- return apiServer.get('/boot');
+ return apiServer.get('/boot');
}
export async function setup(name, password) {
- return apiServer.post('/setup', { name, password });
+ return apiServer.post('/setup', { name, password });
}
export async function logIn(name, password) {
- return apiServer.post('/auth/login', { name, password });
+ return apiServer.post('/auth/login', { name, password });
}
export async function logOut() {
- return apiServer.post('/auth/logout', {});
+ return apiServer.post('/auth/logout', {});
}
export async function changePassword(password, to) {
- return apiServer.post('/password', { password, to });
+ return apiServer.post('/password', { password, to });
}
export async function createChannel(name) {
- return apiServer.post('/channels', { name });
+ return apiServer.post('/channels', { name });
}
export async function postToChannel(channelId, body) {
- return apiServer.post(`/channels/${channelId}`, { body });
+ return apiServer.post(`/channels/${channelId}`, { body });
}
export async function createInvite() {
- return apiServer.post(`/invite`, {});
+ return apiServer.post(`/invite`, {});
}
export async function getInvite(inviteId) {
- return apiServer.get(`/invite/${inviteId}`);
+ return apiServer.get(`/invite/${inviteId}`);
}
export async function acceptInvite(inviteId, username, password) {
- const data = {
- name: username,
- password
- };
- return apiServer.post(`/invite/${inviteId}`, data);
+ const data = {
+ name: username,
+ password
+ };
+ return apiServer.post(`/invite/${inviteId}`, data);
}
export function subscribeToEvents(resume_point) {
- const eventsUrl = new URL('/api/events', window.location);
- eventsUrl.searchParams.append('resume_point', resume_point);
- 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
- // infinite browser memory.
- /*
- * Known message types as of now:
- * - created: a channel is created.
- * - action: ignore.
- * - message: a message is created.
- * - action: display message in channel.
- * - message_deleted: a message is deleted.
- * - action: replace message with <...>.
- * - deleted: a channel is deleted.
- * - action: remove channel from sidebar.
- */
- evtSource.onmessage = (evt) => {
- const data = JSON.parse(evt.data);
-
- switch (data.type) {
- case 'login':
- onLoginEvent(data);
- break;
- case 'channel':
- onChannelEvent(data);
- break;
- case 'message':
- onMessageEvent(data);
- break;
- }
- };
-
- return evtSource;
+ const eventsUrl = new URL('/api/events', window.location);
+ eventsUrl.searchParams.append('resume_point', resume_point);
+ 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
+ // infinite browser memory.
+ /*
+ * Known message types as of now:
+ * - created: a channel is created.
+ * - action: ignore.
+ * - message: a message is created.
+ * - action: display message in channel.
+ * - message_deleted: a message is deleted.
+ * - action: replace message with <...>.
+ * - deleted: a channel is deleted.
+ * - action: remove channel from sidebar.
+ */
+ evtSource.onmessage = (evt) => {
+ const data = JSON.parse(evt.data);
+
+ switch (data.type) {
+ case 'login':
+ onLoginEvent(data);
+ break;
+ case 'channel':
+ onChannelEvent(data);
+ break;
+ case 'message':
+ onMessageEvent(data);
+ break;
+ }
+ };
+
+ return evtSource;
}
function onLoginEvent(data) {
- switch (data.event) {
- case 'created':
- logins.update((value) => value.addLogin(data.id, data.name));
- break;
- }
+ switch (data.event) {
+ case 'created':
+ logins.update((value) => value.addLogin(data.id, data.name));
+ break;
+ }
}
function onChannelEvent(data) {
- switch (data.event) {
- case 'created':
- channelsList.update((value) => value.addChannel(data.id, data.name));
- break;
- case 'deleted':
- channelsList.update((value) => value.deleteChannel(data.id));
- messages.update((value) => value.deleteChannel(data.id));
- break;
- }
+ switch (data.event) {
+ case 'created':
+ channelsList.update((value) => value.addChannel(data.id, data.name));
+ break;
+ case 'deleted':
+ channelsList.update((value) => value.deleteChannel(data.id));
+ messages.update((value) => value.deleteChannel(data.id));
+ break;
+ }
}
function onMessageEvent(data) {
- switch (data.event) {
- case 'sent':
- messages.update((value) =>
- value.addMessage(data.channel, data.id, data.at, data.sender, data.body)
- );
- break;
- case 'deleted':
- messages.update((value) => value.deleteMessage(data.id));
- break;
- }
+ switch (data.event) {
+ case 'sent':
+ messages.update((value) =>
+ value.addMessage(data.channel, data.id, data.at, data.sender, data.body)
+ );
+ break;
+ case 'deleted':
+ messages.update((value) => value.deleteMessage(data.id));
+ break;
+ }
}
diff --git a/ui/lib/components/ActiveChannel.svelte b/ui/lib/components/ActiveChannel.svelte
index ac1a0b9..a4ccd24 100644
--- a/ui/lib/components/ActiveChannel.svelte
+++ b/ui/lib/components/ActiveChannel.svelte
@@ -1,42 +1,42 @@
<script>
- import { messages } from '$lib/store';
- import MessageRun from './MessageRun.svelte';
+ import { messages } from '$lib/store';
+ import MessageRun from './MessageRun.svelte';
- let { channel } = $props();
- let messageList = $derived(channel !== null ? $messages.inChannel(channel) : []);
+ 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];
- }
+ 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];
- }
- }
+ chunk = [x];
+ key = newKey;
+ } else {
+ chunk.push(x);
+ }
+ }
+ if (chunk !== undefined) {
+ yield [key, chunk];
+ }
+ }
</script>
<div class="container">
- {#each chunkBy(messageList, (msg) => msg.sender) as [sender, messages]}
- <div>
- <MessageRun {sender} {messages} />
- </div>
- {/each}
+ {#each chunkBy(messageList, (msg) => msg.sender) as [sender, messages]}
+ <div>
+ <MessageRun {sender} {messages} />
+ </div>
+ {/each}
</div>
<style>
- .container {
- overflow: auto;
- }
+ .container {
+ overflow: auto;
+ }
</style>
diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte
index fdc5d56..e84c6d0 100644
--- a/ui/lib/components/Channel.svelte
+++ b/ui/lib/components/Channel.svelte
@@ -1,10 +1,10 @@
<script>
- let { id, name, active } = $props();
+ let { id, name, active } = $props();
</script>
<li class="rounded-full" class:bg-slate-400={active}>
- <a href="/ch/{id}">
- <span class="badge bg-primary-500">#</span>
- <span class="flex-auto">{name}</span>
- </a>
+ <a href="/ch/{id}">
+ <span class="badge bg-primary-500">#</span>
+ <span class="flex-auto">{name}</span>
+ </a>
</li>
diff --git a/ui/lib/components/ChannelList.svelte b/ui/lib/components/ChannelList.svelte
index 9d1227e..51dd6cf 100644
--- a/ui/lib/components/ChannelList.svelte
+++ b/ui/lib/components/ChannelList.svelte
@@ -1,13 +1,13 @@
<script>
- import Channel from './Channel.svelte';
+ import Channel from './Channel.svelte';
- let { channels, active } = $props();
+ let { channels, active } = $props();
</script>
<nav class="list-nav">
- <ul>
- {#each channels as channel}
- <Channel {...channel} active={active === channel.id} />
- {/each}
- </ul>
+ <ul>
+ {#each channels as channel}
+ <Channel {...channel} active={active === channel.id} />
+ {/each}
+ </ul>
</nav>
diff --git a/ui/lib/components/CreateChannelForm.svelte b/ui/lib/components/CreateChannelForm.svelte
index ac43925..afa3f78 100644
--- a/ui/lib/components/CreateChannelForm.svelte
+++ b/ui/lib/components/CreateChannelForm.svelte
@@ -1,30 +1,30 @@
<script>
- import { createChannel } from '$lib/apiServer';
+ import { createChannel } from '$lib/apiServer';
- let name = $state('');
- let pending = false;
- let disabled = $derived(pending);
+ let name = $state('');
+ let pending = false;
+ let disabled = $derived(pending);
- async function handleSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await createChannel(name);
- if (200 <= response.status && response.status < 300) {
- name = '';
- }
- pending = false;
- }
+ async function handleSubmit(event) {
+ event.preventDefault();
+ pending = true;
+ const response = await createChannel(name);
+ if (200 <= response.status && response.status < 300) {
+ name = '';
+ }
+ pending = false;
+ }
</script>
<form onsubmit={handleSubmit} class="form form-row flex-nowrap">
- <input
- type="text"
- placeholder="create channel"
- bind:value={name}
- {disabled}
- class="input flex-auto h-6 w-9/12"
- />
- <button type="submit" class="flex-none w-6 h-6">&#x2795;</button>
+ <input
+ type="text"
+ placeholder="create channel"
+ bind:value={name}
+ {disabled}
+ class="input flex-auto h-6 w-9/12"
+ />
+ <button type="submit" class="flex-none w-6 h-6">&#x2795;</button>
</form>
<style>
diff --git a/ui/lib/components/CurrentUser.svelte b/ui/lib/components/CurrentUser.svelte
index 46c76b0..56bf915 100644
--- a/ui/lib/components/CurrentUser.svelte
+++ b/ui/lib/components/CurrentUser.svelte
@@ -1,25 +1,25 @@
<script>
- import { goto } from '$app/navigation';
- import { logOut } from '$lib/apiServer';
- import { currentUser } from '$lib/store';
+ import { goto } from '$app/navigation';
+ import { logOut } from '$lib/apiServer';
+ import { currentUser } from '$lib/store';
- async function handleLogout(event) {
- event.preventDefault();
- const response = await logOut();
- if (200 <= response.status && response.status < 300) {
- currentUser.update(() => null);
- goto('/login');
- }
- }
+ async function handleLogout(event) {
+ event.preventDefault();
+ const response = await logOut();
+ if (200 <= response.status && response.status < 300) {
+ currentUser.update(() => null);
+ goto('/login');
+ }
+ }
</script>
<form onsubmit={handleLogout}>
- {#if $currentUser}
- <a href="/me">@{$currentUser.username}</a>
- {/if}
- <button class="border-slate-500 border-solid border-2 font-bold p-1 rounded" type="submit"
- >log out</button
- >
+ {#if $currentUser}
+ <a href="/me">@{$currentUser.username}</a>
+ {/if}
+ <button class="border-slate-500 border-solid border-2 font-bold p-1 rounded" type="submit"
+ >log out</button
+ >
</form>
<style>
diff --git a/ui/lib/components/Invite.svelte b/ui/lib/components/Invite.svelte
index 79fe087..35e00b4 100644
--- a/ui/lib/components/Invite.svelte
+++ b/ui/lib/components/Invite.svelte
@@ -1,12 +1,12 @@
<script>
- import { clipboard } from '@skeletonlabs/skeleton';
+ import { clipboard } from '@skeletonlabs/skeleton';
- let { id } = $props();
- let inviteUrl = $derived(new URL(`/invite/${id}`, document.location));
+ let { id } = $props();
+ 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
+ class="border-slate-500 border-solid border-2 font-bold p-1 rounded"
+ 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 337ee7e..cc14f3b 100644
--- a/ui/lib/components/Invites.svelte
+++ b/ui/lib/components/Invites.svelte
@@ -1,23 +1,23 @@
<script>
- import { createInvite } from '$lib/apiServer';
- import Invite from '$lib/components/Invite.svelte';
+ import { createInvite } from '$lib/apiServer';
+ import Invite from '$lib/components/Invite.svelte';
- let invites = $state([]);
+ let invites = $state([]);
- async function onSubmit(event) {
- event.preventDefault();
- let response = await createInvite();
- if (response.status == 200) {
- invites.push(response.data);
- }
- }
+ async function onSubmit(event) {
+ event.preventDefault();
+ let response = await createInvite();
+ if (response.status == 200) {
+ invites.push(response.data);
+ }
+ }
</script>
<ul>
- {#each invites as invite}
- <li><Invite id={invite.id} /></li>
- {/each}
+ {#each invites as invite}
+ <li><Invite id={invite.id} /></li>
+ {/each}
</ul>
<form onsubmit={onSubmit}>
- <button class="btn variant-filled" type="submit"> Create Invitation </button>
+ <button class="btn variant-filled" type="submit"> Create Invitation </button>
</form>
diff --git a/ui/lib/components/LogIn.svelte b/ui/lib/components/LogIn.svelte
index 4e28abe..7fb91e8 100644
--- a/ui/lib/components/LogIn.svelte
+++ b/ui/lib/components/LogIn.svelte
@@ -1,39 +1,39 @@
<script>
- let {
- username = $bindable(),
- password = $bindable(),
- legend = 'sign in',
- disabled,
- onsubmit
- } = $props();
+ let {
+ username = $bindable(),
+ password = $bindable(),
+ legend = 'sign in',
+ disabled,
+ onsubmit
+ } = $props();
</script>
<div class="card m-4 p-4">
- <form {onsubmit}>
- <label class="label" for="username">
- username
- <input
- class="input"
- name="username"
- type="text"
- placeholder="username"
- bind:value={username}
- {disabled}
- />
- </label>
- <label class="label" for="password">
- password
- <input
- class="input"
- name="password"
- type="password"
- placeholder="password"
- bind:value={password}
- {disabled}
- />
- </label>
- <button class="btn variant-filled" type="submit">
- {legend}
- </button>
- </form>
+ <form {onsubmit}>
+ <label class="label" for="username">
+ username
+ <input
+ class="input"
+ name="username"
+ type="text"
+ placeholder="username"
+ bind:value={username}
+ {disabled}
+ />
+ </label>
+ <label class="label" for="password">
+ password
+ <input
+ class="input"
+ name="password"
+ type="password"
+ placeholder="password"
+ bind:value={password}
+ {disabled}
+ />
+ </label>
+ <button class="btn variant-filled" type="submit">
+ {legend}
+ </button>
+ </form>
</div>
diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte
index f47e9b6..f0e7045 100644
--- a/ui/lib/components/Message.svelte
+++ b/ui/lib/components/Message.svelte
@@ -1,32 +1,32 @@
<script>
- import { marked } from 'marked';
- import DOMPurify from 'dompurify';
+ import { marked } from 'marked';
+ import DOMPurify from 'dompurify';
- function scroll(message) {
- message.scrollIntoView();
- }
+ function scroll(message) {
+ message.scrollIntoView();
+ }
- let { at, body } = $props();
- let renderedBody = $derived(DOMPurify.sanitize(marked.parse(body, { breaks: true })));
+ let { at, body } = $props();
+ let renderedBody = $derived(DOMPurify.sanitize(marked.parse(body, { breaks: true })));
</script>
<div class="message relative">
- <span class="timestamp chip variant-soft absolute top-0 right-0">{at}</span>
- <section use:scroll class="py-1 message-body">
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
- {@html renderedBody}
- </section>
+ <span class="timestamp chip variant-soft absolute top-0 right-0">{at}</span>
+ <section use:scroll class="py-1 message-body">
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
+ {@html renderedBody}
+ </section>
</div>
<style>
- .message .timestamp {
- display: none;
- }
- .message:hover .timestamp {
- display: flex;
- }
- .message-body:empty:after {
- content: '.';
- visibility: hidden;
- }
+ .message .timestamp {
+ display: none;
+ }
+ .message:hover .timestamp {
+ display: flex;
+ }
+ .message-body:empty:after {
+ content: '.';
+ visibility: hidden;
+ }
</style>
diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte
index 220ed3b..907391c 100644
--- a/ui/lib/components/MessageInput.svelte
+++ b/ui/lib/components/MessageInput.svelte
@@ -1,40 +1,40 @@
<script>
- import { postToChannel } from '$lib/apiServer';
+ import { postToChannel } from '$lib/apiServer';
- let { channel } = $props();
+ let { channel } = $props();
- let form;
- let value = $state('');
- let pending = false;
+ let form;
+ let value = $state('');
+ let pending = false;
- let disabled = $derived(pending);
+ let disabled = $derived(pending);
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- await postToChannel(channel, value);
- form.reset();
- pending = false;
- }
+ async function onSubmit(event) {
+ event.preventDefault();
+ pending = true;
+ await postToChannel(channel, value);
+ form.reset();
+ pending = false;
+ }
- function onKeyDown(event) {
- if (!event.altKey && event.key === 'Enter') {
- onSubmit(event);
- }
- }
+ function onKeyDown(event) {
+ if (!event.altKey && event.key === 'Enter') {
+ onSubmit(event);
+ }
+ }
</script>
<form bind:this={form} onsubmit={onSubmit} class="flex flex-row flex-nowrap">
- <textarea
- onkeydown={onKeyDown}
- bind:value
- {disabled}
- type="search"
- class="flex-auto h-6 input rounded-r-none"
- ></textarea>
- <button
- color="primary variant-filled-secondary"
- type="submit"
- class="flex-none w-6 h-6 btn-icon variant-filled rounded-l-none">&raquo;</button
- >
+ <textarea
+ onkeydown={onKeyDown}
+ bind:value
+ {disabled}
+ type="search"
+ class="flex-auto h-6 input rounded-r-none"
+ ></textarea>
+ <button
+ color="primary variant-filled-secondary"
+ type="submit"
+ class="flex-none w-6 h-6 btn-icon variant-filled rounded-l-none">&raquo;</button
+ >
</form>
diff --git a/ui/lib/components/MessageRun.svelte b/ui/lib/components/MessageRun.svelte
index 23c2186..b3e3eee 100644
--- a/ui/lib/components/MessageRun.svelte
+++ b/ui/lib/components/MessageRun.svelte
@@ -1,22 +1,22 @@
<script>
- import { logins, currentUser } from '$lib/store';
- import Message from '$lib/components/Message.svelte';
+ import { logins, currentUser } from '$lib/store';
+ import Message from '$lib/components/Message.svelte';
- let { sender, messages } = $props();
+ let { sender, messages } = $props();
- let name = $derived($logins.get(sender));
- let ownMessage = $derived($currentUser !== null && $currentUser.id == sender);
+ let name = $derived($logins.get(sender));
+ let ownMessage = $derived($currentUser !== null && $currentUser.id == sender);
</script>
<div
- class="card card-hover m-4 px-4 py-1 relative"
- class:own-message={ownMessage}
- class:other-message={!ownMessage}
+ class="card card-hover m-4 px-4 py-1 relative"
+ class:own-message={ownMessage}
+ class:other-message={!ownMessage}
>
- <span class="chip variant-soft sticky top-o left-0">
- @{name}:
- </span>
- {#each messages as { at, body }}
- <Message {at} {body} />
- {/each}
+ <span class="chip variant-soft sticky top-o left-0">
+ @{name}:
+ </span>
+ {#each messages as { at, body }}
+ <Message {at} {body} />
+ {/each}
</div>
diff --git a/ui/lib/store/channels.js b/ui/lib/store/channels.js
index b57ca7e..37dc673 100644
--- a/ui/lib/store/channels.js
+++ b/ui/lib/store/channels.js
@@ -1,36 +1,36 @@
export class Channels {
- constructor() {
- this.channels = [];
- }
+ constructor() {
+ this.channels = [];
+ }
- setChannels(channels) {
- this.channels = [...channels];
- this.sort();
- return this;
- }
+ setChannels(channels) {
+ this.channels = [...channels];
+ this.sort();
+ return this;
+ }
- addChannel(id, name) {
- this.channels = [...this.channels, { id, name }];
- 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;
- }
+ 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;
- });
- }
+ 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/logins.js b/ui/lib/store/logins.js
index 5b45206..d449b3a 100644
--- a/ui/lib/store/logins.js
+++ b/ui/lib/store/logins.js
@@ -1,22 +1,22 @@
export class Logins {
- constructor() {
- this.logins = {};
- }
+ constructor() {
+ this.logins = {};
+ }
- addLogin(id, name) {
- this.logins[id] = name;
- return this;
- }
+ addLogin(id, name) {
+ this.logins[id] = name;
+ return this;
+ }
- setLogins(logins) {
- this.logins = {};
- for (let { id, name } of logins) {
- this.addLogin(id, name);
- }
- return this;
- }
+ setLogins(logins) {
+ this.logins = {};
+ for (let { id, name } of logins) {
+ this.addLogin(id, name);
+ }
+ return this;
+ }
- get(id) {
- return this.logins[id];
- }
+ get(id) {
+ return this.logins[id];
+ }
}
diff --git a/ui/lib/store/messages.js b/ui/lib/store/messages.js
index 884b296..62c567a 100644
--- a/ui/lib/store/messages.js
+++ b/ui/lib/store/messages.js
@@ -1,40 +1,40 @@
export class Messages {
- constructor() {
- this.channels = {};
- }
+ constructor() {
+ this.channels = {};
+ }
- inChannel(channel) {
- return (this.channels[channel] = this.channels[channel] || []);
- }
+ 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;
- }
+ 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;
- }
+ 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;
- }
+ 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;
- }
+ 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;
- }
+ 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/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte
index 356a096..94e4509 100644
--- a/ui/routes/(app)/+layout.svelte
+++ b/ui/routes/(app)/+layout.svelte
@@ -1,122 +1,122 @@
<script>
- import { page } from '$app/stores';
- import { goto } from '$app/navigation';
- import { onMount, onDestroy } from 'svelte';
+ import { page } from '$app/stores';
+ import { goto } from '$app/navigation';
+ import { onMount, onDestroy } from 'svelte';
- import { boot, subscribeToEvents } from '$lib/apiServer';
- import { showMenu, currentUser, logins, channelsList, messages } from '$lib/store';
+ import { boot, subscribeToEvents } from '$lib/apiServer';
+ import { showMenu, currentUser, logins, channelsList, messages } from '$lib/store';
- import ChannelList from '$lib/components/ChannelList.svelte';
- import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
+ import ChannelList from '$lib/components/ChannelList.svelte';
+ import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
- let events = null;
+ let events = null;
- let loading = $state(true);
- let channel = $derived($page.params.channel);
+ let loading = $state(true);
+ let channel = $derived($page.params.channel);
- function onBooted(boot) {
- currentUser.update(() => ({
- id: boot.login.id,
- username: boot.login.name
- }));
- logins.update((value) => value.setLogins(boot.logins));
- channelsList.update((value) => value.setChannels(boot.channels));
- messages.update((value) => value.setMessages(boot.messages));
- }
+ function onBooted(boot) {
+ currentUser.update(() => ({
+ id: boot.login.id,
+ username: boot.login.name
+ }));
+ logins.update((value) => value.setLogins(boot.logins));
+ channelsList.update((value) => value.setChannels(boot.channels));
+ messages.update((value) => value.setMessages(boot.messages));
+ }
- onMount(async () => {
- let response = await boot();
- switch (response.status) {
- case 200:
- onBooted(response.data);
- events = subscribeToEvents(response.data.resume_point);
- break;
- case 401:
- currentUser.update(() => null);
- goto('/login');
- break;
- case 503:
- currentUser.update(() => null);
- goto('/setup');
- break;
- default:
- // TODO: display error.
- break;
- }
- loading = false;
- });
+ onMount(async () => {
+ let response = await boot();
+ switch (response.status) {
+ case 200:
+ onBooted(response.data);
+ events = subscribeToEvents(response.data.resume_point);
+ break;
+ case 401:
+ currentUser.update(() => null);
+ goto('/login');
+ break;
+ case 503:
+ currentUser.update(() => null);
+ goto('/setup');
+ break;
+ default:
+ // TODO: display error.
+ break;
+ }
+ loading = false;
+ });
- onDestroy(async () => {
- if (events !== null) {
- events.close();
- }
- });
+ onDestroy(async () => {
+ if (events !== null) {
+ events.close();
+ }
+ });
</script>
<svelte:head>
- <title>understory</title>
+ <title>understory</title>
</svelte:head>
{#if loading}
- <h2>Loading&hellip;</h2>
+ <h2>Loading&hellip;</h2>
{:else}
- <div id="interface" class="p-2">
- <nav id="sidebar" data-expanded={$showMenu}>
- <div class="channel-list">
- <ChannelList active={channel} channels={$channelsList.channels} />
- </div>
- <div class="create-channel">
- <CreateChannelForm />
- </div>
- </nav>
- <main>
- <slot />
- </main>
- </div>
+ <div id="interface" class="p-2">
+ <nav id="sidebar" data-expanded={$showMenu}>
+ <div class="channel-list">
+ <ChannelList active={channel} channels={$channelsList.channels} />
+ </div>
+ <div class="create-channel">
+ <CreateChannelForm />
+ </div>
+ </nav>
+ <main>
+ <slot />
+ </main>
+ </div>
{/if}
<style>
- :root {
- --app-bar-height: 68px;
- --input-row-height: 2rem;
- --interface-padding: 16px;
- }
+ :root {
+ --app-bar-height: 68px;
+ --input-row-height: 2rem;
+ --interface-padding: 16px;
+ }
- #interface {
- margin: unset;
- display: grid;
- grid-template:
- 'side main' 1fr
- / auto 1fr;
- height: calc(100vh - var(--app-bar-height));
+ #interface {
+ margin: unset;
+ display: grid;
+ grid-template:
+ 'side main' 1fr
+ / auto 1fr;
+ height: calc(100vh - var(--app-bar-height));
- @media (width > 640px) {
- --overlay: static;
- --translate: 0;
- }
- }
- nav {
- grid-area: side;
- background-color: rgb(var(--color-surface-800));
- inset: auto auto 0 0;
- padding: 0.25rem;
- position: var(--overlay, absolute);
- transition: translate 300ms ease-out;
- width: 21rem;
- height: calc(100vh - var(--app-bar-height) - var(--interface-padding));
- z-index: 10;
- }
- main {
- grid-area: main;
- height: calc(100vh - var(--app-bar-height) - var(--interface-padding));
- }
- .channel-list {
- height: calc(
- 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height)
- );
- overflow: auto;
- }
- nav[data-expanded='false'] {
- translate: var(--translate, -100% 0);
- }
+ @media (width > 640px) {
+ --overlay: static;
+ --translate: 0;
+ }
+ }
+ nav {
+ grid-area: side;
+ background-color: rgb(var(--color-surface-800));
+ inset: auto auto 0 0;
+ padding: 0.25rem;
+ position: var(--overlay, absolute);
+ transition: translate 300ms ease-out;
+ width: 21rem;
+ height: calc(100vh - var(--app-bar-height) - var(--interface-padding));
+ z-index: 10;
+ }
+ main {
+ grid-area: main;
+ height: calc(100vh - var(--app-bar-height) - var(--interface-padding));
+ }
+ .channel-list {
+ height: calc(
+ 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height)
+ );
+ overflow: auto;
+ }
+ nav[data-expanded='false'] {
+ translate: var(--translate, -100% 0);
+ }
</style>
diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte
index a5836fc..49c1c29 100644
--- a/ui/routes/(app)/ch/[channel]/+page.svelte
+++ b/ui/routes/(app)/ch/[channel]/+page.svelte
@@ -1,23 +1,23 @@
<script>
- import { page } from '$app/stores';
- import ActiveChannel from '$lib/components/ActiveChannel.svelte';
- import MessageInput from '$lib/components/MessageInput.svelte';
+ import { page } from '$app/stores';
+ import ActiveChannel from '$lib/components/ActiveChannel.svelte';
+ import MessageInput from '$lib/components/MessageInput.svelte';
- let channel = $derived($page.params.channel);
+ let channel = $derived($page.params.channel);
</script>
<div class="active-channel">
- <ActiveChannel {channel} />
+ <ActiveChannel {channel} />
</div>
<div class="create-message max-h-full">
- <MessageInput {channel} />
+ <MessageInput {channel} />
</div>
<style>
- .active-channel {
- height: calc(
- 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height)
- );
- overflow: auto;
- }
+ .active-channel {
+ height: calc(
+ 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height)
+ );
+ overflow: auto;
+ }
</style>
diff --git a/ui/routes/(app)/me/+page.svelte b/ui/routes/(app)/me/+page.svelte
index 4531a91..30da6f0 100644
--- a/ui/routes/(app)/me/+page.svelte
+++ b/ui/routes/(app)/me/+page.svelte
@@ -1,64 +1,64 @@
<script>
- import { changePassword } from '$lib/apiServer.js';
+ import { changePassword } from '$lib/apiServer.js';
- import Invites from '$lib/components/Invites.svelte';
+ import Invites from '$lib/components/Invites.svelte';
- let currentPassword = $state(''),
- newPassword = $state(''),
- confirmPassword = $state(''),
- passwordForm;
- let pending = $state(false);
- let valid = $derived(newPassword === confirmPassword && newPassword !== currentPassword);
- let disabled = $derived(pending || !valid);
+ let currentPassword = $state(''),
+ newPassword = $state(''),
+ confirmPassword = $state(''),
+ passwordForm;
+ let pending = $state(false);
+ let valid = $derived(newPassword === confirmPassword && newPassword !== currentPassword);
+ let disabled = $derived(pending || !valid);
- async function onPasswordChange(event) {
- event.preventDefault();
- pending = true;
- let response = await changePassword(currentPassword, newPassword);
- switch (response.status) {
- case 200:
- passwordForm.reset();
- break;
- }
- pending = false;
- }
+ async function onPasswordChange(event) {
+ event.preventDefault();
+ pending = true;
+ let response = await changePassword(currentPassword, newPassword);
+ switch (response.status) {
+ case 200:
+ passwordForm.reset();
+ break;
+ }
+ pending = false;
+ }
</script>
<form onsubmit={onPasswordChange} bind:this={passwordForm}>
- <label
- >current password
- <input
- class="input"
- name="currentPassword"
- type="password"
- placeholder="password"
- bind:value={currentPassword}
- />
- </label>
+ <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
+ >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>
+ <label
+ >confirm new password
+ <input
+ class="input"
+ name="confirmPassword"
+ type="password"
+ placeholder="password"
+ bind:value={confirmPassword}
+ />
+ </label>
- <button class="btn variant-filled" type="submit" {disabled}> change password </button>
+ <button class="btn variant-filled" type="submit" {disabled}> change password </button>
</form>
<Invites />
diff --git a/ui/routes/(login)/invite/[invite]/+page.js b/ui/routes/(login)/invite/[invite]/+page.js
index 32c9290..5d526df 100644
--- a/ui/routes/(login)/invite/[invite]/+page.js
+++ b/ui/routes/(login)/invite/[invite]/+page.js
@@ -1,23 +1,23 @@
import { getInvite } from '$lib/apiServer';
async function loadInvite(invite) {
- let response = await getInvite(invite);
- switch (response.status) {
- case 200: {
- let invite = response.data;
- return invite;
- }
- case 404:
- return null;
- default:
- // TODO: display error.
- break;
- }
+ let response = await getInvite(invite);
+ switch (response.status) {
+ case 200: {
+ let invite = response.data;
+ return invite;
+ }
+ case 404:
+ return null;
+ default:
+ // TODO: display error.
+ break;
+ }
}
export function load({ params }) {
- let { invite } = params;
- return {
- invite: loadInvite(invite)
- };
+ let { invite } = params;
+ return {
+ invite: loadInvite(invite)
+ };
}
diff --git a/ui/routes/(login)/invite/[invite]/+page.svelte b/ui/routes/(login)/invite/[invite]/+page.svelte
index 65f5a97..18bf437 100644
--- a/ui/routes/(login)/invite/[invite]/+page.svelte
+++ b/ui/routes/(login)/invite/[invite]/+page.svelte
@@ -1,36 +1,36 @@
<script>
- import { goto } from '$app/navigation';
- import { acceptInvite } from '$lib/apiServer';
+ import { goto } from '$app/navigation';
+ import { acceptInvite } from '$lib/apiServer';
- import LogIn from '$lib/components/LogIn.svelte';
+ import LogIn from '$lib/components/LogIn.svelte';
- let { data } = $props();
+ let { data } = $props();
- let username = $state(''),
- password = $state('');
- let pending = false;
- let disabled = $derived(pending);
+ let username = $state(''),
+ password = $state('');
+ let pending = false;
+ let disabled = $derived(pending);
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await acceptInvite(data.invite.id, username, password);
- if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
- goto('/');
- }
- pending = false;
- }
+ async function onSubmit(event) {
+ event.preventDefault();
+ pending = true;
+ const response = await acceptInvite(data.invite.id, username, password);
+ if (200 <= response.status && response.status < 300) {
+ username = '';
+ password = '';
+ goto('/');
+ }
+ pending = false;
+ }
</script>
{#await data.invite}
- <div class="card m-4 p-4">
- <p>Loading invitation…</p>
- </div>
+ <div class="card m-4 p-4">
+ <p>Loading invitation…</p>
+ </div>
{:then invite}
- <div class="card m-4 p-4">
- <p>Hi there! {invite.issuer} invites you to the conversation.</p>
- </div>
- <LogIn {disabled} bind:username bind:password onsubmit={onSubmit} />
+ <div class="card m-4 p-4">
+ <p>Hi there! {invite.issuer} invites you to the conversation.</p>
+ </div>
+ <LogIn {disabled} bind:username bind:password onsubmit={onSubmit} />
{/await}
diff --git a/ui/routes/(login)/login/+page.svelte b/ui/routes/(login)/login/+page.svelte
index 77c2d62..a1291ea 100644
--- a/ui/routes/(login)/login/+page.svelte
+++ b/ui/routes/(login)/login/+page.svelte
@@ -1,25 +1,25 @@
<script>
- import { goto } from '$app/navigation';
- import { logIn } from '$lib/apiServer';
+ import { goto } from '$app/navigation';
+ import { logIn } from '$lib/apiServer';
- import LogIn from '$lib/components/LogIn.svelte';
+ import LogIn from '$lib/components/LogIn.svelte';
- let username = '',
- password = '';
- let pending = false;
- $: disabled = pending;
+ let username = '',
+ password = '';
+ let pending = false;
+ $: disabled = pending;
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await logIn(username, password);
- if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
- goto('/');
- }
- pending = false;
- }
+ async function onSubmit(event) {
+ event.preventDefault();
+ pending = true;
+ const response = await logIn(username, password);
+ if (200 <= response.status && response.status < 300) {
+ username = '';
+ password = '';
+ goto('/');
+ }
+ pending = false;
+ }
</script>
<LogIn {disabled} bind:username bind:password onsubmit={onSubmit} />
diff --git a/ui/routes/(login)/setup/+page.svelte b/ui/routes/(login)/setup/+page.svelte
index f95403f..f162ded 100644
--- a/ui/routes/(login)/setup/+page.svelte
+++ b/ui/routes/(login)/setup/+page.svelte
@@ -1,25 +1,25 @@
<script>
- import { goto } from '$app/navigation';
- import { setup } from '$lib/apiServer';
+ import { goto } from '$app/navigation';
+ import { setup } from '$lib/apiServer';
- import LogIn from '$lib/components/LogIn.svelte';
+ import LogIn from '$lib/components/LogIn.svelte';
- let username = $state(''),
- password = $state('');
- let pending = false;
- let disabled = $derived(pending);
+ let username = $state(''),
+ password = $state('');
+ let pending = false;
+ let disabled = $derived(pending);
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await setup(username, password);
- if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
- goto('/');
- }
- pending = false;
- }
+ async function onSubmit(event) {
+ event.preventDefault();
+ pending = true;
+ const response = await setup(username, password);
+ if (200 <= response.status && response.status < 300) {
+ username = '';
+ password = '';
+ goto('/');
+ }
+ pending = false;
+ }
</script>
<LogIn {disabled} bind:username bind:password legend="set up" onsubmit={onSubmit} />
diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte
index e0355dd..0477195 100644
--- a/ui/routes/+layout.svelte
+++ b/ui/routes/+layout.svelte
@@ -1,46 +1,46 @@
<script>
- import { onNavigate } from '$app/navigation';
- import '../app.css';
- import logo from '$lib/assets/logo.png';
+ import { onNavigate } from '$app/navigation';
+ import '../app.css';
+ import logo from '$lib/assets/logo.png';
- import { AppBar } from '@skeletonlabs/skeleton';
- import { showMenu, currentUser } from '$lib/store';
+ import { AppBar } from '@skeletonlabs/skeleton';
+ import { showMenu, currentUser } from '$lib/store';
- import CurrentUser from '$lib/components/CurrentUser.svelte';
+ import CurrentUser from '$lib/components/CurrentUser.svelte';
- function toggleMenu() {
- showMenu.update((value) => !value);
- }
+ function toggleMenu() {
+ showMenu.update((value) => !value);
+ }
- onNavigate(() => {
- showMenu.update(() => false);
- });
+ onNavigate(() => {
+ showMenu.update(() => false);
+ });
</script>
<div id="app" class="m-0 p-0 h-vh w-full">
- <div class="w-full">
- <AppBar class="app-bar">
- <svelte:fragment slot="lead">
- <button on:click|preventDefault={toggleMenu} class="cursor-pointer">
- <img class="w-8 h-8" alt="logo" src={logo} />
- </button>
- </svelte:fragment>
- <a href="/">understory</a>
- <svelte:fragment slot="trail">
- {#if $currentUser}
- <CurrentUser />
- {/if}
- </svelte:fragment>
- </AppBar>
- </div>
+ <div class="w-full">
+ <AppBar class="app-bar">
+ <svelte:fragment slot="lead">
+ <button on:click|preventDefault={toggleMenu} class="cursor-pointer">
+ <img class="w-8 h-8" alt="logo" src={logo} />
+ </button>
+ </svelte:fragment>
+ <a href="/">understory</a>
+ <svelte:fragment slot="trail">
+ {#if $currentUser}
+ <CurrentUser />
+ {/if}
+ </svelte:fragment>
+ </AppBar>
+ </div>
- <slot />
+ <slot />
</div>
<style>
- #app {
- margin: 0;
- height: 100vh;
- width: 100%;
- }
+ #app {
+ margin: 0;
+ height: 100vh;
+ width: 100%;
+ }
</style>