generated from ryanatkn/fuz_template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Toot.svelte
303 lines (286 loc) · 8.38 KB
/
Toot.svelte
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
<script lang="ts">
import Pending_Button from '@ryanatkn/fuz/Pending_Button.svelte';
import {slide} from 'svelte/transition';
import {intersect} from 'svelte-intersect';
import type {Fetch_Value_Cache} from '@ryanatkn/belt/fetch.js';
import type {Logger} from '@ryanatkn/belt/log.js';
import type {Snippet} from 'svelte';
import Mastodon_Status_Tree from '$lib/Mastodon_Status_Tree.svelte';
import Mastodon_Status_Item from '$lib/Mastodon_Status_Item.svelte';
import Toot_Loader from '$lib/Toot_Loader.svelte';
import {load_from_storage, set_in_storage} from '$lib/storage.js';
import {parse_mastodon_status_url} from '$lib/mastodon.js';
import Toot_Input from '$lib/Toot_Input.svelte';
// TODO some of this may be broken after the Svelte 5 upgrade, the patterns are a mess
interface Props {
initial_url: string; // TODO this API is awkward, ideally it would be `url`? maybe rename `url` to `current_url` then?
url?: string;
/**
* Whether to fetch and display replies aka descendants in the status context.
*/
replies?: boolean;
/**
* Whether to fetch and display the ancestors in the status context.
*/
ancestors?: boolean;
/**
* Optional API result cache.
*/
cache?: Fetch_Value_Cache | null | undefined;
/**
* Optional logger for network calls.
*/
log?: Logger | undefined;
/**
* @readonly
*/
loading?: boolean | undefined;
/**
* @readonly
*/
load_time?: number | undefined;
storage_key?: string | undefined;
initial_show_settings?: boolean;
show_settings?: boolean;
autoload_key?: string | undefined;
initial_autoload?: boolean;
autoload?: boolean;
onreset?: () => void;
settings?: Snippet;
}
let {
initial_url, // eslint-disable-line prefer-const
url = initial_url,
replies = false, // eslint-disable-line prefer-const
ancestors = false, // eslint-disable-line prefer-const
cache, // eslint-disable-line prefer-const
log, // eslint-disable-line prefer-const
loading = $bindable(),
load_time = $bindable(),
storage_key, // eslint-disable-line prefer-const
initial_show_settings = false, // eslint-disable-line prefer-const
show_settings = $bindable(),
autoload_key = 'autoload', // eslint-disable-line prefer-const
initial_autoload = false, // eslint-disable-line prefer-const
autoload = autoload_key
? load_from_storage(autoload_key, () => initial_autoload)
: initial_autoload,
onreset, // eslint-disable-line prefer-const
settings, // eslint-disable-line prefer-const
}: Props = $props();
let loaded_status_key = $state(1);
export const reset = (): void => {
loaded_status_key++;
url = initial_url;
// these get bound but their values stick because they're optional, so reset them
loading = undefined;
load_time = undefined;
onreset?.();
};
// TODO refactor with storage helpers with serialize/parse as options, locallyStored?
const show_settings_key = $derived(storage_key && 'show_settings' + storage_key);
$effect(() => {
if (show_settings === undefined) {
show_settings = show_settings_key
? load_from_storage(show_settings_key, () => initial_show_settings)
: initial_show_settings;
}
});
$effect(() => {
if (show_settings_key) {
set_in_storage(show_settings_key, show_settings); // TODO @multiple wastefully sets on init
}
});
const toggle_settings = () => {
show_settings = !show_settings;
};
$effect(() => {
if (autoload_key) {
set_in_storage(autoload_key, autoload); // TODO @multiple wastefully sets on init and across multiple `Toot` instances if bound
}
});
const parsed = $derived(parse_mastodon_status_url(url));
const id = $derived(parsed?.status_id ?? null);
const host = $derived(parsed?.host ?? null);
const with_context = $derived(replies || ancestors);
const enable_load = $derived(loading !== false && !!host);
const enable_reset = $derived(loading !== undefined || url !== initial_url);
</script>
{#key loaded_status_key}
<Toot_Loader {host} {id} {with_context} {cache} {log} bind:loading bind:load_time>
{#snippet children({item, context, replies, load, loading, load_time})}
<!-- TODO this transition is working on my blog but not on this docs website, what's going on? I tried it on `/about` too -->
<div class="toot" class:replies transition:slide>
<div class="toot_content">
{#if ancestors && context}
<div transition:slide>
<!-- TODO style differently or something -->
{#each context.ancestors as ancestor}
<Mastodon_Status_Item item={ancestor} />
{/each}
</div>
{/if}
<div class="main_post panel">
<div class="panel bg_panel">
{#if item}
<!-- TODO Svelte 5 animation bug - keeping this one here because the alternative is a janky animation,
and it's not as bad for UX as the contentwarning one below -->
<div class="transition_wrapper" transition:slide>
<Mastodon_Status_Item {item} />
</div>
{:else}
<div class="transition_wrapper" transition:slide>
<Pending_Button
pending={loading || false}
disabled={!enable_load}
onclick={() => load()}
>
<div class="icon_button_content">
<div class="icon">🦣</div>
<div class="button_content">
<div>
load toot{#if replies || ancestors}s{/if} from
</div>
<code class="ellipsis"
>{#if host}{host}{:else}invalid url{/if}</code
>
</div>
</div>
</Pending_Button>
</div>
{/if}
</div>
</div>
{#if item && replies}
<!-- TODO Svelte 5 animation bug -->
<!-- <div transition:slide> -->
<Mastodon_Status_Tree {item} items={replies} />
<!-- </div> -->
{/if}
</div>
<div class="toot_controls">
<div
class="controls"
use:intersect={{
onintersect: ({intersecting}) => {
if (intersecting && autoload) load();
},
count: 1,
}}
>
<div class="row">
<button
type="button"
onclick={toggle_settings}
class="deselectable"
class:selected={show_settings}
style:margin-right="var(--space_sm)"
>
settings
</button>
<div class="reset">
<button type="button" onclick={reset} disabled={!enable_reset}>
reset
</button>{#if load_time !== undefined}<div class="loaded_message" transition:slide>
loaded in {Math.round(load_time)}ms
</div>{/if}
</div>
</div>
</div>
{#if show_settings}
<div transition:slide class="settings controls panel">
<form class="w_100">
<div class="mb_lg">
<Toot_Input bind:url />
</div>
<fieldset class="row">
<label
class="row"
title={autoload
? 'replies will load automatically when scrolled intersect'
: 'replies are not loaded until you request them'}
><input type="checkbox" bind:checked={autoload} />automatically load when
scrolled onscreen</label
>
</fieldset>
</form>
{#if settings}{@render settings()}{/if}
</div>
{/if}
</div>
</div>
{/snippet}
</Toot_Loader>
{/key}
<style>
.toot {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
gap: var(--space_md);
width: 100%;
}
.toot_content {
flex: 1;
}
.toot_controls {
display: flex;
flex-direction: column;
flex: 1;
}
.controls {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space_md);
}
.icon_button_content {
width: 100%;
display: flex;
align-items: center;
text-align: left;
}
/* TODO messy */
.icon_button_content .button_content {
overflow: hidden;
}
.button_content {
line-height: var(--line_height_lg);
}
.main_post {
padding: var(--space_md);
}
.toot.replies .main_post {
margin-bottom: var(--space_md);
}
.bg_panel {
background-color: var(--bg);
padding: var(--space_xs);
}
.icon {
font-size: var(--icon_size_md);
padding: var(--space_sm) var(--space_md) var(--space_sm) 0;
}
.reset {
display: flex;
align-items: center;
}
/* TODO hacky */
.reset :global(button) {
margin-bottom: 0;
}
.loaded_message {
margin-left: var(--space_sm);
}
.settings {
display: flex;
padding: var(--space_md);
margin-top: var(--space_md);
width: 100%;
}
.transition_wrapper {
display: flex;
flex-direction: column;
}
</style>