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); } }