From 8d412732dc094ead3c5cf86c005d187f9624fc65 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 1 Jul 2025 14:24:36 -0400 Subject: Replace `channel` with `conversation` throughout the API. This is a **breaking change** for essentially all clients. Thankfully, there's presently just the one, so we don't need to go to much effort to accommoate that; the client is modified in this commit to adapt, users can reload their client, and life will go on. --- ui/lib/state/remote/messages.svelte.js | 12 ++++++------ ui/lib/state/remote/state.svelte.js | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) (limited to 'ui/lib/state/remote') diff --git a/ui/lib/state/remote/messages.svelte.js b/ui/lib/state/remote/messages.svelte.js index 1be001b..852f29e 100644 --- a/ui/lib/state/remote/messages.svelte.js +++ b/ui/lib/state/remote/messages.svelte.js @@ -2,21 +2,21 @@ import { DateTime } from 'luxon'; import { render } from '$lib/markdown.js'; class Message { - static boot({ id, at, channel, sender, body }) { + static boot({ id, at, conversation, sender, body }) { return new Message({ id, at: DateTime.fromISO(at), - channel, + conversation, sender, body, renderedBody: render(body), }); } - constructor({ id, at, channel, sender, body, renderedBody }) { + constructor({ id, at, conversation, sender, body, renderedBody }) { this.id = id; this.at = at; - this.channel = channel; + this.conversation = conversation; this.sender = sender; this.body = body; this.renderedBody = renderedBody; @@ -26,8 +26,8 @@ class Message { export class Messages { all = $state([]); - add({ id, at, channel, sender, body }) { - const message = Message.boot({ id, at, channel, sender, body }); + add({ id, at, conversation, sender, body }) { + const message = Message.boot({ id, at, conversation, sender, body }); this.all.push(message); } diff --git a/ui/lib/state/remote/state.svelte.js b/ui/lib/state/remote/state.svelte.js index fb46489..ffc88c6 100644 --- a/ui/lib/state/remote/state.svelte.js +++ b/ui/lib/state/remote/state.svelte.js @@ -30,8 +30,8 @@ export class State { // Heartbeats are actually completely ignored here. They're handled in `Session`, but not as a // special case; _any_ event is a heartbeat event. switch (event.type) { - case 'channel': - return this.onChannelEvent(event); + case 'conversation': + return this.onConversationEvent(event); case 'user': return this.onUserEvent(event); case 'message': @@ -39,21 +39,21 @@ export class State { } } - onChannelEvent(event) { + onConversationEvent(event) { switch (event.event) { case 'created': - return this.onChannelCreated(event); + return this.onConversationCreated(event); case 'deleted': - return this.onChannelDeleted(event); + return this.onConversationDeleted(event); } } - onChannelCreated(event) { + onConversationCreated(event) { const { id, name } = event; this.channels.add({ id, name }); } - onChannelDeleted(event) { + onConversationDeleted(event) { const { id } = event; this.channels.remove(id); } @@ -80,8 +80,8 @@ export class State { } onMessageSent(event) { - const { id, at, channel, sender, body } = event; - this.messages.add({ id, at, channel, sender, body }); + const { id, at, conversation, sender, body } = event; + this.messages.add({ id, at, conversation, sender, body }); } onMessageDeleted(event) { -- cgit v1.2.3 From 9b38cb1a62ede4900fde4ba47a7b065db329e994 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 1 Jul 2025 15:40:11 -0400 Subject: Rename "channel" to "conversation" throughout the client. Existing client state, stored in local storage, is migrated to new keys (that mention "conversation" instead of "channel" where appropriate) the first time the client loads. --- ui/app.css | 6 +- ui/lib/apiServer.js | 7 +- ui/lib/components/Channel.svelte | 14 --- ui/lib/components/ChannelList.svelte | 13 --- ui/lib/components/Conversation.svelte | 14 +++ ui/lib/components/ConversationList.svelte | 17 +++ ui/lib/components/CreateChannelForm.svelte | 22 ---- ui/lib/components/CreateConversationForm.svelte | 22 ++++ ui/lib/outbox.svelte.js | 24 ++--- ui/lib/session.svelte.js | 16 +-- ui/lib/state/local/channels.svelte.js | 118 -------------------- ui/lib/state/local/conversations.svelte.js | 119 +++++++++++++++++++++ ui/lib/state/remote/channels.svelte.js | 29 ----- ui/lib/state/remote/conversations.svelte.js | 29 +++++ ui/lib/state/remote/state.svelte.js | 8 +- ui/routes/(app)/+layout.svelte | 46 ++++---- ui/routes/(app)/+page.svelte | 4 +- ui/routes/(app)/c/[conversation]/+page.svelte | 28 ++--- ui/styles/active-channel.css | 6 -- ui/styles/active-conversation.css | 6 ++ ui/styles/overscroll.css | 11 +- ui/styles/sidebar.css | 12 +-- ui/styles/variables.css | 4 +- .../components/CreateChannelForm.svelte.test.js | 18 ++-- .../lib/components/MessageInput.svelte.test.js | 2 +- 25 files changed, 305 insertions(+), 290 deletions(-) delete mode 100644 ui/lib/components/Channel.svelte delete mode 100644 ui/lib/components/ChannelList.svelte create mode 100644 ui/lib/components/Conversation.svelte create mode 100644 ui/lib/components/ConversationList.svelte delete mode 100644 ui/lib/components/CreateChannelForm.svelte create mode 100644 ui/lib/components/CreateConversationForm.svelte delete mode 100644 ui/lib/state/local/channels.svelte.js create mode 100644 ui/lib/state/local/conversations.svelte.js delete mode 100644 ui/lib/state/remote/channels.svelte.js create mode 100644 ui/lib/state/remote/conversations.svelte.js delete mode 100644 ui/styles/active-channel.css create mode 100644 ui/styles/active-conversation.css (limited to 'ui/lib/state/remote') diff --git a/ui/app.css b/ui/app.css index 79f36eb..34a74b7 100644 --- a/ui/app.css +++ b/ui/app.css @@ -5,14 +5,14 @@ @import url('styles/app-bar.css'); @import url('styles/app-layout.css'); @import url('styles/sidebar.css'); -@import url('styles/active-channel.css'); +@import url('styles/active-conversation.css'); @import url('styles/messages.css'); @import url('styles/textarea.css'); @import url('styles/forms.css'); @import url('styles/invites.css'); body { - background-color: var(--colour-active-channel-bg); + background-color: var(--colour-active-conversation-bg); color: var(--dark-text); font-family: 'Roboto', sans-serif; } @@ -21,7 +21,7 @@ hr { width: 90%; } -.no-active-channel { +.no-active-conversation { display: table; height: 100%; width: 100%; diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js index 1bca6f6..ac707a5 100644 --- a/ui/lib/apiServer.js +++ b/ui/lib/apiServer.js @@ -1,7 +1,6 @@ import axios from 'axios'; import * as r from './retry.js'; -import { timedDelay } from './retry.js'; export const apiServer = axios.create({ baseURL: '/api/', @@ -27,12 +26,12 @@ export async function changePassword(password, to) { return await apiServer.post('/password', { password, to }).catch(responseError); } -export async function createChannel(name) { +export async function createConversation(name) { return await apiServer.post('/conversations', { name }).catch(responseError); } -export async function postToChannel(channelId, body) { - return await apiServer.post(`/conversations/${channelId}`, { body }).catch(responseError); +export async function sendToConversation(conversationId, body) { + return await apiServer.post(`/conversations/${conversationId}`, { body }).catch(responseError); } export async function deleteMessage(messageId) { diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte deleted file mode 100644 index 9004e50..0000000 --- a/ui/lib/components/Channel.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
  • - - {#if hasUnreads} - - {:else} - - {/if} - {name} - -
  • diff --git a/ui/lib/components/ChannelList.svelte b/ui/lib/components/ChannelList.svelte deleted file mode 100644 index 51dd6cf..0000000 --- a/ui/lib/components/ChannelList.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/ui/lib/components/Conversation.svelte b/ui/lib/components/Conversation.svelte new file mode 100644 index 0000000..9004e50 --- /dev/null +++ b/ui/lib/components/Conversation.svelte @@ -0,0 +1,14 @@ + + +
  • + + {#if hasUnreads} + + {:else} + + {/if} + {name} + +
  • diff --git a/ui/lib/components/ConversationList.svelte b/ui/lib/components/ConversationList.svelte new file mode 100644 index 0000000..71332e0 --- /dev/null +++ b/ui/lib/components/ConversationList.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/lib/components/CreateChannelForm.svelte b/ui/lib/components/CreateChannelForm.svelte deleted file mode 100644 index 471c2b7..0000000 --- a/ui/lib/components/CreateChannelForm.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
    - - -
    diff --git a/ui/lib/components/CreateConversationForm.svelte b/ui/lib/components/CreateConversationForm.svelte new file mode 100644 index 0000000..e390a78 --- /dev/null +++ b/ui/lib/components/CreateConversationForm.svelte @@ -0,0 +1,22 @@ + + +
    + + +
    diff --git a/ui/lib/outbox.svelte.js b/ui/lib/outbox.svelte.js index 183f8ff..c4e2324 100644 --- a/ui/lib/outbox.svelte.js +++ b/ui/lib/outbox.svelte.js @@ -4,9 +4,9 @@ import * as msg from './state/remote/messages.svelte.js'; import * as api from './apiServer.js'; import * as md from './markdown.js'; -class PostToChannel { - constructor(channel, body) { - this.channel = channel; +class SendToConversation { + constructor(conversation, body) { + this.conversation = conversation; this.body = body; this.at = DateTime.now(); this.renderedBody = md.render(body); @@ -16,7 +16,7 @@ class PostToChannel { return { id: null, at: this.at, - channel: this.channel, + conversation: this.conversation, sender, body: this.body, renderedBody: this.renderedBody, @@ -24,7 +24,7 @@ class PostToChannel { } async send() { - return await api.retry(() => api.postToChannel(this.channel, this.body)); + return await api.retry(() => api.sendToConversation(this.conversation, this.body)); } } @@ -38,19 +38,19 @@ class DeleteMessage { } } -class CreateChannel { +class CreateConversation { constructor(name) { this.name = name; } async send() { - return await api.retry(() => api.createChannel(this.name)); + return await api.retry(() => api.createConversation(this.name)); } } export class Outbox { pending = $state([]); - messages = $derived(this.pending.filter((operation) => operation instanceof PostToChannel)); + messages = $derived(this.pending.filter((operation) => operation instanceof SendToConversation)); deleted = $derived(this.pending.filter((operation) => operation instanceof DeleteMessage)); static empty() { @@ -66,12 +66,12 @@ export class Outbox { this.start(); } - createChannel(name) { - this.enqueue(new CreateChannel(name)); + createConversation(name) { + this.enqueue(new CreateConversation(name)); } - postToChannel(channel, body) { - this.enqueue(new PostToChannel(channel, body)); + sendToConversation(conversationId, body) { + this.enqueue(new SendToConversation(conversationId, body)); } deleteMessage(messageId) { diff --git a/ui/lib/session.svelte.js b/ui/lib/session.svelte.js index 0c73e00..4430e8a 100644 --- a/ui/lib/session.svelte.js +++ b/ui/lib/session.svelte.js @@ -4,11 +4,11 @@ import { goto } from '$app/navigation'; 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 * as l from './state/local/conversations.svelte.js'; import { Watchdog } from './watchdog.js'; import { DateTime } from 'luxon'; -class Channel { +class Conversation { static fromRemote({ at, id, name }, messages, meta) { const sentAt = messages .filter((message) => message.conversation === id) @@ -17,7 +17,7 @@ class Channel { const lastReadAt = meta.get(id)?.lastReadAt; const hasUnreads = lastReadAt === undefined || lastEventAt > lastReadAt; - return new Channel({ at, id, name, hasUnreads }); + return new Conversation({ at, id, name, hasUnreads }); } constructor({ at, id, name, hasUnreads }) { @@ -58,9 +58,9 @@ class Session { messages = $derived( 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), + conversations = $derived( + this.remote.conversations.all.map((conversation) => + Conversation.fromRemote(conversation, this.messages, this.local.all), ), ); @@ -71,7 +71,7 @@ class Session { heartbeat, events, }); - const local = l.Channels.fromLocalStorage(); + const local = l.Conversations.fromLocalStorage(); return new Session(remote, local); } @@ -109,7 +109,7 @@ class Session { onMessage(message) { const event = JSON.parse(message.data); this.remote.onEvent(event); - this.local.retainChannels(this.remote.channels.all); + this.local.retainConversations(this.remote.conversations.all); this.watchdog.reset(this.heartbeatMillis()); } diff --git a/ui/lib/state/local/channels.svelte.js b/ui/lib/state/local/channels.svelte.js deleted file mode 100644 index 669aa1e..0000000 --- a/ui/lib/state/local/channels.svelte.js +++ /dev/null @@ -1,118 +0,0 @@ -import { DateTime } from 'luxon'; -import { SvelteMap } from 'svelte/reactivity'; - -import * as iter from '$lib/iterator.js'; - -export const STORE_KEY_CHANNELS_DATA = 'pilcrow:channelsData'; - -class Channel { - draft = $state(); - lastReadAt = $state(null); - scrollPosition = $state(null); - - static fromStored({ draft, lastReadAt, scrollPosition }) { - return new Channel({ - draft, - lastReadAt: lastReadAt == null ? null : DateTime.fromISO(lastReadAt), - scrollPosition, - }); - } - - constructor({ draft = '', lastReadAt = null, scrollPosition = null } = {}) { - this.draft = draft; - this.lastReadAt = lastReadAt; - this.scrollPosition = scrollPosition; - } - - toStored() { - const { draft, lastReadAt, scrollPosition } = this; - return { - draft, - lastReadAt: lastReadAt?.toISO(), - scrollPosition, - }; - } -} - -export class Channels { - // Store channelId -> { draft = '', lastReadAt = null, scrollPosition = null } - all = $state(); - - static fromLocalStorage() { - const stored = localStorage.getItem(STORE_KEY_CHANNELS_DATA); - if (stored !== null) { - return Channels.fromStored(JSON.parse(stored)); - } - return Channels.empty(); - } - - static fromStored(stored) { - const loaded = Object.keys(stored).map((channelId) => [ - channelId, - Channel.fromStored(stored[channelId]), - ]); - const all = new SvelteMap(loaded); - return new Channels({ all }); - } - - static empty() { - return new Channels({ all: new SvelteMap() }); - } - - constructor({ all }) { - this.all = all; - } - - channel(channelId) { - let channel = this.all.get(channelId); - if (channel === undefined) { - channel = new Channel(); - this.all.set(channelId, channel); - } - return channel; - } - - updateLastReadAt(channelId, at) { - const channel = this.channel(channelId); - // Do it this way, rather than with Math.max tricks, to avoid assignment - // when we don't need it, to minimize reactive changes: - if (channel.lastReadAt === null || at > channel.lastReadAt) { - channel.lastReadAt = at; - this.save(); - } - } - - retainChannels(channels) { - const channelIds = channels.map((channel) => channel.id); - const retain = new Set(channelIds); - for (const channelId of Array.from(this.all.keys())) { - if (!retain.has(channelId)) { - this.all.delete(channelId); - } - } - this.save(); - } - - toStored() { - return iter.reduce( - this.all.entries(), - (stored, [channelId, channel]) => ({ - ...stored, - [channelId]: channel.toStored(), - }), - {}, - ); - } - - save() { - let stored = this.toStored(); - localStorage.setItem(STORE_KEY_CHANNELS_DATA, JSON.stringify(stored)); - } -} - -function objectMap(object, mapFn) { - return Object.keys(object).reduce((result, key) => { - result[key] = mapFn(object[key]); - return result; - }, {}); -} diff --git a/ui/lib/state/local/conversations.svelte.js b/ui/lib/state/local/conversations.svelte.js new file mode 100644 index 0000000..835c237 --- /dev/null +++ b/ui/lib/state/local/conversations.svelte.js @@ -0,0 +1,119 @@ +import { DateTime } from 'luxon'; +import { SvelteMap } from 'svelte/reactivity'; + +import * as iter from '$lib/iterator.js'; + +// Conversations were called "channels" in previous iterations. Support loading +// data saved under that name to prevent the change from resetting everyone's +// unread tracking. +export const STORE_KEY_CHANNELS = 'pilcrow:channelsData'; +export const STORE_KEY_CONVERSATIONS = 'pilcrow:conversations'; + +class Conversation { + draft = $state(); + lastReadAt = $state(null); + scrollPosition = $state(null); + + static fromStored({ draft, lastReadAt, scrollPosition }) { + return new Conversation({ + draft, + lastReadAt: lastReadAt == null ? null : DateTime.fromISO(lastReadAt), + scrollPosition, + }); + } + + constructor({ draft = '', lastReadAt = null, scrollPosition = null } = {}) { + this.draft = draft; + this.lastReadAt = lastReadAt; + this.scrollPosition = scrollPosition; + } + + toStored() { + const { draft, lastReadAt, scrollPosition } = this; + return { + draft, + lastReadAt: lastReadAt?.toISO(), + scrollPosition, + }; + } +} + +export class Conversations { + // Store conversationId -> { draft = '', lastReadAt = null, scrollPosition = null } + all = $state(); + + static fromLocalStorage() { + const stored = + localStorage.getItem(STORE_KEY_CONVERSATIONS) ?? localStorage.getItem(STORE_KEY_CHANNELS); + if (stored !== null) { + return Conversations.fromStored(JSON.parse(stored)); + } + return Conversations.empty(); + } + + static fromStored(stored) { + const loaded = Object.keys(stored).map((conversationId) => [ + conversationId, + Conversation.fromStored(stored[conversationId]), + ]); + const all = new SvelteMap(loaded); + return new Conversations({ all }); + } + + static empty() { + return new Conversations({ all: new SvelteMap() }); + } + + constructor({ all }) { + this.all = all; + } + + conversation(conversationId) { + let conversation = this.all.get(conversationId); + if (conversation === undefined) { + conversation = new Conversation(); + this.all.set(conversationId, conversation); + } + return conversation; + } + + updateLastReadAt(conversationId, at) { + const conversation = this.conversation(conversationId); + // Do it this way, rather than with Math.max tricks, to avoid assignment + // when we don't need it, to minimize reactive changes: + if (conversation.lastReadAt === null || at > conversation.lastReadAt) { + conversation.lastReadAt = at; + this.save(); + } + } + + retainConversations(conversations) { + const conversationIds = conversations.map((conversation) => conversation.id); + const retain = new Set(conversationIds); + for (const conversationId of Array.from(this.all.keys())) { + if (!retain.has(conversationId)) { + this.all.delete(conversationId); + } + } + this.save(); + } + + toStored() { + return iter.reduce( + this.all.entries(), + (stored, [conversationId, conversation]) => ({ + ...stored, + [conversationId]: conversation.toStored(), + }), + {}, + ); + } + + save() { + let stored = this.toStored(); + localStorage.setItem(STORE_KEY_CONVERSATIONS, JSON.stringify(stored)); + // If we were able to save the data under `pilcrow:conversations`, then remove the old data; + // it is no longer needed and wouldn't be loaded anyways. + localStorage.removeItem(STORE_KEY_CHANNELS); + } +} diff --git a/ui/lib/state/remote/channels.svelte.js b/ui/lib/state/remote/channels.svelte.js deleted file mode 100644 index 1e40075..0000000 --- a/ui/lib/state/remote/channels.svelte.js +++ /dev/null @@ -1,29 +0,0 @@ -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([]); - - add({ at, id, name }) { - this.all.push(Channel.boot({ at, id, name })); - } - - remove(id) { - this.all = this.all.filter((channel) => channel.id !== id); - } -} diff --git a/ui/lib/state/remote/conversations.svelte.js b/ui/lib/state/remote/conversations.svelte.js new file mode 100644 index 0000000..79868f4 --- /dev/null +++ b/ui/lib/state/remote/conversations.svelte.js @@ -0,0 +1,29 @@ +import { DateTime } from 'luxon'; + +class Conversation { + static boot({ at, id, name }) { + return new Conversation({ + at: DateTime.fromISO(at), + id, + name, + }); + } + + constructor({ at, id, name }) { + this.at = at; + this.id = id; + this.name = name; + } +} + +export class Conversations { + all = $state([]); + + add({ at, id, name }) { + this.all.push(Conversation.boot({ at, id, name })); + } + + remove(id) { + this.all = this.all.filter((conversation) => conversation.id !== id); + } +} diff --git a/ui/lib/state/remote/state.svelte.js b/ui/lib/state/remote/state.svelte.js index ffc88c6..3d65e4a 100644 --- a/ui/lib/state/remote/state.svelte.js +++ b/ui/lib/state/remote/state.svelte.js @@ -1,11 +1,11 @@ import { User, Users } from './users.svelte.js'; -import { Channels } from './channels.svelte.js'; +import { Conversations } from './conversations.svelte.js'; import { Messages } from './messages.svelte.js'; export class State { currentUser = $state(); users = $state(new Users()); - channels = $state(new Channels()); + conversations = $state(new Conversations()); messages = $state(new Messages()); static boot({ currentUser, heartbeat, resumePoint, events }) { @@ -50,12 +50,12 @@ export class State { onConversationCreated(event) { const { id, name } = event; - this.channels.add({ id, name }); + this.conversations.add({ id, name }); } onConversationDeleted(event) { const { id } = event; - this.channels.remove(id); + this.conversations.remove(id); } onUserEvent(event) { diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index e3272bc..658d966 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -7,9 +7,8 @@ import TinyGesture from 'tinygesture'; - import * as api from '$lib/apiServer.js'; - import ChannelList from '$lib/components/ChannelList.svelte'; - import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; + import ConversationList from '$lib/components/ConversationList.svelte'; + import CreateConversationForm from '$lib/components/CreateConversationForm.svelte'; let gesture = null; @@ -20,9 +19,9 @@ onDestroy(session.end.bind(session)); let pageContext = getContext('page'); - let channel = $derived(page.params.conversation); + let conversationId = $derived(page.params.conversation); - let channels = $derived(session.channels); + let conversations = $derived(session.conversations); function setUpGestures() { if (!browser) { @@ -46,28 +45,35 @@ } }); - const STORE_KEY_LAST_ACTIVE = 'pilcrow:lastActiveChannel'; + // Automatically migrate last-active-channel info now that we call them "conversations." + const STORE_KEY_LAST_ACTIVE = 'pilcrow:lastActiveConversation'; + const STORE_KEY_LAST_ACTIVE_CHANNEL = 'pilcrow:lastActiveChannel'; - function getLastActiveChannel() { - return browser && JSON.parse(localStorage.getItem(STORE_KEY_LAST_ACTIVE)); + function getLastActiveConversation() { + const stored = + localStorage.getItem(STORE_KEY_LAST_ACTIVE) ?? + localStorage.getItem(STORE_KEY_LAST_ACTIVE_CHANNEL); + return JSON.parse(stored); } - function setLastActiveChannel(channelId) { - browser && localStorage.setItem(STORE_KEY_LAST_ACTIVE, JSON.stringify(channelId)); + function setLastActiveConversation(conversationId) { + localStorage.setItem(STORE_KEY_LAST_ACTIVE, JSON.stringify(conversationId)); + // Once we've saved to the new key, we no longer need the old one. Clean it up. + localStorage.removeItem(STORE_KEY_LAST_ACTIVE_CHANNEL); } afterNavigate(() => { - const lastActiveChannel = getLastActiveChannel(); + const conversationId = getLastActiveConversation(); const inRoot = page.url.pathname === '/'; - if (inRoot && lastActiveChannel) { - goto(`/c/${lastActiveChannel}`); - } else if (channel) { - setLastActiveChannel(channel || null); + if (inRoot && conversationId) { + goto(`/c/${conversationId}`); + } else if (conversationId) { + setLastActiveConversation(conversationId || null); } }); - async function createChannel(name) { - outbox.createChannel(name); + async function createConversation(name) { + outbox.createConversation(name); } function onbeforeunload(event) { @@ -114,9 +120,9 @@
    diff --git a/ui/routes/(app)/+page.svelte b/ui/routes/(app)/+page.svelte index 007c5c6..1db0eb2 100644 --- a/ui/routes/(app)/+page.svelte +++ b/ui/routes/(app)/+page.svelte @@ -1,3 +1,3 @@ -
    - Please select or create a channel. +
    + Please select or create a conversation.
    diff --git a/ui/routes/(app)/c/[conversation]/+page.svelte b/ui/routes/(app)/c/[conversation]/+page.svelte index 4d2cc86..e6cd845 100644 --- a/ui/routes/(app)/c/[conversation]/+page.svelte +++ b/ui/routes/(app)/c/[conversation]/+page.svelte @@ -8,14 +8,18 @@ const { data } = $props(); const { session, outbox } = data; - let activeChannel; + let activeConversation; - const channelId = $derived(page.params.conversation); - const channel = $derived(session.channels.find((channel) => channel.id === channelId)); + const conversationId = $derived(page.params.conversation); + const conversation = $derived( + session.conversations.find((conversation) => conversation.id === conversationId), + ); const messages = $derived( - session.messages.filter((message) => message.conversation === channelId), + session.messages.filter((message) => message.conversation === conversationId), + ); + const unsent = $derived( + outbox.messages.filter((message) => message.conversation === conversationId), ); - 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))), @@ -35,12 +39,12 @@ } function getLastVisibleMessage() { - if (activeChannel) { - const childElements = activeChannel.getElementsByClassName('message'); + if (activeConversation) { + const childElements = activeConversation.getElementsByClassName('message'); const lastInView = Array.from(childElements) .reverse() .find((el) => { - return inView(activeChannel, el); + return inView(activeConversation, el); }); return lastInView; } @@ -48,9 +52,9 @@ function setLastRead() { const lastInView = getLastVisibleMessage(); - const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : channel?.at; + const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : conversation?.at; if (!!at) { - session.local.updateLastReadAt(channelId, at); + session.local.updateLastReadAt(conversationId, at); } } @@ -79,7 +83,7 @@ } async function sendMessage(message) { - outbox.postToChannel(channelId, message); + outbox.sendToConversation(conversationId, message); } async function deleteMessage(id) { @@ -89,7 +93,7 @@ -
    +
    {#each messageRuns as { sender, ownMessage, messages }} ({ - createChannel: vi.fn(), + createConversation: vi.fn(), })); -describe('CreateChannelForm', async () => { +describe('CreateConversationForm', async () => { beforeEach(async () => { - render(CreateChannelForm, { - createChannel: mocks.createChannel, + render(CreateConversationForm, { + createConversation: mocks.createConversation, }); }); - describe('creates channels', async () => { + describe('creates conversations', async () => { it('with a non-empty name', async () => { const input = screen.getByRole('textbox'); - await user.type(input, 'channel name'); + await user.type(input, 'conversation name'); const create = screen.getByRole('button'); await user.click(create); - expect(mocks.createChannel).toHaveBeenCalledExactlyOnceWith('channel name'); + expect(mocks.createConversation).toHaveBeenCalledExactlyOnceWith('conversation name'); }); it('with an empty name', async () => { const create = screen.getByRole('button'); await user.click(create); - expect(mocks.createChannel).toHaveBeenCalledExactlyOnceWith(''); + expect(mocks.createConversation).toHaveBeenCalledExactlyOnceWith(''); }); }); }); diff --git a/ui/tests/lib/components/MessageInput.svelte.test.js b/ui/tests/lib/components/MessageInput.svelte.test.js index c32ce11..b459737 100644 --- a/ui/tests/lib/components/MessageInput.svelte.test.js +++ b/ui/tests/lib/components/MessageInput.svelte.test.js @@ -9,7 +9,7 @@ const mocks = vi.hoisted(() => ({ sendMessage: vi.fn(), })); -describe('CreateChannelForm', async () => { +describe('MessageInput', async () => { beforeEach(async () => { render(MessageInput, { sendMessage: mocks.sendMessage, -- cgit v1.2.3