diff options
| -rw-r--r-- | docs/api/boot.md | 10 | ||||
| -rw-r--r-- | src/channel/event.rs | 4 | ||||
| -rw-r--r-- | src/channel/history.rs | 2 | ||||
| -rw-r--r-- | src/channel/repo.rs | 10 | ||||
| -rw-r--r-- | src/channel/snapshot.rs | 4 | ||||
| -rw-r--r-- | ui/lib/session.svelte.js | 52 | ||||
| -rw-r--r-- | ui/lib/state/remote/channels.svelte.js | 26 | ||||
| -rw-r--r-- | ui/lib/state/remote/messages.svelte.js | 7 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 24 | ||||
| -rw-r--r-- | ui/routes/(app)/ch/[channel]/+page.svelte | 15 |
10 files changed, 96 insertions, 58 deletions
diff --git a/docs/api/boot.md b/docs/api/boot.md index 46b972f..2b9cf00 100644 --- a/docs/api/boot.md +++ b/docs/api/boot.md @@ -51,6 +51,7 @@ This endpoint will respond with a status of ], "channels": [ { + "at": "2025-04-14T23:58:11.421901Z", "name": "nonsense and such", "id": "C1234abcd" } @@ -94,10 +95,11 @@ Each element of the `users` array describes a distinct user, and will include th Each element of the `channels` array describes a distinct channel, and will include the following fields: -| Field | Type | Description | -|:-------|:-------|:----------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | string | The name for the channel. | -| `id` | string | A unique identifier for the channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | +| Field | Type | Description | +|:-------|:----------|:----------------------------------------------------------------------------------------------------------------------------------------------| +| `at` | timestamp | The moment the channel was created. | +| `name` | string | The name for the channel. | +| `id` | string | A unique identifier for the channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | Each element of the `messages` array describes a distinct message, and will include the following fields: diff --git a/src/channel/event.rs b/src/channel/event.rs index f3dca3e..a5739f9 100644 --- a/src/channel/event.rs +++ b/src/channel/event.rs @@ -14,7 +14,7 @@ pub enum Event { impl Sequenced for Event { fn instant(&self) -> Instant { match self { - Self::Created(event) => event.instant, + Self::Created(event) => event.channel.created, Self::Deleted(event) => event.instant, } } @@ -23,8 +23,6 @@ impl Sequenced for Event { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Created { #[serde(flatten)] - pub instant: Instant, - #[serde(flatten)] pub channel: Channel, } diff --git a/src/channel/history.rs b/src/channel/history.rs index 4af46ce..faf6a0e 100644 --- a/src/channel/history.rs +++ b/src/channel/history.rs @@ -9,7 +9,6 @@ use crate::event::{Instant, Sequence}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct History { pub channel: Channel, - pub created: Instant, pub deleted: Option<Instant>, } @@ -50,7 +49,6 @@ impl History { fn created(&self) -> Event { Created { - instant: self.created, channel: self.channel.clone(), } .into() diff --git a/src/channel/repo.rs b/src/channel/repo.rs index 91f245b..812a259 100644 --- a/src/channel/repo.rs +++ b/src/channel/repo.rs @@ -57,11 +57,11 @@ impl Channels<'_> { let channel = History { channel: Channel { + created, id, name: name.clone(), deleted_at: None, }, - created, deleted: None, }; @@ -91,11 +91,11 @@ impl Channels<'_> { .map(|row| { Ok::<_, name::Error>(History { channel: Channel { + created: Instant::new(row.created_at, row.created_sequence), id: row.id, name: Name::optional(row.display_name, row.canonical_name)?.unwrap_or_default(), deleted_at: row.deleted_at, }, - created: Instant::new(row.created_at, row.created_sequence), deleted: Instant::optional(row.deleted_at, row.deleted_sequence), }) }) @@ -129,11 +129,11 @@ impl Channels<'_> { .map(|row| { Ok::<_, name::Error>(History { channel: Channel { + created: Instant::new(row.created_at, row.created_sequence), id: row.id, name: Name::optional(row.display_name, row.canonical_name)?.unwrap_or_default(), deleted_at: row.deleted_at, }, - created: Instant::new(row.created_at, row.created_sequence), deleted: Instant::optional(row.deleted_at, row.deleted_sequence), }) }) @@ -168,11 +168,11 @@ impl Channels<'_> { .map(|row| { Ok::<_, name::Error>(History { channel: Channel { + created: Instant::new(row.created_at, row.created_sequence), id: row.id, name: Name::optional(row.display_name, row.canonical_name)?.unwrap_or_default(), deleted_at: row.deleted_at, }, - created: Instant::new(row.created_at, row.created_sequence), deleted: Instant::optional(row.deleted_at, row.deleted_sequence), }) }) @@ -299,11 +299,11 @@ impl Channels<'_> { .map(|row| { Ok::<_, name::Error>(History { channel: Channel { + created: Instant::new(row.created_at, row.created_sequence), id: row.id, name: Name::optional(row.display_name, row.canonical_name)?.unwrap_or_default(), deleted_at: row.deleted_at, }, - created: Instant::new(row.created_at, row.created_sequence), deleted: Instant::optional(row.deleted_at, row.deleted_sequence), }) }) diff --git a/src/channel/snapshot.rs b/src/channel/snapshot.rs index 046ac38..96801b8 100644 --- a/src/channel/snapshot.rs +++ b/src/channel/snapshot.rs @@ -2,10 +2,12 @@ use super::{ Id, event::{Created, Event}, }; -use crate::{clock::DateTime, name::Name}; +use crate::{clock::DateTime, event::Instant, name::Name}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Channel { + #[serde(flatten)] + pub created: Instant, pub id: Id, pub name: Name, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/ui/lib/session.svelte.js b/ui/lib/session.svelte.js index 21a391d..2dae3c4 100644 --- a/ui/lib/session.svelte.js +++ b/ui/lib/session.svelte.js @@ -6,16 +6,62 @@ import * as api from './apiServer.js'; import * as r from './state/remote/state.svelte.js'; import * as l from './state/local/channels.svelte.js'; import { Watchdog } from './watchdog.js'; +import { DateTime } from 'luxon'; +import { render } from '$lib/markdown.js'; + +class Channel { + static fromRemote({ at, id, name }, messages, meta) { + const sentAt = messages + .filter((message) => message.channel === id) + .map((message) => message.at); + const lastEventAt = Math.max(at, ...sentAt); + const lastReadAt = meta.get(id)?.lastReadAt; + + const hasUnreads = lastReadAt === undefined || lastEventAt > lastReadAt; + return new Channel({ at, id, name, hasUnreads }); + } + + constructor({ at, id, name, hasUnreads }) { + this.at = at; + this.id = id; + this.name = name; + this.hasUnreads = hasUnreads; + } +} + +class Message { + static fromRemote({ id, at, channel, sender, body, renderedBody }, users) { + return new Message({ + id, + at, + channel, + sender: users.get(sender), + body, + renderedBody + }); + } + + constructor({ id, at, channel, sender, body, renderedBody }) { + this.id = id; + this.at = at; + this.channel = channel; + this.sender = sender; + this.body = body; + this.renderedBody = renderedBody; + } +} class Session { remote = $state(); local = $state(); currentUser = $derived(this.remote.currentUser); users = $derived(this.remote.users.all); - channels = $derived(this.remote.channels.all); messages = $derived( - this.remote.messages.all.map((message) => - message.resolve({ sender: (id) => this.users.get(id) }) + this.remote.messages.all.map((message) => Message.fromRemote(message, this.users)) + ); + channels = $derived( + this.remote.channels.all.map((channel) => + Channel.fromRemote(channel, this.messages, this.local.all) ) ); diff --git a/ui/lib/state/remote/channels.svelte.js b/ui/lib/state/remote/channels.svelte.js index 64edb09..8b190dd 100644 --- a/ui/lib/state/remote/channels.svelte.js +++ b/ui/lib/state/remote/channels.svelte.js @@ -1,10 +1,26 @@ -import { SvelteMap } from 'svelte/reactivity'; +import { DateTime } from 'luxon'; + +class Channel { + static boot({ at, id, name }) { + return new Channel({ + at: DateTime.fromISO(at), + id, + name + }); + } + + constructor({ at, id, name }) { + this.at = at; + this.id = id; + this.name = name; + } +} export class Channels { - all = $state(); + all = $state([]); static boot(channels) { - const all = new SvelteMap(channels.map((channel) => [channel.id, channel])); + const all = channels.map((channel) => Channel.boot(channel)); return new Channels({ all }); } @@ -12,8 +28,8 @@ export class Channels { this.all = all; } - add({ id, name }) { - this.all.set(id, { id, name }); + add({ at, id, name }) { + this.all.set(id, Channel.boot({ at, id, name })); } remove(id) { diff --git a/ui/lib/state/remote/messages.svelte.js b/ui/lib/state/remote/messages.svelte.js index c6d31f0..0a081bb 100644 --- a/ui/lib/state/remote/messages.svelte.js +++ b/ui/lib/state/remote/messages.svelte.js @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import { render } from '$lib/markdown.js'; -export class Message { +class Message { static boot({ id, at, channel, sender, body }) { return new Message({ id, @@ -21,11 +21,6 @@ export class Message { this.body = body; this.renderedBody = renderedBody; } - - resolve(get) { - const { sender, ...rest } = this; - return new Message({ sender: get.sender(sender), ...rest }); - } } export class Messages { diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index a4ae442..c7e1f22 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -22,27 +22,7 @@ let pageContext = getContext('page'); let channel = $derived(page.params.channel); - let rawChannels = $derived(session.channels); - let rawChannelsMeta = $derived(session.local.all); - let rawMessages = $derived(session.messages); - - function enrichChannels(channels, channelsMeta, messages) { - const enrichedChannels = []; - for (const ch of channels.values()) { - const channelMessages = messages.filter((message) => message.channel === ch.id); - const lastMessage = channelMessages.slice(-1)[0]; - const lastMessageAt = lastMessage?.at; - const lastReadAt = channelsMeta.get(ch.id)?.lastReadAt; - const hasUnreads = lastReadAt === undefined || lastMessageAt > lastReadAt; - enrichedChannels.push({ - ...ch, - hasUnreads - }); - } - return enrichedChannels; - } - - const enrichedChannels = $derived(enrichChannels(rawChannels, rawChannelsMeta, rawMessages)); + let channels = $derived(session.channels); function setUpGestures() { if (!browser) { @@ -134,7 +114,7 @@ <div id="interface"> <nav id="sidebar" data-expanded={pageContext.showMenu}> - <ChannelList active={channel} channels={enrichedChannels} /> + <ChannelList active={channel} {channels} /> <div class="create-channel"> <CreateChannelForm {createChannel} /> </div> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 33a9bdf..76bb638 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -10,9 +10,10 @@ const { session, outbox } = data; let activeChannel; - const channel = $derived(page.params.channel); - const messages = $derived(session.messages.filter((message) => message.channel === channel)); - const unsent = $derived(outbox.messages.filter((message) => message.channel === channel)); + const channelId = $derived(page.params.channel); + const channel = $derived(session.channels.find((channel) => channel.id === channelId)); + const messages = $derived(session.messages.filter((message) => message.channel === channelId)); + const unsent = $derived(outbox.messages.filter((message) => message.channel === channelId)); const deleted = $derived(outbox.deleted.map((message) => message.messageId)); const unsentSkeletons = $derived( unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))) @@ -45,9 +46,9 @@ function setLastRead() { const lastInView = getLastVisibleMessage(); - if (lastInView) { - const at = DateTime.fromISO(lastInView.dataset.at); - session.local.updateLastReadAt(channel, at); + const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : channel?.at; + if (!!at) { + session.local.updateLastReadAt(channelId, at); } } @@ -76,7 +77,7 @@ } async function sendMessage(message) { - outbox.postToChannel(channel, message); + outbox.postToChannel(channelId, message); } async function deleteMessage(id) { |
