summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rwxr-xr-xtools/run14
-rw-r--r--ui/lib/apiServer.js27
-rw-r--r--ui/lib/assets/logo.pngbin0 -> 137101 bytes
-rw-r--r--ui/lib/components/Channel.svelte8
-rw-r--r--ui/lib/components/MessageRun.svelte45
-rw-r--r--ui/lib/store.js1
-rw-r--r--ui/lib/store/channels.js14
-rw-r--r--ui/routes/(app)/+layout.svelte109
-rw-r--r--ui/routes/+layout.svelte48
-rw-r--r--ui/static/favicon.pngbin1571 -> 137101 bytes
11 files changed, 208 insertions, 62 deletions
diff --git a/package.json b/package.json
index ec8a66c..8a54766 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,10 @@
"name": "hi",
"version": "0.0.1",
"private": true,
+ "engines" : {
+ "npm" : ">=10.8.3 <11.0.0",
+ "node" : ">=22.9.0 <23.0.0"
+ },
"scripts": {
"dev": "vite dev",
"build": "vite build",
diff --git a/tools/run b/tools/run
index b063eb7..562a94d 100755
--- a/tools/run
+++ b/tools/run
@@ -1,7 +1,15 @@
#!/bin/bash -e
## tools/run [ARGS...]
-##
-## Run the server in development mode. Shorthand for `cargo run`.
-cargo run -- "$@"
+if [ -z ${HI_DEV+x} ]; then
+ tools/build-ui
+ cargo run -- "$@"
+else
+ npm run dev & PIDS[0]=$!
+ cargo run -- "$@" & PIDS[1]=$!
+
+ trap "kill ${PIDS[*]}" SIGINT
+
+ wait
+fi
diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js
index 5c6e5ef..3e270fc 100644
--- a/ui/lib/apiServer.js
+++ b/ui/lib/apiServer.js
@@ -1,5 +1,6 @@
import axios from 'axios';
-import { channelsList, logins, messages } from '$lib/store';
+import { get } from 'svelte/store';
+import { currentUser, channelsList, logins, messages } from '$lib/store';
export const apiServer = axios.create({
baseURL: '/api/',
@@ -115,9 +116,33 @@ function onMessageEvent(data) {
switch (data.event) {
case 'sent':
messages.update((value) => value.addMessage(data.channel, data.id, data.at, data.sender, data.body));
+ displayToast(data);
break;
case 'deleted':
messages.update((value) => value.deleteMessage(data.id));
break;
}
}
+
+function displayToast(data) {
+ // we use get throughout this as this function is not reactive, and just
+ // needs the values of the stores at a moment in time.
+ const currentUserId = get(currentUser).id;
+ if (currentUserId === data.sender) {
+ return;
+ }
+
+ const senderName = get(logins).get(data.sender);
+ const channelName = get(channelsList).get(data.channel);
+ const title = `${senderName} in ${channelName}`;
+
+ const opts = {
+ body: data.body,
+ tag: title,
+ // TODO: we need to inject the understory/hi icon in a more principled way here:
+ icon: "/ui/lib/assets/logo.png",
+ // TODO: support onclick bringing you to the relevant channel?
+ onclick: null
+ }
+ new Notification(title, opts);
+}
diff --git a/ui/lib/assets/logo.png b/ui/lib/assets/logo.png
new file mode 100644
index 0000000..5df6b4e
--- /dev/null
+++ b/ui/lib/assets/logo.png
Binary files differ
diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte
index e62f0f3..bbe9ff7 100644
--- a/ui/lib/components/Channel.svelte
+++ b/ui/lib/components/Channel.svelte
@@ -1,14 +1,20 @@
<script>
+ import { showMenu } from '$lib/store';
+
export let id;
export let name;
export let active = false;
+
+ function hideMenu() {
+ showMenu.update(() => false);
+ }
</script>
<li
class="rounded-full"
class:bg-slate-400={active}
>
-<a href="/ch/{id}">
+<a href="/ch/{id}" on:click={hideMenu}>
<span class="badge bg-primary-500">#</span>
<span class="flex-auto">{name}</span>
</a>
diff --git a/ui/lib/components/MessageRun.svelte b/ui/lib/components/MessageRun.svelte
index 687eec3..cbf4f04 100644
--- a/ui/lib/components/MessageRun.svelte
+++ b/ui/lib/components/MessageRun.svelte
@@ -1,23 +1,34 @@
<script>
- import { logins } from '$lib/store';
- import Message from '$lib/components/Message.svelte';
+ import { logins, currentUser } from '$lib/store';
+ import Message from '$lib/components/Message.svelte';
- export let sender;
- export let messages;
+ export let sender;
+ export let messages;
- let name;
- $: name = $logins.get(sender);
-
- let scroll = (message) => {
- message.scrollIntoView();
- }
+ let name;
+ $: name = $logins.get(sender);
+ $: ownMessage = $currentUser.id == sender;
</script>
-<div class="card card-hover m-4 px-4 py-1 relative">
- <span class="chip variant-soft sticky top-o left-0">
- @{name}:
- </span>
- {#each messages as { at, body }}
- <Message {at} {body} />
- {/each}
+<div
+ 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}
</div>
+
+<style>
+ .own-message {
+ width: 80%;
+ margin-right: auto;
+ }
+ .other-message {
+ width: 80%;
+ margin-left: auto;
+ }
+</style>
diff --git a/ui/lib/store.js b/ui/lib/store.js
index ae17ffa..bdd3e3b 100644
--- a/ui/lib/store.js
+++ b/ui/lib/store.js
@@ -3,6 +3,7 @@ import { Channels } from '$lib/store/channels';
import { Messages } from '$lib/store/messages';
import { Logins } from '$lib/store/logins';
+export const showMenu = writable(false);
export const currentUser = writable(null);
export const logins = writable(new Logins());
export const channelsList = writable(new Channels());
diff --git a/ui/lib/store/channels.js b/ui/lib/store/channels.js
index b57ca7e..6d722c5 100644
--- a/ui/lib/store/channels.js
+++ b/ui/lib/store/channels.js
@@ -1,17 +1,26 @@
export class Channels {
constructor() {
this.channels = [];
+ this.channelData = {};
}
setChannels(channels) {
this.channels = [...channels];
this.sort();
+ this.channelData = channels.reduce(
+ (acc, val) => ({
+ ...acc,
+ [val.id]: val.name,
+ }),
+ {}
+ );
return this;
}
addChannel(id, name) {
this.channels = [...this.channels, { id, name }];
this.sort();
+ this.channelData[id] = name;
return this;
}
@@ -20,9 +29,14 @@ export class Channels {
if (channelIndex !== -1) {
this.channels.splice(channelIndex, 1);
}
+ delete this.channelData[id];
return this;
}
+ get(id) {
+ return this.channelData[id];
+ }
+
sort() {
this.channels.sort((a, b) => {
if (a.name < b.name) {
diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte
index 08c6694..cf5d5f1 100644
--- a/ui/routes/(app)/+layout.svelte
+++ b/ui/routes/(app)/+layout.svelte
@@ -4,7 +4,7 @@
import { onMount, onDestroy } from 'svelte';
import { boot, subscribeToEvents } from '$lib/apiServer';
- import { currentUser, logins, channelsList, messages } from '$lib/store';
+ import { showMenu, currentUser, logins, channelsList, messages } from '$lib/store';
import ChannelList from '$lib/components/ChannelList.svelte';
import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
@@ -12,6 +12,15 @@
let loading = true;
let events = null;
+ let showMenuValue;
+ showMenu.subscribe((value) => {
+ showMenuValue = value;
+ });
+
+
+ function toggleMenu() {
+ showMenu.update((value) => !value);
+ }
$: channel = $page?.params?.channel;
@@ -54,36 +63,84 @@
});
</script>
+<svelte:head>
+ <title>understory</title>
+</svelte:head>
+
{#if loading}
<h2>Loading&hellip;</h2>
{:else}
- <div id="interface">
- <div class="channel-list">
- <ChannelList active={channel} />
- </div>
- <div class="active-channel">
- <slot />
- </div>
- <div class="create-channel">
- <CreateChannelForm />
- </div>
- <div class="create-message">
- <MessageInput {channel} />
- </div>
+ <div id="interface" class="p-2">
+ <nav id="sidebar" data-expanded={showMenuValue}>
+ <button class="h-4 w-4" aria-controls="sidebar" aria-expanded={showMenuValue} on:click={toggleMenu}>&#10006;</button>
+ <div class="channel-list">
+ <ChannelList active={channel} />
+ </div>
+ <div class="create-channel">
+ <CreateChannelForm />
+ </div>
+ </nav>
+ <main>
+ <div class="active-channel border border-solid border-gray-400 rounded-[1.25rem]">
+ <slot />
+ </div>
+ <div class="create-message overflow-scroll max-h-full">
+ <MessageInput {channel} />
+ </div>
+ </main>
</div>
{/if}
<style>
- #interface {
- height: 88vh;
- margin: 1rem;
- display: grid;
- grid-template-columns: 18rem auto;
- grid-template-rows: auto 2rem;
- grid-gap: 0.25rem;
- }
- #interface div {
- max-height: 100%;
- overflow: scroll;
- }
+: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));
+
+ @media (width > 640px) {
+ --overlay: static;
+ --translate: 0;
+ }
+}
+nav {
+ grid-area: side;
+ background-color: rgb(var(--color-surface-600));
+ 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;
+}
+nav button {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+main {
+ grid-area: main;
+ height: calc(100vh - var(--app-bar-height) - var(--interface-padding));
+}
+.active-channel {
+ height: calc(100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height));
+ overflow: scroll;
+}
+.channel-list {
+ height: calc(100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height));
+ overflow: scroll;
+}
+nav[data-expanded=false] {
+ translate: var(--translate, -100% 0);
+}
</style>
diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte
index 0140699..eb29179 100644
--- a/ui/routes/+layout.svelte
+++ b/ui/routes/+layout.svelte
@@ -1,21 +1,41 @@
<script>
- import { AppBar } from '@skeletonlabs/skeleton';
- import "../app.css";
+ import "../app.css";
+ import logo from '$lib/assets/logo.png';
- import { currentUser } from '$lib/store';
- import CurrentUser from '$lib/components/CurrentUser.svelte';
+ import { onMount } from 'svelte';
+
+ import { AppBar } from '@skeletonlabs/skeleton';
+ import { showMenu, currentUser } from '$lib/store';
+
+ import CurrentUser from '$lib/components/CurrentUser.svelte';
+
+ function toggleMenu() {
+ showMenu.update((value) => !value);
+ }
+
+ onMount(() => {
+ Notification.requestPermission().then((result) => {
+ console.log(result);
+ });
+ });
</script>
-<div id="app">
- <AppBar>
- <svelte:fragment slot="lead">🌳</svelte:fragment>
- <a href="/">understory</a>
- <svelte:fragment slot="trail">
- {#if $currentUser}
- <CurrentUser />
- {/if}
- </svelte:fragment>
- </AppBar>
+<div id="app" class="m-0 p-0 h-vh w-full">
+ <div class="w-full">
+ <AppBar class="app-bar">
+ <svelte:fragment slot="lead">
+ <a on:click|preventDefault={toggleMenu} class="cursor-pointer">
+ <img class="w-8 h-8" alt="logo" src={logo} />
+ </a>
+ </svelte:fragment>
+ <a href="/">understory</a>
+ <svelte:fragment slot="trail">
+ {#if $currentUser}
+ <CurrentUser />
+ {/if}
+ </svelte:fragment>
+ </AppBar>
+ </div>
<slot />
</div>
diff --git a/ui/static/favicon.png b/ui/static/favicon.png
index 825b9e6..5df6b4e 100644
--- a/ui/static/favicon.png
+++ b/ui/static/favicon.png
Binary files differ