summaryrefslogtreecommitdiff
path: root/ui/routes/(app)/+layout.svelte
blob: e3272bc094f842fa16e865ebbeb7421e14dfdc7a (plain)
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
<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.conversation);

  let channels = $derived(session.channels);

  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(`/c/${lastActiveChannel}`);
    } else if (channel) {
      setLastActiveChannel(channel || null);
    }
  });

  async function createChannel(name) {
    outbox.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} />
    <div class="create-channel">
      <CreateChannelForm {createChannel} />
    </div>
  </nav>
  <main>
    {@render children?.()}
  </main>
</div>