summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc2
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock70
-rw-r--r--Cargo.toml11
-rw-r--r--Dockerfile.builder4
-rw-r--r--debian/default4
-rw-r--r--debian/hi.service11
-rw-r--r--debian/pilcrow.service13
-rwxr-xr-xdebian/postinst4
-rw-r--r--docs/api/book.toml2
-rw-r--r--docs/design.md4
-rw-r--r--docs/internal-server-errors.md2
-rw-r--r--docs/ops.md8
-rw-r--r--eslint.config.js24
-rwxr-xr-xgit-hooks/pre-commit4
-rw-r--r--package-lock.json4
-rw-r--r--package.json2
-rw-r--r--src/cli.rs33
-rw-r--r--src/error.rs2
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs2
-rwxr-xr-xtools/build-builder4
-rwxr-xr-xtools/build-debian2
-rwxr-xr-xtools/run3
-rwxr-xr-xtools/version2
-rw-r--r--ui/lib/apiServer.js6
-rw-r--r--ui/lib/components/ActiveChannel.svelte43
-rw-r--r--ui/lib/components/ChangePassword.svelte60
-rw-r--r--ui/lib/components/Invite.svelte5
-rw-r--r--ui/lib/components/Invites.svelte13
-rw-r--r--ui/lib/components/LogOut.svelte18
-rw-r--r--ui/lib/components/MessageInput.svelte5
-rw-r--r--ui/lib/components/MessageRun.svelte2
-rw-r--r--ui/lib/store.js2
-rw-r--r--ui/lib/store/messages.js40
-rw-r--r--ui/lib/store/messages.svelte.js74
-rw-r--r--ui/routes/(app)/+layout.svelte4
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte4
-rw-r--r--ui/routes/(app)/me/+page.svelte84
-rw-r--r--ui/routes/+layout.svelte4
-rw-r--r--vite.config.js2
41 files changed, 298 insertions, 290 deletions
diff --git a/.envrc b/.envrc
index 0423909..56295f5 100644
--- a/.envrc
+++ b/.envrc
@@ -1,6 +1,6 @@
PATH_add tools
PATH_add target/debug
-export DATABASE_URL="${DATABASE_URL:-sqlite://.hi?mode=rwc}"
+export DATABASE_URL="${DATABASE_URL:-sqlite://pilcrow.db?mode=rwc}"
source_env_if_exists .envrc.local
diff --git a/.gitignore b/.gitignore
index 7977b18..11b7d6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,7 @@
/.hi
/.hi.pre-commit
/.hi.backup
+/pilcrow.db
+/pilcrow.db.backup
+/pilcrow.db.pre-commit
/vite.config.js.*.mjs
diff --git a/Cargo.lock b/Cargo.lock
index d98f51f..0e4c143 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -799,41 +799,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
-name = "hi"
-version = "0.1.0"
-dependencies = [
- "argon2",
- "async-trait",
- "axum",
- "axum-extra",
- "chrono",
- "clap",
- "faker_rand",
- "futures",
- "headers",
- "hex-literal",
- "itertools",
- "mime",
- "password-hash",
- "pin-project",
- "rand",
- "rand_core",
- "rusqlite",
- "rust-embed",
- "serde",
- "serde_json",
- "sqlx",
- "thiserror",
- "tokio",
- "tokio-stream",
- "unicode-casefold",
- "unicode-normalization",
- "unicode-segmentation",
- "unix_path",
- "uuid",
-]
-
-[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1263,6 +1228,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
+name = "pilcrow"
+version = "0.1.0"
+dependencies = [
+ "argon2",
+ "async-trait",
+ "axum",
+ "axum-extra",
+ "chrono",
+ "clap",
+ "faker_rand",
+ "futures",
+ "headers",
+ "hex-literal",
+ "itertools",
+ "mime",
+ "password-hash",
+ "pin-project",
+ "rand",
+ "rand_core",
+ "rusqlite",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "unicode-casefold",
+ "unicode-normalization",
+ "unicode-segmentation",
+ "unix_path",
+ "uuid",
+]
+
+[[package]]
name = "pin-project"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 868308f..371ade5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,23 +1,22 @@
[package]
-name = "hi"
+name = "pilcrow"
version = "0.1.0"
edition = "2021"
rust-version = "1.82"
-default-run = "hi"
authors = [
- "Owen Jacobson <hi@grimoire.ca>",
+ "Owen Jacobson <owen@grimoire.ca>",
"Kit La Touche <kit@transneptune.net>",
]
[package.metadata.deb]
-maintainer = "Owen Jacobson <hi@grimoire.ca>"
+maintainer = "Owen Jacobson <owen@grimoire.ca>"
maintainer-scripts = "debian"
assets = [
# Binaries
- ["target/release/hi", "/usr/bin/hi", "755"],
+ ["target/release/pilcrow", "/usr/bin/pilcrow", "755"],
# Configuration
- ["debian/default", "/etc/default/hi", "644"],
+ ["debian/default", "/etc/default/pilcrow", "644"],
]
[package.metadata.deb.systemd-units]
diff --git a/Dockerfile.builder b/Dockerfile.builder
index da80c69..720b95a 100644
--- a/Dockerfile.builder
+++ b/Dockerfile.builder
@@ -1,7 +1,7 @@
FROM rust:1-slim-bookworm
-COPY builder /opt/hi-builder
-RUN /opt/hi-builder/image-setup
+COPY builder /opt/pilcrow-builder
+RUN /opt/pilcrow-builder/image-setup
RUN mkdir /app
WORKDIR /app
diff --git a/debian/default b/debian/default
index 3076699..3cabe21 100644
--- a/debian/default
+++ b/debian/default
@@ -1,2 +1,2 @@
-DATABASE_URL=sqlite:///var/lib/hi/hi.db
-BACKUP_DATABASE_URL=sqlite:///var/lib/hi/backup.db
+DATABASE_URL=sqlite:///var/lib/pilcrow/pilcrow.db
+BACKUP_DATABASE_URL=sqlite:///var/lib/pilcrow/backup.db
diff --git a/debian/hi.service b/debian/hi.service
deleted file mode 100644
index cc4a951..0000000
--- a/debian/hi.service
+++ /dev/null
@@ -1,11 +0,0 @@
-[Unit]
-Description=Hi chat service
-After=network-online.target
-
-[Service]
-EnvironmentFile=/etc/default/hi
-ExecStart=/usr/bin/hi
-Restart=on-failure
-
-[Install]
-WantedBy=multi-user.target
diff --git a/debian/pilcrow.service b/debian/pilcrow.service
new file mode 100644
index 0000000..3471d20
--- /dev/null
+++ b/debian/pilcrow.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Pilcrow chat service
+After=network-online.target
+
+[Service]
+EnvironmentFile=/etc/default/pilcrow
+User=pilcrow
+Group=pilcrow
+ExecStart=/usr/bin/pilcrow
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/debian/postinst b/debian/postinst
index d88a7ad..a3f58a0 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -4,7 +4,7 @@ set -e
adduser \
--system \
--group \
- --home /var/lib/hi \
- hi
+ --home /var/lib/pilcrow \
+ pilcrow
#DEBHELPER#
diff --git a/docs/api/book.toml b/docs/api/book.toml
index 476872c..493939b 100644
--- a/docs/api/book.toml
+++ b/docs/api/book.toml
@@ -1,5 +1,5 @@
[book]
-title = "The hi API"
+title = "The Pilcrow API"
authors = ["Owen Jacobson"]
language = "en"
multilingual = false
diff --git a/docs/design.md b/docs/design.md
index 1180b83..6cd0075 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -1,6 +1,6 @@
# Internal design
-`hi`'s design is discovered and not planned. Do not take this as doctrine; continue to experiment on the structure as you find new needs.
+`pilcrow`'s design is discovered and not planned. Do not take this as doctrine; continue to experiment on the structure as you find new needs.
As of this writing, the basic flow of most requests hits several subsystems:
@@ -15,7 +15,7 @@ This approach helps enable testing (see [testing.md] and the implementation of m
Handling time in a service is always tricky.
-`hi` takes the approach that a request is considered to be serviced at one time, and that that time is determined when the request is received. The internals of `hi` - the "app" and data access types described below, as well as most other supporting tools - are written with an eye towards accepting the "current time" as an argument, rather than calling out to a clock for themselves.
+`pilcrow` takes the approach that a request is considered to be serviced at one time, and that that time is determined when the request is received. The internals of `pilcrow` - the "app" and data access types described below, as well as most other supporting tools - are written with an eye towards accepting the "current time" as an argument, rather than calling out to a clock for themselves.
The "current time" for a request is determined in `src/clock.rs`, which runs on every request, and is available to the handler via the `RequestedAt` extractor defined in that module.
diff --git a/docs/internal-server-errors.md b/docs/internal-server-errors.md
index 16d61a2..7532e10 100644
--- a/docs/internal-server-errors.md
+++ b/docs/internal-server-errors.md
@@ -1,6 +1,6 @@
# Internal Server Errors
-When `hi` encounters a problem that prevents a request from completing, it may report a `500 Internal Server Error` to clients, along with an error code. The actual error will be printed to standard error, with the error code. The following sections describe errors we've encountered, the likely operational consequences, and recommend approaches for addressing them.
+When the `pilcrow` server encounters a problem that prevents a request from completing, it may report a `500 Internal Server Error` to clients, along with an error code. The actual error will be printed to standard error, with the error code. The following sections describe errors we've encountered, the likely operational consequences, and recommend approaches for addressing them.
## database is locked
diff --git a/docs/ops.md b/docs/ops.md
index 02644c2..4274e22 100644
--- a/docs/ops.md
+++ b/docs/ops.md
@@ -1,11 +1,11 @@
-# Operating `hi`
+# Operating pilcrow
## Upgrades
-`hi` will automatically upgrade its database on startup. Before doing so, it will create a backup of your database (at `.hi.backup`, or controlled by `--backup-database-url`). If the migration process succeeds, this backup will be deleted automatically. If the migration process _fails_, however, `hi` will attempt to restore your existing database from the backup before exiting. If the restore process also fails, then both the backup database and the suspected-broken database will be left in place.
+The `pilcrow` server will automatically upgrade its database on startup. Before doing so, it will create a backup of your database (at `pilcrow.db.backup`, or controlled by `--backup-database-url`). If the migration process succeeds, this backup will be deleted automatically. If the migration process _fails_, however, `pilcrow` will attempt to restore your existing database from the backup before exiting. If the restore process also fails, then both the backup database and the suspected-broken database will be left in place.
-To avoid destroying backups that may still be needed, `hi` will not start if the backup database already exists. **There is no catch-all advice on how to proceed**, but you can try the following:
+To avoid destroying backups that may still be needed, `pilcrow` will not start if the backup database already exists. **There is no catch-all advice on how to proceed**, but you can try the following:
* Start the server with **a copy** of the backup database, and determine if any data has been lost. If not, shut it down, replace your main database by copying the backup, and carry on.
-The `hi` database is an ordinary file. While the server is not running, it can be freely copied or renamed without invalidating the data in it.
+The `pilcrow` database is an ordinary file. While the server is not running, it can be freely copied or renamed without invalidating the data in it.
diff --git a/eslint.config.js b/eslint.config.js
index 2eeebcc..7d23626 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,23 +1,3 @@
-import js from '@eslint/js';
-import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
-import globals from 'globals';
-
-/** @type {import('eslint').Linter.Config[]} */
-export default [
- js.configs.recommended,
- ...svelte.configs['flat/recommended'],
- prettier,
- ...svelte.configs['flat/prettier'],
- {
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.node
- }
- }
- },
- {
- ignores: ['build/', '.svelte-kit/', 'dist/']
- }
-];
+import svelte from 'eslint-plugin-svelte';
+export default [prettier, ...svelte.configs['flat/prettier']];
diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit
index 6715430..587e349 100755
--- a/git-hooks/pre-commit
+++ b/git-hooks/pre-commit
@@ -13,7 +13,7 @@ npm run lint
cargo check
# Make sure the prepared statement data in .sqlx is up to date. Requires
# `cargo-sqlx` (`cargo install cargo-sqlx`).
-export DATABASE_URL=sqlite://.hi.pre-commit?mode=rwc
-rm -f .hi.pre-commit
+export DATABASE_URL=sqlite://pilcrow.db.pre-commit?mode=rwc
+rm -f pilcrow.db.pre-commit
cargo sqlx migrate run
cargo sqlx prepare --check
diff --git a/package-lock.json b/package-lock.json
index 2f85351..01b0cce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "hi",
+ "name": "pilcrow",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "hi",
+ "name": "pilcrow",
"version": "0.0.1",
"dependencies": {
"axios": "^1.7.7",
diff --git a/package.json b/package.json
index cb57efe..c51974c 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "hi",
+ "name": "pilcrow",
"version": "0.0.1",
"private": true,
"engines": {
diff --git a/src/cli.rs b/src/cli.rs
index 308294d..0d448d2 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,6 +1,6 @@
-//! The `hi` command-line interface.
+//! The `pilcrow` command-line interface.
//!
-//! This module supports running `hi` as a freestanding program, via the
+//! This module supports running `pilcrow` as a freestanding program, via the
//! [`Args`] struct.
use std::{future, io};
@@ -22,18 +22,18 @@ use crate::{
ui,
};
-/// Command-line entry point for running the `hi` server.
+/// Command-line entry point for running the `pilcrow` server.
///
/// This is intended to be used as a Clap [Parser], to capture command-line
-/// arguments for the `hi` server:
+/// arguments for the `pilcrow` server:
///
/// ```no_run
-/// # use hi::cli::Error;
+/// # use pilcrow::cli::Error;
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// use clap::Parser;
-/// use hi::cli::Args;
+/// use pilcrow::cli::Args;
///
/// let args = Args::parse();
/// args.run().await?;
@@ -43,35 +43,36 @@ use crate::{
#[derive(Parser)]
#[command(
version,
- about = "Run the `hi` server.",
- long_about = r#"Run the `hi` server.
+ about = "Run the `pilcrow` server.",
+ long_about = r#"Run the `pilcrow` server.
The database at `--database-url` will be created, or upgraded, automatically."#
)]
pub struct Args {
- /// The network address `hi` should listen on
+ /// The network address `pilcrow` should listen on
#[arg(short, long, env, default_value = "localhost")]
address: String,
- /// The network port `hi` should listen on
+ /// The network port `pilcrow` should listen on
#[arg(short, long, env, default_value_t = 64209)]
port: u16,
- /// Sqlite URL or path for the `hi` database
- #[arg(short, long, env, default_value = "sqlite://.hi")]
+ /// Sqlite URL or path for the `pilcrow` database
+ #[arg(short, long, env, default_value = "sqlite://pilcrow.db")]
database_url: String,
- /// Sqlite URL or path for a backup of the `hi` database during upgrades
- #[arg(short = 'D', long, env, default_value = "sqlite://.hi.backup")]
+ /// Sqlite URL or path for a backup of the `pilcrow` database during
+ /// upgrades
+ #[arg(short = 'D', long, env, default_value = "sqlite://pilcrow.db.backup")]
backup_database_url: String,
}
impl Args {
- /// Runs the `hi` server, using the parsed configuation in `self`.
+ /// Runs the `pilcrow` server, using the parsed configuation in `self`.
///
/// This will perform the following tasks:
///
- /// * Migrate the `hi` database (at `--database-url`).
+ /// * Migrate the `pilcrow` database (at `--database-url`).
/// * Start an HTTP server (on the interface and port controlled by
/// `--address` and `--port`).
/// * Print a status message.
diff --git a/src/error.rs b/src/error.rs
index f3399c6..7483f00 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -40,7 +40,7 @@ impl fmt::Display for Internal {
impl IntoResponse for Internal {
fn into_response(self) -> Response {
let Self(id, error) = &self;
- eprintln!("hi: [{id}] {error}");
+ eprintln!("pilcrow: [{id}] {error}");
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 84b8dfc..765e625 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-//! `hi` - a web-based, self-hosted chat system.
+//! Pilcrow - a web-based, self-hosted chat system.
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
diff --git a/src/main.rs b/src/main.rs
index d0830ff..427294e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
use clap::Parser;
-use hi::cli;
+use pilcrow::cli;
#[tokio::main]
async fn main() -> Result<(), cli::Error> {
diff --git a/tools/build-builder b/tools/build-builder
index fcb1e84..8174b4e 100755
--- a/tools/build-builder
+++ b/tools/build-builder
@@ -8,7 +8,7 @@ cd "$(dirname "$0")/.."
docker build \
--platform "linux/arm64,linux/amd64" \
- --tag "hi-debian-builder:$(tools/version)" \
- --tag "hi-debian-builder:latest" \
+ --tag "pilcrow-debian-builder:$(tools/version)" \
+ --tag "pilcrow-debian-builder:latest" \
--file Dockerfile.builder \
.
diff --git a/tools/build-debian b/tools/build-debian
index c64fc78..0e496a8 100755
--- a/tools/build-debian
+++ b/tools/build-debian
@@ -15,6 +15,6 @@ for platform in linux/arm64 linux/amd64; do
--interactive \
--tty \
--volume "$PWD:/app" \
- "hi-debian-builder:$(tools/version)" \
+ "pilcrow-debian-builder:$(tools/version)" \
cargo deb
done
diff --git a/tools/run b/tools/run
index ac42e93..8c450cf 100755
--- a/tools/run
+++ b/tools/run
@@ -2,8 +2,7 @@
## tools/run [ARGS...]
-if [ -z ${HI_DEV+x} ]; then
- tools/build-ui
+if [ -z ${PILCROW_DEV+x} ]; then
cargo run -- "$@"
else
npm run dev -- --host 192.168.68.57 & PIDS[0]=$!
diff --git a/tools/version b/tools/version
index 8e47a7c..f11c0e8 100755
--- a/tools/version
+++ b/tools/version
@@ -8,4 +8,4 @@ cd "$(dirname "$0")/.."
cargo metadata \
--format-version 1 |
-jq -r '.packages[] | select(.name == "hi") | .version'
+jq -r '.packages[] | select(.name == "pilcrow") | .version'
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;
+ }
+}
diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte
index 84090e7..84c71ec 100644
--- a/ui/routes/(app)/+layout.svelte
+++ b/ui/routes/(app)/+layout.svelte
@@ -91,7 +91,7 @@
</script>
<svelte:head>
- <title>understory</title>
+ <title>pilcrow</title>
</svelte:head>
{#if loading}
@@ -106,7 +106,7 @@
<CreateChannelForm />
</div>
</nav>
- <main>
+ <main class="pl-4">
{@render children?.()}
</main>
</div>
diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte
index 49c1c29..0961665 100644
--- a/ui/routes/(app)/ch/[channel]/+page.svelte
+++ b/ui/routes/(app)/ch/[channel]/+page.svelte
@@ -2,12 +2,14 @@
import { page } from '$app/stores';
import ActiveChannel from '$lib/components/ActiveChannel.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
+ import { messages } from '$lib/store';
let channel = $derived($page.params.channel);
+ let messageRuns = $derived($messages.inChannel(channel));
</script>
<div class="active-channel">
- <ActiveChannel {channel} />
+ <ActiveChannel {messageRuns} />
</div>
<div class="create-message max-h-full">
<MessageInput {channel} />
diff --git a/ui/routes/(app)/me/+page.svelte b/ui/routes/(app)/me/+page.svelte
index 8d24a61..aded292 100644
--- a/ui/routes/(app)/me/+page.svelte
+++ b/ui/routes/(app)/me/+page.svelte
@@ -1,79 +1,17 @@
<script>
- import { goto } from '$app/navigation';
- import { changePassword, logOut } from '$lib/apiServer.js';
- import { currentUser } from '$lib/store';
-
+ import LogOut from '$lib/components/LogOut.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);
-
- async function onLogOut(event) {
- event.preventDefault();
- const response = await logOut();
- if (200 <= response.status && response.status < 300) {
- currentUser.update(() => null);
- goto('/login');
- }
- }
-
- async function onPasswordChange(event) {
- event.preventDefault();
- pending = true;
- let response = await changePassword(currentPassword, newPassword);
- switch (response.status) {
- case 200:
- passwordForm.reset();
- break;
- }
- pending = false;
- }
+ import ChangePassword from '$lib/components/ChangePassword.svelte';
</script>
-<form onsubmit={onLogOut}>
- <button class="btn variant-filled" type="submit">log out</button>
-</form>
-
-<form onsubmit={onPasswordChange} bind:this={passwordForm}>
- <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>
+<div class="mb-4">
+ <ChangePassword />
+</div>
- <button class="btn variant-filled" type="submit" {disabled}> change password </button>
-</form>
+<div class="mb-4">
+ <Invites />
+</div>
-<Invites />
+<div>
+ <LogOut />
+</div>
diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte
index ef3e823..8940659 100644
--- a/ui/routes/+layout.svelte
+++ b/ui/routes/+layout.svelte
@@ -30,10 +30,10 @@
<img class="w-8 h-8" alt="logo" src={logo} />
</button>
</svelte:fragment>
- <a href="/">understory</a>
+ <a href="/">pilcrow</a>
<svelte:fragment slot="trail">
{#if $currentUser}
- <div class="rounded-full bg-secondary-400 px-2 py-1">
+ <div class="rounded-full bg-secondary-400 px-3 py-1">
<a href="/me">@{$currentUser.username}</a>
</div>
{/if}
diff --git a/vite.config.js b/vite.config.js
index 3229f33..863d652 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -10,7 +10,7 @@ export default defineConfig({
]
},
proxy: {
- '/api': 'http://localhost:64209',
+ '/api': process.env['API_SERVER'] || 'http://localhost:64209',
},
},
});