summaryrefslogtreecommitdiff
path: root/ui/lib/state
diff options
context:
space:
mode:
Diffstat (limited to 'ui/lib/state')
-rw-r--r--ui/lib/state/local/channels.svelte.js118
-rw-r--r--ui/lib/state/local/conversations.svelte.js119
-rw-r--r--ui/lib/state/remote/conversations.svelte.js (renamed from ui/lib/state/remote/channels.svelte.js)10
-rw-r--r--ui/lib/state/remote/messages.svelte.js12
-rw-r--r--ui/lib/state/remote/state.svelte.js26
5 files changed, 143 insertions, 142 deletions
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) {