diff options
| -rw-r--r-- | ui/lib/constants.js | 3 | ||||
| -rw-r--r-- | ui/lib/store.js | 11 | ||||
| -rw-r--r-- | ui/lib/store/channels.svelte.js | 98 | ||||
| -rw-r--r-- | ui/routes/(app)/+layout.svelte | 43 | ||||
| -rw-r--r-- | ui/routes/(app)/ch/[channel]/+page.svelte | 31 | ||||
| -rw-r--r-- | ui/routes/+layout.svelte | 2 |
6 files changed, 140 insertions, 48 deletions
diff --git a/ui/lib/constants.js b/ui/lib/constants.js new file mode 100644 index 0000000..f854f5d --- /dev/null +++ b/ui/lib/constants.js @@ -0,0 +1,3 @@ +export const EPOCH_STRING = '1970-01-01T00:00:00Z'; + +export const STORE_KEY_CHANNELS_DATA = 'pilcrow:channelsData'; diff --git a/ui/lib/store.js b/ui/lib/store.js index c179dac..57b5cce 100644 --- a/ui/lib/store.js +++ b/ui/lib/store.js @@ -1,11 +1,18 @@ import { writable } from 'svelte/store'; -import { Channels } from '$lib/store/channels.svelte.js'; +import { browser } from '$app/environment'; +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 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()); +export const channelsMetaList = writable(new ChannelsMeta({ channelsMetaData })); +export const channelsList = writable(new Channels({ channelsMetaList })); export const messages = writable(new Messages()); export function onEvent(event) { diff --git a/ui/lib/store/channels.svelte.js b/ui/lib/store/channels.svelte.js index c82f9aa..9058d86 100644 --- a/ui/lib/store/channels.svelte.js +++ b/ui/lib/store/channels.svelte.js @@ -1,39 +1,31 @@ import { DateTime } from 'luxon'; -const EPOCH_STRING = '1970-01-01T00:00:00Z'; - -// 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 }) { - return { - id, - name, - lastReadAt: lastReadAt || DateTime.fromISO(EPOCH_STRING), - draft, - scrollPosition - }; -} +import { get } from 'svelte/store'; +import { STORE_KEY_CHANNELS_DATA, EPOCH_STRING } from '$lib/constants'; export class Channels { channels = $state([]); + constructor({ channelsMetaList }) { + // This is the state wrapper around the channels meta object. Dammit. + this.channelsMetaList = channelsMetaList; + } + getChannel(channelId) { return this.channels.filter((ch) => ch.id === channelId)[0] || null; } setChannels(channels) { - this.channels = channels.map(makeChannelObject); + // 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; } @@ -57,3 +49,67 @@ 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 02f7d19..d1bd7d0 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -1,13 +1,21 @@ <script> - import { page } from '$app/state'; - import { goto } from '$app/navigation'; - import { browser } from '$app/environment'; import { getContext, onDestroy, onMount } from 'svelte'; + + import { browser } from '$app/environment'; + import { goto, afterNavigate } from '$app/navigation'; + import { page } from '$app/state'; + import TinyGesture from 'tinygesture'; import * as api from '$lib/apiServer.js'; - import { channelsList, currentUser, logins, messages, onEvent } from '$lib/store'; - + import { + channelsList, + channelsMetaList, + currentUser, + logins, + messages, + onEvent + } from '$lib/store'; import ChannelList from '$lib/components/ChannelList.svelte'; import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; @@ -20,11 +28,14 @@ let channel = $derived(page.params.channel); let rawChannels = $derived($channelsList.channels); + let rawChannelsMeta = $derived($channelsMetaList.channelsMeta); let rawMessages = $derived($messages); let enrichedChannels = $derived.by(() => { const channels = rawChannels; + const channelsMeta = rawChannelsMeta; const messages = rawMessages; + const enrichedChannels = []; if (channels && messages) { for (let ch of channels) { @@ -32,7 +43,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 @@ -110,6 +121,26 @@ return ''; } + const STORE_KEY_LAST_ACTIVE = 'pilcrow:lastActiveChannel'; + + function getLastActiveChannel() { + return browser && JSON.parse(localStorage.getItem(STORE_KEY_LAST_ACTIVE)); + } + + function setLastActiveChannel(channelId) { + browser && localStorage.setItem(STORE_KEY_LAST_ACTIVE, JSON.stringify(channelId)); + } + + afterNavigate(() => { + const lastActiveChannel = getLastActiveChannel(); + const inRoot = page.url.pathname === '/'; + if (inRoot && lastActiveChannel) { + goto(`/ch/${lastActiveChannel}`); + } else if (channel) { + setLastActiveChannel(channel || null); + } + }); + async function createChannel(name) { await api.createChannel(name); } diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 8de9859..095e66a 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/state'; import ActiveChannel from '$lib/components/ActiveChannel.svelte'; import MessageInput from '$lib/components/MessageInput.svelte'; - import { channelsList, currentUser, logins, messages } from '$lib/store'; + import { channelsList, channelsMetaList, currentUser, logins, messages } from '$lib/store'; import * as api from '$lib/apiServer'; let channel = $derived(page.params.channel); @@ -34,27 +34,22 @@ } function getLastVisibleMessage() { - const parentElement = activeChannel; - const childElements = parentElement.getElementsByClassName('message'); - const lastInView = Array.from(childElements) - .reverse() - .find((el) => { - return inView(parentElement, el); - }); - return lastInView; + if (activeChannel) { + const childElements = activeChannel.getElementsByClassName('message'); + const lastInView = Array.from(childElements) + .reverse() + .find((el) => { + return inView(activeChannel, el); + }); + return lastInView; + } } function setLastRead() { - const channelObject = $channelsList.getChannel(channel); const lastInView = getLastVisibleMessage(); - if (!channelObject || !lastInView) { - return; - } - const at = DateTime.fromISO(lastInView.dataset.at); - // 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; + if (lastInView) { + const at = DateTime.fromISO(lastInView.dataset.at); + $channelsMetaList.updateLastReadAt(channel, at); } } diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte index 750f1f8..6c19a95 100644 --- a/ui/routes/+layout.svelte +++ b/ui/routes/+layout.svelte @@ -1,9 +1,9 @@ <script> import { setContext } from 'svelte'; import { onNavigate } from '$app/navigation'; + import { page } from '$app/stores'; import '../app.css'; import logo from '$lib/assets/logo.png'; - import { currentUser } from '$lib/store'; let pageContext = $state({ |
