diff options
| -rw-r--r-- | package.json | 4 | ||||
| -rwxr-xr-x | tools/run | 14 | ||||
| -rw-r--r-- | ui/lib/apiServer.js | 27 | ||||
| -rw-r--r-- | ui/lib/assets/logo.png | bin | 0 -> 137101 bytes | |||
| -rw-r--r-- | ui/lib/components/Channel.svelte | 8 | ||||
| -rw-r--r-- | ui/lib/components/MessageRun.svelte | 45 | ||||
| -rw-r--r-- | ui/lib/store.js | 1 | ||||
| -rw-r--r-- | ui/lib/store/channels.js | 14 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 109 | ||||
| -rw-r--r-- | ui/routes/+layout.svelte | 48 | ||||
| -rw-r--r-- | ui/static/favicon.png | bin | 1571 -> 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", @@ -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 Binary files differnew file mode 100644 index 0000000..5df6b4e --- /dev/null +++ b/ui/lib/assets/logo.png 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…</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}>✖</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 Binary files differindex 825b9e6..5df6b4e 100644 --- a/ui/static/favicon.png +++ b/ui/static/favicon.png |
