1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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);
}
}
|