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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
<script>
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 ChannelList from '$lib/components/ChannelList.svelte';
import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
let gesture = null;
const { data, children } = $props();
const { session, outbox } = data;
onMount(session.begin.bind(session));
onDestroy(session.end.bind(session));
let pageContext = getContext('page');
let channel = $derived(page.params.channel);
let rawChannels = $derived(session.channels);
let rawChannelsMeta = $derived(session.local.all);
let rawMessages = $derived(session.messages);
function enrichChannels(channels, channelsMeta, messages) {
const enrichedChannels = [];
for (const ch of channels.values()) {
const channelMessages = messages.filter((message) => message.channel === ch.id);
const lastMessage = channelMessages.slice(-1)[0];
const lastMessageAt = lastMessage?.at;
const lastReadAt = channelsMeta.get(ch.id)?.lastReadAt;
const hasUnreads = lastReadAt === undefined || lastMessageAt > lastReadAt;
enrichedChannels.push({
...ch,
hasUnreads
});
}
return enrichedChannels;
}
const enrichedChannels = $derived(enrichChannels(rawChannels, rawChannelsMeta, rawMessages));
function setUpGestures() {
if (!browser) {
// Meaningless if we're not in a browser, so...
return;
}
gesture = new TinyGesture(window);
gesture.on('swiperight', () => {
pageContext.showMenu = true;
});
gesture.on('swipeleft', () => {
pageContext.showMenu = false;
});
}
onMount(setUpGestures);
onDestroy(async () => {
if (gesture !== null) {
gesture.destroy();
}
});
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);
}
function onbeforeunload(event) {
if (outbox.pending.length > 0) {
// Prompt the user that they have unsaved (unsent) messages that may be lost.
event.preventDefault();
}
}
</script>
<!--
In theory, we [should be][bfcache-why] using an ephemeral event handler for this, rather than
leaving it hooked up at all times. Some browsers decide whether a page is eligible for
back/forward caching based on whether it has a beforeunload handler (among other factors), and
having the event handler registered can slow down navigation on those browsers by forcing a
network reload when the page could have been restored from memory.
Most browsers _apparently_ no longer use that criterion, but I would have been inclined to follow
the advice regardless as I don't feel up to the task of cataloguing which browsers it applies to.
Unfortunately, it wouldn't matter if we did: SvelteKit itself registers beforeunload handlers, so
(at least as of this writing) any work we do to try to dynamically register or unregister
beforeunload handlers dynamically state would be wasted effort.
For posterity, though, the appropriate code for doing so based on outbox state looks like
$effect(() => {
if (outbox.pending.length > 0) {
window.addEventListener('beforeunload', onbeforeunload);
}
return () => {
window.removeEventListener('beforeunload', onbeforeunload);
};
});
[bfcache-why]: https://web.dev/articles/bfcache#beforeunload-caution
-->
<svelte:window {onbeforeunload} />
<svelte:head>
<!-- TODO: unread count? -->
<title>pilcrow</title>
</svelte:head>
<div id="interface">
<nav id="sidebar" data-expanded={pageContext.showMenu}>
<ChannelList active={channel} channels={enrichedChannels} />
<div class="create-channel">
<CreateChannelForm {createChannel} />
</div>
</nav>
<main>
{@render children?.()}
</main>
</div>
|