diff options
Diffstat (limited to 'ui')
22 files changed, 267 insertions, 250 deletions
@@ -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 397638c..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) { - return await apiServer.post('/channels', { name }).catch(responseError); +export async function createConversation(name) { + return await apiServer.post('/conversations', { name }).catch(responseError); } -export async function postToChannel(channelId, body) { - return await apiServer.post(`/channels/${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/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 @@ -<script> - import Channel from './Channel.svelte'; - - let { channels, active } = $props(); -</script> - -<nav class="list-nav"> - <ul> - {#each channels as channel} - <Channel {...channel} active={active === channel.id} /> - {/each} - </ul> -</nav> diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Conversation.svelte index 4f908d2..9004e50 100644 --- a/ui/lib/components/Channel.svelte +++ b/ui/lib/components/Conversation.svelte @@ -3,7 +3,7 @@ </script> <li class:active> - <a href="/ch/{id}"> + <a href="/c/{id}"> {#if hasUnreads} <span class="badge has-unreads">❦</span> {:else} 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 @@ +<script> + import Conversation from './Conversation.svelte'; + + let { conversations, active } = $props(); + + function isActive(conversation) { + return active === conversation.id; + } +</script> + +<nav class="list-nav"> + <ul> + {#each conversations as conversation} + <Conversation {...conversation} active={isActive(conversation)} /> + {/each} + </ul> +</nav> diff --git a/ui/lib/components/CreateChannelForm.svelte b/ui/lib/components/CreateConversationForm.svelte index 471c2b7..e390a78 100644 --- a/ui/lib/components/CreateChannelForm.svelte +++ b/ui/lib/components/CreateConversationForm.svelte @@ -1,5 +1,5 @@ <script> - let { createChannel = async (name) => {} } = $props(); + let { createConversation = async (name) => {} } = $props(); let name = $state(''); let disabled = $state(false); @@ -8,7 +8,7 @@ event.preventDefault(); disabled = true; try { - await createChannel(name); + await createConversation(name); event.target.reset(); } finally { disabled = false; @@ -17,6 +17,6 @@ </script> <form {onsubmit}> - <input type="text" placeholder="create channel" bind:value={name} {disabled} /> + <input type="text" placeholder="start a conversation" bind:value={name} {disabled} /> <button type="submit">➕</button> </form> 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 838401c..4430e8a 100644 --- a/ui/lib/session.svelte.js +++ b/ui/lib/session.svelte.js @@ -4,20 +4,20 @@ 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.channel === id) + .filter((message) => message.conversation === id) .map((message) => message.at); const lastEventAt = DateTime.max(at, ...sentAt); 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 }) { @@ -29,21 +29,21 @@ class Channel { } class Message { - static fromRemote({ id, at, channel, sender, body, renderedBody }, users) { + static fromRemote({ id, at, conversation, sender, body, renderedBody }, users) { return new Message({ id, at, - channel, + conversation, sender: users.get(sender), body, renderedBody, }); } - 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; @@ -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/conversations.svelte.js index 1e40075..79868f4 100644 --- a/ui/lib/state/remote/channels.svelte.js +++ b/ui/lib/state/remote/conversations.svelte.js @@ -1,8 +1,8 @@ import { DateTime } from 'luxon'; -class Channel { +class Conversation { static boot({ at, id, name }) { - return new Channel({ + return new Conversation({ at: DateTime.fromISO(at), id, name, @@ -16,14 +16,14 @@ class Channel { } } -export class Channels { +export class Conversations { all = $state([]); add({ at, id, name }) { - this.all.push(Channel.boot({ at, id, name })); + this.all.push(Conversation.boot({ at, id, name })); } remove(id) { - this.all = this.all.filter((channel) => channel.id !== id); + this.all = this.all.filter((conversation) => conversation.id !== id); } } 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..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 }) { @@ -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,23 +39,23 @@ 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 }); + this.conversations.add({ id, name }); } - onChannelDeleted(event) { + onConversationDeleted(event) { const { id } = event; - this.channels.remove(id); + this.conversations.remove(id); } onUserEvent(event) { @@ -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) { diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index c7e1f22..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.channel); + 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(`/ch/${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 @@ <div id="interface"> <nav id="sidebar" data-expanded={pageContext.showMenu}> - <ChannelList active={channel} {channels} /> - <div class="create-channel"> - <CreateChannelForm {createChannel} /> + <ConversationList active={conversationId} {conversations} /> + <div class="create-conversation"> + <CreateConversationForm {createConversation} /> </div> </nav> <main> 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 @@ -<div class="no-active-channel"> - <span class="vertical-aligner"> Please select or create a channel. </span> +<div class="no-active-conversation"> + <span class="vertical-aligner">Please select or create a conversation.</span> </div> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/c/[conversation]/+page.svelte index 87918f7..e6cd845 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/c/[conversation]/+page.svelte @@ -8,12 +8,18 @@ const { data } = $props(); const { session, outbox } = data; - let activeChannel; + let activeConversation; - 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 conversationId = $derived(page.params.conversation); + const conversation = $derived( + session.conversations.find((conversation) => conversation.id === conversationId), + ); + const messages = $derived( + session.messages.filter((message) => message.conversation === conversationId), + ); + const unsent = $derived( + outbox.messages.filter((message) => message.conversation === conversationId), + ); const deleted = $derived(outbox.deleted.map((message) => message.messageId)); const unsentSkeletons = $derived( unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))), @@ -33,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; } @@ -46,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); } } @@ -77,7 +83,7 @@ } async function sendMessage(message) { - outbox.postToChannel(channelId, message); + outbox.sendToConversation(conversationId, message); } async function deleteMessage(id) { @@ -87,7 +93,7 @@ <svelte:window onkeydown={handleKeydown} /> -<div class="active-channel" {onscroll} bind:this={activeChannel}> +<div class="active-conversation" {onscroll} bind:this={activeConversation}> {#each messageRuns as { sender, ownMessage, messages }} <MessageRun {sender} diff --git a/ui/styles/active-channel.css b/ui/styles/active-conversation.css index d6a9b42..981862b 100644 --- a/ui/styles/active-channel.css +++ b/ui/styles/active-conversation.css @@ -1,4 +1,4 @@ -.active-channel { +.active-conversation { padding-left: 1rem; padding-right: 1rem; overflow: auto; diff --git a/ui/styles/overscroll.css b/ui/styles/overscroll.css index 8898f9a..a54235c 100644 --- a/ui/styles/overscroll.css +++ b/ui/styles/overscroll.css @@ -1,8 +1,9 @@ -/* This should help minimize swipe-to-go-back behaviour, enabling our -* swipe-to-reveal-channel-menu behaviour. It won't work in all cases; in iOS -* Safari, when swiping from the screen edge, the OS gets th event and -* handles it before the browser does. -*/ +/* + * This should help minimize swipe-to-go-back behaviour, enabling our + * swipe-to-reveal-conversation-menu behaviour. It won't work in all cases; in + * iOS Safari, when swiping from the screen edge, the OS gets the event and + * handles it before the browser does. + */ html, body { overscroll-behavior-x: none; diff --git a/ui/styles/sidebar.css b/ui/styles/sidebar.css index b825545..aa0d53b 100644 --- a/ui/styles/sidebar.css +++ b/ui/styles/sidebar.css @@ -1,4 +1,4 @@ -/* Sidebar and channel selector */ +/* Sidebar and conversation selector */ #sidebar { background-color: var(--colour-navbar-bg); } @@ -38,19 +38,19 @@ color: var(--colour-navbar-hover-text); } -/* create channel form */ -.create-channel { +/* create conversation form */ +.create-conversation { padding-left: 0.5rem; } -.create-channel form { +.create-conversation form { display: flex; flex-direction: row; justify-content: flex-start; align-items: stretch; } -.create-channel input { +.create-conversation input { padding: 0.5rem; border-radius: 0.5rem 0 0 0.5rem; border: 1px solid var(--colour-input-border); @@ -60,7 +60,7 @@ color: var(--colour-input-text); } -.create-channel button { +.create-conversation button { border-radius: 0 0.5rem 0.5rem 0; border: 1px solid var(--colour-input-border); background-color: var(--colour-input-bg); diff --git a/ui/styles/variables.css b/ui/styles/variables.css index 2758aa1..99705f2 100644 --- a/ui/styles/variables.css +++ b/ui/styles/variables.css @@ -40,8 +40,8 @@ --colour-input-border: color-mix(in srgb, var(--colour-input-bg) 50%, black); --colour-input-text: var(--dark-text); - /* Active channel */ - --colour-active-channel-bg: color-mix(in srgb, var(--colour-base) 25%, white); + /* Active conversation */ + --colour-active-conversation-bg: color-mix(in srgb, var(--colour-base) 25%, white); /* MessageRun */ --colour-message-run-self-bg: color-mix(in srgb, var(--colour-base) 30%, white); diff --git a/ui/tests/lib/components/CreateChannelForm.svelte.test.js b/ui/tests/lib/components/CreateChannelForm.svelte.test.js index 197cb6b..8c7b3fb 100644 --- a/ui/tests/lib/components/CreateChannelForm.svelte.test.js +++ b/ui/tests/lib/components/CreateChannelForm.svelte.test.js @@ -1,37 +1,37 @@ import { render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; import { beforeEach, expect, test, describe, it, vi } from 'vitest'; -import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; +import CreateConversationForm from '$lib/components/CreateConversationForm.svelte'; const user = userEvent.setup(); const mocks = vi.hoisted(() => ({ - 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, |
