diff options
Diffstat (limited to 'ui/lib/state/local/conversations.svelte.js')
| -rw-r--r-- | ui/lib/state/local/conversations.svelte.js | 119 |
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); + } +} |
