summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/boot.md10
-rw-r--r--src/channel/event.rs4
-rw-r--r--src/channel/history.rs2
-rw-r--r--src/channel/repo.rs10
-rw-r--r--src/channel/snapshot.rs4
-rw-r--r--ui/lib/session.svelte.js52
-rw-r--r--ui/lib/state/remote/channels.svelte.js26
-rw-r--r--ui/lib/state/remote/messages.svelte.js7
-rw-r--r--ui/routes/(app)/+layout.svelte24
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte15
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) {