diff options
Diffstat (limited to 'ui/lib/state')
| -rw-r--r-- | ui/lib/state/local/channels.svelte.js | 118 | ||||
| -rw-r--r-- | ui/lib/state/remote/channels.svelte.js | 22 | ||||
| -rw-r--r-- | ui/lib/state/remote/logins.svelte.js | 18 | ||||
| -rw-r--r-- | ui/lib/state/remote/messages.svelte.js | 52 | ||||
| -rw-r--r-- | ui/lib/state/remote/state.svelte.js | 89 |
5 files changed, 299 insertions, 0 deletions
diff --git a/ui/lib/state/local/channels.svelte.js b/ui/lib/state/local/channels.svelte.js new file mode 100644 index 0000000..9040685 --- /dev/null +++ b/ui/lib/state/local/channels.svelte.js @@ -0,0 +1,118 @@ +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(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(channelIds) { + 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(); + console.log(this, stored); + 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/remote/channels.svelte.js b/ui/lib/state/remote/channels.svelte.js new file mode 100644 index 0000000..64edb09 --- /dev/null +++ b/ui/lib/state/remote/channels.svelte.js @@ -0,0 +1,22 @@ +import { SvelteMap } from 'svelte/reactivity'; + +export class Channels { + all = $state(); + + static boot(channels) { + const all = new SvelteMap(channels.map((channel) => [channel.id, channel])); + return new Channels({ all }); + } + + constructor({ all }) { + this.all = all; + } + + add({ id, name }) { + this.all.set(id, { id, name }); + } + + remove(id) { + this.all.delete(id); + } +} diff --git a/ui/lib/state/remote/logins.svelte.js b/ui/lib/state/remote/logins.svelte.js new file mode 100644 index 0000000..d19068d --- /dev/null +++ b/ui/lib/state/remote/logins.svelte.js @@ -0,0 +1,18 @@ +import { SvelteMap } from 'svelte/reactivity'; + +export class Logins { + all = $state(); + + static boot(logins) { + const all = new SvelteMap(logins.map((login) => [login.id, login])); + return new Logins({ all }); + } + + constructor({ all }) { + this.all = all; + } + + add({ id, name }) { + this.all.set(id, { id, name }); + } +} diff --git a/ui/lib/state/remote/messages.svelte.js b/ui/lib/state/remote/messages.svelte.js new file mode 100644 index 0000000..576a74e --- /dev/null +++ b/ui/lib/state/remote/messages.svelte.js @@ -0,0 +1,52 @@ +import { DateTime } from 'luxon'; +import { render } from '$lib/markdown.js'; + +class Message { + static boot({ id, at, channel, sender, body }) { + return new Message({ + id, + at: DateTime.fromISO(at), + channel, + sender, + body, + renderedBody: render(body) + }); + } + + constructor({ id, at, channel, sender, body, renderedBody }) { + this.id = id; + this.at = at; + this.channel = channel; + this.sender = sender; + this.body = body; + this.renderedBody = renderedBody; + } + + resolve(get) { + const { sender, ...rest } = this; + return new Message({ sender: get.sender(sender), ...rest }); + } +} + +export class Messages { + all = $state([]); + + static boot(messages) { + const all = messages.map(Message.boot); + return new Messages({ all }); + } + + constructor({ all }) { + this.all = all; + } + + add({ id, at, channel, sender, body }) { + const message = Message.boot({ id, at, channel, sender, body }); + this.all.push(message); + } + + remove(id) { + const index = this.all.findIndex((message) => message.id === id); + this.all.splice(index, 1); + } +} diff --git a/ui/lib/state/remote/state.svelte.js b/ui/lib/state/remote/state.svelte.js new file mode 100644 index 0000000..c4daf17 --- /dev/null +++ b/ui/lib/state/remote/state.svelte.js @@ -0,0 +1,89 @@ +import { Logins } from './logins.svelte.js'; +import { Channels } from './channels.svelte.js'; +import { Messages } from './messages.svelte.js'; + +export class State { + currentUser = $state(); + logins = $state(); + channels = $state(); + messages = $state(); + + static boot({ currentUser, logins, channels, messages, resumePoint }) { + return new State({ + currentUser, + logins: Logins.boot(logins), + channels: Channels.boot(channels), + messages: Messages.boot(messages), + resumePoint + }); + } + + constructor({ currentUser, logins, channels, messages, resumePoint }) { + this.currentUser = currentUser; + this.logins = logins; + this.channels = channels; + this.messages = messages; + this.resumePoint = resumePoint; + } + + onEvent(event) { + switch (event.type) { + case 'channel': + return this.onChannelEvent(event); + case 'login': + return this.onLoginEvent(event); + case 'message': + return this.onMessageEvent(event); + } + } + + onChannelEvent(event) { + switch (event.event) { + case 'created': + return this.onChannelCreated(event); + case 'deleted': + return this.onChannelDeleted(event); + } + } + + onChannelCreated(event) { + const { id, name } = event; + this.channels.add({ id, name }); + } + + onChannelDeleted(event) { + const { id } = event; + this.channels.remove(id); + } + + onLoginEvent(event) { + switch (event.event) { + case 'created': + return this.onLoginCreated(event); + } + } + + onLoginCreated(event) { + const { id, name } = event; + this.logins.add({ id, name }); + } + + onMessageEvent(event) { + switch (event.event) { + case 'sent': + return this.onMessageSent(event); + case 'deleted': + return this.onMessageDeleted(event); + } + } + + onMessageSent(event) { + const { id, at, channel, sender, body } = event; + this.messages.add({ id, at, channel, sender, body }); + } + + onMessageDeleted(event) { + const { id } = event; + this.messages.remove(id); + } +} |
