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
|
<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 ConversationList from '$lib/components/ConversationList.svelte';
import CreateConversationForm from '$lib/components/CreateConversationForm.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 conversationId = $derived(page.params.conversation);
let conversations = $derived(session.conversations);
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();
}
});
// Automatically migrate last-active-channel info now that we call them "conversations."
const STORE_KEY_LAST_ACTIVE = 'pilcrow:lastActiveConversation';
const STORE_KEY_LAST_ACTIVE_CHANNEL = 'pilcrow:lastActiveChannel';
function getLastActiveConversation() {
const stored =
localStorage.getItem(STORE_KEY_LAST_ACTIVE) ??
localStorage.getItem(STORE_KEY_LAST_ACTIVE_CHANNEL);
return JSON.parse(stored);
}
function setLastActiveConversation(conversationId) {
localStorage.setItem(STORE_KEY_LAST_ACTIVE, JSON.stringify(conversationId));
// Once we've saved to the new key, we no longer need the old one. Clean it up.
localStorage.removeItem(STORE_KEY_LAST_ACTIVE_CHANNEL);
}
afterNavigate(() => {
const conversationId = getLastActiveConversation();
const inRoot = page.url.pathname === '/';
if (inRoot && conversationId) {
goto(`/c/${conversationId}`);
} else if (conversationId) {
setLastActiveConversation(conversationId || null);
}
});
async function createConversation(name) {
outbox.createConversation(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}>
<ConversationList active={conversationId} {conversations} />
<div class="create-conversation">
<CreateConversationForm {createConversation} />
</div>
</nav>
<main>
{@render children?.()}
</main>
</div>
|