summaryrefslogtreecommitdiff
path: root/ui/lib/state/local/conversations.svelte.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/lib/state/local/conversations.svelte.js')
-rw-r--r--ui/lib/state/local/conversations.svelte.js119
1 files changed, 119 insertions, 0 deletions
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);
+ }
+}