summaryrefslogtreecommitdiff
path: root/ui/lib/state/local
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-07-01 15:40:11 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-07-03 22:43:44 -0400
commit9b38cb1a62ede4900fde4ba47a7b065db329e994 (patch)
treeabf0b9d993ef03a53903aae03f375b78473952da /ui/lib/state/local
parent1cafeb5ec92c1dc4ad74fbed58b15a8ab2f3c0cf (diff)
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.
Diffstat (limited to 'ui/lib/state/local')
-rw-r--r--ui/lib/state/local/channels.svelte.js118
-rw-r--r--ui/lib/state/local/conversations.svelte.js119
2 files changed, 119 insertions, 118 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);
+ }
+}