summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorKit La Touche <kit@transneptune.net>2025-02-21 22:18:56 -0500
committerKit La Touche <kit@transneptune.net>2025-02-21 22:53:49 -0500
commit9d1dbac74866a6175c65a25bbd8a3ccbe8cf87e4 (patch)
treef15b3f0695b948e335774aa4d92a5b064a1c0f10 /ui
parent743b59b69857da81b214970ec9252bc918ad243d (diff)
parent36cadfe00cacc6a6523f9862d3f7a08a9d0ce611 (diff)
Merge branch 'main' into prop/preserve-state
Diffstat (limited to 'ui')
-rw-r--r--ui/app.css2
-rw-r--r--ui/lib/apiServer.js77
-rw-r--r--ui/lib/components/Channel.svelte8
-rw-r--r--ui/lib/components/LogOut.svelte2
-rw-r--r--ui/lib/components/MessageInput.svelte3
-rw-r--r--ui/lib/store.js61
-rw-r--r--ui/routes/(app)/+layout.svelte36
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte9
-rw-r--r--ui/routes/(login)/invite/[invite]/+page.svelte2
-rw-r--r--ui/routes/(login)/login/+page.svelte2
-rw-r--r--ui/routes/(login)/setup/+page.svelte2
-rw-r--r--ui/styles/app-bar.css2
-rw-r--r--ui/styles/fonts.css19
-rw-r--r--ui/styles/forms.css4
-rw-r--r--ui/styles/messages.css4
-rw-r--r--ui/styles/reset.css7
-rw-r--r--ui/styles/sidebar.css3
-rw-r--r--ui/styles/textarea.css2
18 files changed, 115 insertions, 130 deletions
diff --git a/ui/app.css b/ui/app.css
index e7da4d8..646c418 100644
--- a/ui/app.css
+++ b/ui/app.css
@@ -14,7 +14,7 @@
body {
background-color: var(--colour-active-channel-bg);
color: var(--dark-text);
- font-family: 'Roboto';
+ font-family: 'Roboto', sans-serif;
}
hr {
diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js
index e52daff..c65b743 100644
--- a/ui/lib/apiServer.js
+++ b/ui/lib/apiServer.js
@@ -1,5 +1,4 @@
import axios from 'axios';
-import { channelsList, logins, messages } from '$lib/store';
export const apiServer = axios.create({
baseURL: '/api/',
@@ -55,75 +54,11 @@ export async function acceptInvite(inviteId, username, password) {
}
export function subscribeToEvents(resumePoint) {
- const eventsUrl = new URL('/api/events', window.location);
- 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
- // 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;
+ const eventsUrl = apiServer.getUri({
+ url: '/events',
+ params: {
+ resume_point: resumePoint
}
- };
-
- return evtSource;
-}
-
-function onLoginEvent(data) {
- 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;
- }
-}
-
-function onMessageEvent(data) {
- switch (data.event) {
- case 'sent':
- messages.update((value) =>
- value.addMessage(data.channel, data.id, {
- at: data.at,
- sender: data.sender,
- body: data.body
- })
- );
- break;
- case 'deleted':
- messages.update((value) => value.deleteMessage(data.id));
- break;
- }
+ });
+ return new EventSource(eventsUrl);
}
diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte
index c73340f..4f908d2 100644
--- a/ui/lib/components/Channel.svelte
+++ b/ui/lib/components/Channel.svelte
@@ -2,13 +2,13 @@
let { id, name, active, hasUnreads } = $props();
</script>
-<a href="/ch/{id}">
- <li class:active>
+<li class:active>
+ <a href="/ch/{id}">
{#if hasUnreads}
<span class="badge has-unreads">❦</span>
{:else}
<span class="badge has-no-unreads">¶</span>
{/if}
<span>{name}</span>
- </li>
-</a>
+ </a>
+</li>
diff --git a/ui/lib/components/LogOut.svelte b/ui/lib/components/LogOut.svelte
index b699cfd..1cb8fb5 100644
--- a/ui/lib/components/LogOut.svelte
+++ b/ui/lib/components/LogOut.svelte
@@ -8,7 +8,7 @@
const response = await logOut();
if (200 <= response.status && response.status < 300) {
currentUser.set(null);
- goto('/login');
+ await goto('/login');
}
}
</script>
diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte
index 5869654..1eb1d7b 100644
--- a/ui/lib/components/MessageInput.svelte
+++ b/ui/lib/components/MessageInput.svelte
@@ -24,7 +24,6 @@
</script>
<form bind:this={form} onsubmit={onSubmit}>
- <textarea onkeydown={onKeyDown} bind:value {disabled} type="search" placeholder="Say something..."
- ></textarea>
+ <textarea onkeydown={onKeyDown} bind:value {disabled} placeholder="Say something..."></textarea>
<button type="submit">&raquo;</button>
</form>
diff --git a/ui/lib/store.js b/ui/lib/store.js
index 508320f..afced4c 100644
--- a/ui/lib/store.js
+++ b/ui/lib/store.js
@@ -15,3 +15,64 @@ export const logins = writable(new Logins());
export const channelsMetaList = writable(new ChannelsMeta({ channelsMetaData }));
export const channelsList = writable(new Channels({ channelsMetaList }));
export const messages = writable(new Messages());
+
+export function onEvent(event) {
+ switch (event.type) {
+ case 'login':
+ onLoginEvent(event);
+ break;
+ case 'channel':
+ onChannelEvent(event);
+ break;
+ case 'message':
+ onMessageEvent(event);
+ break;
+ }
+}
+
+onEvent.fromJson = (event) => {
+ const parsed = JSON.parse(event);
+ return onEvent(parsed);
+};
+
+onEvent.fromMessage = (message) => {
+ const data = message.data;
+ return onEvent.fromJson(data);
+};
+
+function onLoginEvent(event) {
+ switch (event.event) {
+ case 'created':
+ logins.update((value) => value.addLogin(event.id, event.name));
+ break;
+ }
+}
+
+function onChannelEvent(event) {
+ switch (event.event) {
+ case 'created':
+ channelsList.update((value) => value.addChannel(event.id, event.name));
+ break;
+ case 'deleted':
+ channelsList.update((value) => value.deleteChannel(event.id));
+ messages.update((value) => value.deleteChannel(event.id));
+ break;
+ }
+}
+
+function onMessageEvent(event) {
+ switch (event.event) {
+ case 'sent':
+ messages.update((value) =>
+ value.addMessage(event.channel, event.id, {
+ at: event.at,
+ sender: event.sender,
+ body: event.body
+ })
+ );
+ break;
+ case 'deleted':
+ messages.update((value) => value.deleteMessage(event.id));
+ break;
+ }
+}
diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte
index 9ade399..888a185 100644
--- a/ui/routes/(app)/+layout.svelte
+++ b/ui/routes/(app)/+layout.svelte
@@ -1,12 +1,12 @@
<script>
- import { page } from '$app/stores';
+ import { page } from '$app/state';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
- import { onMount, onDestroy, getContext } from 'svelte';
+ import { getContext, onDestroy, onMount } from 'svelte';
import TinyGesture from 'tinygesture';
import { boot, subscribeToEvents } from '$lib/apiServer';
- import { currentUser, logins, channelsList, channelsMetaList, messages } from '$lib/store';
+ import { channelsList, channelsMetaList, currentUser, logins, messages, onEvent } from '$lib/store';
import ChannelList from '$lib/components/ChannelList.svelte';
import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
@@ -17,20 +17,11 @@
let pageContext = getContext('page');
let { children } = $props();
let loading = $state(true);
- let channel = $derived($page.params.channel);
+ let channel = $derived(page.params.channel);
- let rawChannels;
- channelsList.subscribe((val) => {
- rawChannels = val.channels;
- });
- let rawChannelsMeta;
- channelsMetaList.subscribe((val) => {
- rawChannelsMeta = val.channelsMeta;
- });
- let rawMessages;
- messages.subscribe((val) => {
- rawMessages = val;
- });
+ let rawChannels = $derived($channelsList.channels);
+ let rawChannelsMeta = $derived($channelsMetaList.channelsMeta);
+ let rawMessages = $derived($messages);
let enrichedChannels = $derived.by(() => {
const channels = rawChannels;
@@ -84,14 +75,15 @@
case 200:
onBooted(response.data);
events = subscribeToEvents(response.data.resume_point);
+ events.onmessage = onEvent.fromMessage;
break;
case 401:
currentUser.set(null);
- goto('/login');
+ await goto('/login');
break;
case 503:
currentUser.set(null);
- goto('/setup');
+ await goto('/setup');
break;
default:
// TODO: display error.
@@ -111,18 +103,18 @@
}
});
- function beforeUnload(evt) {
- evt.preventDefault();
+ function onbeforeunload(event) {
+ event.preventDefault();
if (events !== null) {
events.close();
}
// For some compat reasons?
- evt.returnValue = '';
+ event.returnValue = '';
return '';
}
</script>
-<svelte:window on:beforeunload={beforeUnload} />
+<svelte:window {onbeforeunload} />
<svelte:head>
<!-- TODO: unread count? -->
diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte
index 25bc318..54ebda7 100644
--- a/ui/routes/(app)/ch/[channel]/+page.svelte
+++ b/ui/routes/(app)/ch/[channel]/+page.svelte
@@ -1,11 +1,11 @@
<script>
import { DateTime } from 'luxon';
- import { page } from '$app/stores';
+ import { page } from '$app/state';
import ActiveChannel from '$lib/components/ActiveChannel.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
import { channelsMetaList, messages } from '$lib/store';
- let channel = $derived($page.params.channel);
+ let channel = $derived(page.params.channel);
let messageRuns = $derived($messages.inChannel(channel));
let activeChannel;
@@ -53,7 +53,8 @@
}
let lastReadCallback = null;
- function handleScroll() {
+
+ function onscroll() {
clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still.
lastReadCallback = setTimeout(setLastRead, 2 * 1000);
}
@@ -61,7 +62,7 @@
<svelte:window onkeydown={handleKeydown} />
-<div class="active-channel" on:scroll={handleScroll} bind:this={activeChannel}>
+<div class="active-channel" {onscroll} bind:this={activeChannel}>
<ActiveChannel {messageRuns} />
</div>
<div class="create-message">
diff --git a/ui/routes/(login)/invite/[invite]/+page.svelte b/ui/routes/(login)/invite/[invite]/+page.svelte
index 132cbc1..0c01286 100644
--- a/ui/routes/(login)/invite/[invite]/+page.svelte
+++ b/ui/routes/(login)/invite/[invite]/+page.svelte
@@ -18,7 +18,7 @@
if (200 <= response.status && response.status < 300) {
username = '';
password = '';
- goto('/');
+ await goto('/');
}
pending = false;
}
diff --git a/ui/routes/(login)/login/+page.svelte b/ui/routes/(login)/login/+page.svelte
index a1291ea..9157cef 100644
--- a/ui/routes/(login)/login/+page.svelte
+++ b/ui/routes/(login)/login/+page.svelte
@@ -16,7 +16,7 @@
if (200 <= response.status && response.status < 300) {
username = '';
password = '';
- goto('/');
+ await goto('/');
}
pending = false;
}
diff --git a/ui/routes/(login)/setup/+page.svelte b/ui/routes/(login)/setup/+page.svelte
index f162ded..c63f198 100644
--- a/ui/routes/(login)/setup/+page.svelte
+++ b/ui/routes/(login)/setup/+page.svelte
@@ -16,7 +16,7 @@
if (200 <= response.status && response.status < 300) {
username = '';
password = '';
- goto('/');
+ await goto('/');
}
pending = false;
}
diff --git a/ui/styles/app-bar.css b/ui/styles/app-bar.css
index 17620ba..0d0a311 100644
--- a/ui/styles/app-bar.css
+++ b/ui/styles/app-bar.css
@@ -28,7 +28,7 @@
.app-bar > a {
line-height: var(--app-bar-height);
- font-family: 'Archistico';
+ font-family: 'Archistico', serif;
letter-spacing: 0.25rem;
}
diff --git a/ui/styles/fonts.css b/ui/styles/fonts.css
index 06f69c8..280f8c6 100644
--- a/ui/styles/fonts.css
+++ b/ui/styles/fonts.css
@@ -6,6 +6,7 @@
font-style: normal;
font-display: swap;
}
+
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Bold.ttf') format('truetype');
@@ -13,6 +14,7 @@
font-style: normal;
font-display: swap;
}
+
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Italic.ttf') format('truetype');
@@ -20,6 +22,7 @@
font-style: italic;
font-display: swap;
}
+
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-BoldItalic.ttf') format('truetype');
@@ -36,6 +39,7 @@
font-style: normal;
font-display: swap;
}
+
@font-face {
font-family: 'Archistico';
src: url('../fonts/Archistico_Bold.ttf') format('truetype');
@@ -52,6 +56,7 @@
font-style: normal;
font-display: swap;
}
+
@font-face {
font-family: 'FiraCode';
src: url('../fonts/FiraCode-Bold.otf') format('opentype');
@@ -59,20 +64,6 @@
font-style: normal;
font-display: swap;
}
-@font-face {
- font-family: 'FiraCode';
- src: url('../fonts/FiraCode-Italic.otf') format('opentype');
- font-weight: normal;
- font-style: italic;
- font-display: swap;
-}
-@font-face {
- font-family: 'FiraCode';
- src: url('../fonts/FiraCode-BoldItalic.otf') format('opentype');
- font-weight: bold;
- font-style: italic;
- font-display: swap;
-}
/*** Overlock ***/
@font-face {
diff --git a/ui/styles/forms.css b/ui/styles/forms.css
index 88a6c41..eb98743 100644
--- a/ui/styles/forms.css
+++ b/ui/styles/forms.css
@@ -5,7 +5,7 @@ label {
}
label input {
- font-family: 'Overlock';
+ font-family: 'Overlock', cursive;
display: block;
width: 90%;
padding: 0.25rem;
@@ -14,7 +14,7 @@ label input {
}
form.form > button {
- font-family: 'Overlock';
+ font-family: 'Overlock', cursive;
background-color: var(--colour-input-bg);
color: var(--colour-input-text);
padding: 0.25rem;
diff --git a/ui/styles/messages.css b/ui/styles/messages.css
index c4ef106..4890f2c 100644
--- a/ui/styles/messages.css
+++ b/ui/styles/messages.css
@@ -51,6 +51,7 @@
.message:hover {
background-color: var(--colour-message-hover-bg);
}
+
.message:hover * {
color: var(--colour-message-hover-text);
}
@@ -112,11 +113,10 @@
.message-body pre {
border: 1px solid #312e81;
border-radius: 0.25rem;
- background-color: var(--colour-message-run-text);
padding: 0.25rem;
}
.message-body code,
.message-body pre {
- font-family: 'FiraCode';
+ font-family: 'FiraCode', monospace;
}
diff --git a/ui/styles/reset.css b/ui/styles/reset.css
index f9fa505..5a17f02 100644
--- a/ui/styles/reset.css
+++ b/ui/styles/reset.css
@@ -93,6 +93,7 @@ video {
/* font: inherit; */
vertical-align: baseline;
}
+
/* HTML5 display-role reset for older browsers */
article,
aside,
@@ -107,24 +108,28 @@ nav,
section {
display: block;
}
+
body {
line-height: 1;
}
+
ol,
ul {
list-style: none;
}
+
blockquote,
q {
quotes: none;
}
+
blockquote:before,
blockquote:after,
q:before,
q:after {
- content: '';
content: none;
}
+
table {
border-collapse: collapse;
border-spacing: 0;
diff --git a/ui/styles/sidebar.css b/ui/styles/sidebar.css
index 5e5e16a..c6aab6a 100644
--- a/ui/styles/sidebar.css
+++ b/ui/styles/sidebar.css
@@ -4,6 +4,8 @@
}
.list-nav a {
+ display: block;
+ padding: 0.5rem;
text-decoration: none;
}
@@ -12,7 +14,6 @@
}
.list-nav li {
- padding: 0.5rem;
border-radius: 0.5rem;
border: 1px solid var(--colour-navbar-border);
margin: 0.25rem;
diff --git a/ui/styles/textarea.css b/ui/styles/textarea.css
index d9be0d6..4b8602c 100644
--- a/ui/styles/textarea.css
+++ b/ui/styles/textarea.css
@@ -18,7 +18,7 @@
flex-grow: 1;
background-color: var(--colour-input-bg);
color: var(--colour-input-text);
- font-family: 'FiraCode';
+ font-family: 'FiraCode', monospace;
}
.create-message button {