diff options
| author | Kit La Touche <kit@transneptune.net> | 2025-02-16 22:00:57 -0500 |
|---|---|---|
| committer | Kit La Touche <kit@transneptune.net> | 2025-02-20 21:53:25 -0500 |
| commit | daaf37a1ed3760f03fceb1123ebe80de3a2f280c (patch) | |
| tree | e823603aa1683a2c07fd08dde724780147822348 /ui | |
| parent | 43af74832f9a2fa7f40dc71985eec9b0ada087dd (diff) | |
Separate channel metadata out into its own store
This is stored locally, and, while parallel to channel info, is not the
same as.
Eventually, this may hold info about moot/decayed channels, and grow
unbounded. That'll need to be addressed.
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/lib/constants.js | 1 | ||||
| -rw-r--r-- | ui/lib/store.js | 10 | ||||
| -rw-r--r-- | ui/lib/store/channels.svelte.js | 150 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 10 | ||||
| -rw-r--r-- | ui/routes/(app)/ch/[channel]/+page.svelte | 4 |
5 files changed, 102 insertions, 73 deletions
diff --git a/ui/lib/constants.js b/ui/lib/constants.js index a707c4b..c001f6d 100644 --- a/ui/lib/constants.js +++ b/ui/lib/constants.js @@ -1 +1,2 @@ export const STORE_KEY_CHANNELS_DATA = 'pilcrow:channelsData'; +export const EPOCH_STRING = '1970-01-01T00:00:00Z'; diff --git a/ui/lib/store.js b/ui/lib/store.js index f408c0c..508320f 100644 --- a/ui/lib/store.js +++ b/ui/lib/store.js @@ -1,13 +1,17 @@ import { writable } from 'svelte/store'; import { browser } from '$app/environment'; -import { Channels } from '$lib/store/channels.svelte.js'; +import { Channels, ChannelsMeta } from '$lib/store/channels.svelte.js'; import { Messages } from '$lib/store/messages.svelte.js'; import { Logins } from '$lib/store/logins'; +import { STORE_KEY_CHANNELS_DATA } from '$lib/constants'; // Get channelsList content from the local storage -const channelsData = (browser && JSON.parse(localStorage.getItem('pilcrow:channelsData'))) || {}; +const channelsMetaData = ( + browser && JSON.parse(localStorage.getItem(STORE_KEY_CHANNELS_DATA)) +) || {}; export const currentUser = writable(null); export const logins = writable(new Logins()); -export const channelsList = writable(new Channels({ channelsData })); +export const channelsMetaList = writable(new ChannelsMeta({ channelsMetaData })); +export const channelsList = writable(new Channels({ channelsMetaList })); export const messages = writable(new Messages()); diff --git a/ui/lib/store/channels.svelte.js b/ui/lib/store/channels.svelte.js index 49cc31c..86d924e 100644 --- a/ui/lib/store/channels.svelte.js +++ b/ui/lib/store/channels.svelte.js @@ -1,78 +1,28 @@ import { DateTime } from 'luxon'; -import { STORE_KEY_CHANNELS_DATA } from '$lib/constants'; -const EPOCH_STRING = '1970-01-01T00:00:00Z'; +import { get } from 'svelte/store' +import { STORE_KEY_CHANNELS_DATA, EPOCH_STRING } from '$lib/constants'; +// # Why we don't have a Channel object +// // For reasons unclear to me, a straight up class definition with a constructor // doesn't seem to work, reactively. So we resort to this. +// // Owen suggests that this sentence in the Svelte docs should make the reason // clear: +// // > If $state is used with an array or a simple object, the result is a deeply // > reactive state proxy. +// // Emphasis on "simple object". +// // --Kit -function makeChannelObject({ id, name, draft = '', lastReadAt = null, scrollPosition = null }) { - let lastReadAtParsed; - if (Boolean(lastReadAt)) { - if (typeof lastReadAt === "string") { - lastReadAtParsed = DateTime.fromISO(lastReadAt); - } else { - lastReadAtParsed = lastReadAt; - } - } else { - lastReadAtParsed = DateTime.fromISO(EPOCH_STRING); - } - return { - id, - name, - lastReadAt: lastReadAtParsed, - draft, - scrollPosition - }; -} - -function mergeLocalData(remoteData, currentData) { - let currentDataObj = currentData.reduce( - (acc, cur) => { - acc[cur.id] = cur; - return acc; - }, - {} - ); - const ret = remoteData.map( - (ch) => { - const newCh = makeChannelObject(ch); - if (Boolean(currentDataObj[ch.id])) { - newCh.lastReadAt = currentDataObj[ch.id].lastReadAt; - } - return newCh; - } - ); - return ret; -} export class Channels { channels = $state([]); - constructor({ channelsData }) { - this.channels = channelsData.map(makeChannelObject); - // On channel edits (inc 'last read' ones), write out to localstorage? - } - - writeOutToLocalStorage() { - localStorage.setItem( - STORE_KEY_CHANNELS_DATA, - JSON.stringify(this.channels) - ); - } - - updateLastReadAt(channelId, at) { - const channelObject = this.getChannel(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 (at > channelObject.lastReadAt) { - channelObject.lastReadAt = at; - this.writeOutToLocalStorage(); - } + constructor({ channelsMetaList }) { + // This is the state wrapper around the channels meta object. Dammit. + this.channelsMetaList = channelsMetaList; } getChannel(channelId) { @@ -80,16 +30,17 @@ export class Channels { } setChannels(channels) { - // This gets called, among other times, when the page is first loaded, with - // server-sent data from the `boot` endpoint. That needs to be merged with - // locally stored data! - this.channels = mergeLocalData(channels, this.channels); + // Because this is called at initialization, we need to initialize the matching meta: + get(this.channelsMetaList).ensureChannels(channels); + this.channels = channels; this.sort(); return this; } addChannel(id, name) { - this.channels = [...this.channels, makeChannelObject({ id, name })]; + const newChannel = { id, name } + this.channels = [...this.channels, newChannel]; + get(this.channelsMetaList).initializeChannel(newChannel); this.sort(); return this; } @@ -113,3 +64,70 @@ export class Channels { }); } } + +export class ChannelsMeta { + // Store channelId -> { draft = '', lastReadAt = null, scrollPosition = null } + channelsMeta = $state({}); + + constructor({ channelsMetaData }) { + const channelsMeta = objectMap(channelsMetaData, (ch) => { + let lastReadAt = ch.lastReadAt; + if (typeof lastReadAt === 'string') { + lastReadAt = DateTime.fromISO(lastReadAt); + } + if (!Boolean(lastReadAt)) { + lastReadAt = DateTime.fromISO(EPOCH_STRING); + } + return { + ...ch, + lastReadAt, + }; + }); + this.channelsMeta = channelsMeta; + } + + writeOutToLocalStorage() { + localStorage.setItem( + STORE_KEY_CHANNELS_DATA, + JSON.stringify(this.channelsMeta) + ); + } + + updateLastReadAt(channelId, at) { + const channelObject = this.getChannel(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 (at > channelObject?.lastReadAt) { + channelObject.lastReadAt = at; + this.writeOutToLocalStorage(); + } + } + + ensureChannels(channelsList) { + channelsList.forEach(({ id }) => { + this.initializeChannel(id); + }); + } + + initializeChannel(channelId) { + if (!this.channelsMeta[channelId]) { + const channelData = { + lastReadAt: null, + draft: '', + scrollPosition: null, + }; + this.channelsMeta[channelId] = channelData; + } + } + + getChannel(channelId) { + return this.channelsMeta[channelId] || null; + } +} + +function objectMap(object, mapFn) { + return Object.keys(object).reduce((result, key) => { + result[key] = mapFn(object[key]) + return result + }, {}); +} diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index cbfef54..9ade399 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -6,7 +6,7 @@ import TinyGesture from 'tinygesture'; import { boot, subscribeToEvents } from '$lib/apiServer'; - import { currentUser, logins, channelsList, messages } from '$lib/store'; + import { currentUser, logins, channelsList, channelsMetaList, messages } from '$lib/store'; import ChannelList from '$lib/components/ChannelList.svelte'; import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; @@ -23,6 +23,10 @@ channelsList.subscribe((val) => { rawChannels = val.channels; }); + let rawChannelsMeta; + channelsMetaList.subscribe((val) => { + rawChannelsMeta = val.channelsMeta; + }); let rawMessages; messages.subscribe((val) => { rawMessages = val; @@ -30,7 +34,9 @@ let enrichedChannels = $derived.by(() => { const channels = rawChannels; + const channelsMeta = rawChannelsMeta; const messages = rawMessages; + const enrichedChannels = []; if (channels && messages) { for (let ch of channels) { @@ -38,7 +44,7 @@ let lastRun = runs?.slice(-1)[0]; let lastMessage = lastRun?.messages.slice(-1)[0]; let lastMessageAt = lastMessage?.at; - let hasUnreads = lastMessageAt > ch.lastReadAt; + let hasUnreads = lastMessageAt > channelsMeta[ch.id]?.lastReadAt; enrichedChannels.push({ ...ch, hasUnreads diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index d64a8c9..7bd0e10 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; import ActiveChannel from '$lib/components/ActiveChannel.svelte'; import MessageInput from '$lib/components/MessageInput.svelte'; - import { channelsList, messages } from '$lib/store'; + import { channelsMetaList, messages } from '$lib/store'; let channel = $derived($page.params.channel); let messageRuns = $derived($messages.inChannel(channel)); @@ -36,7 +36,7 @@ const lastInView = getLastVisibleMessage(); if (lastInView) { const at = DateTime.fromISO(lastInView.dataset.at); - $channelsList.updateLastReadAt(channel, at); + $channelsMetaList.updateLastReadAt(channel, at); } } |
