summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/lib/constants.js3
-rw-r--r--ui/lib/store.js11
-rw-r--r--ui/lib/store/channels.svelte.js98
-rw-r--r--ui/routes/(app)/+layout.svelte43
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte31
-rw-r--r--ui/routes/+layout.svelte2
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({