tangled
alpha
login
or
join now
mackuba.eu
/
skythread
14
fork
atom
Thread viewer for Bluesky
14
fork
atom
overview
issues
pulls
pipelines
converted more replies/hidden replies loading to svelte
mackuba.eu
3 months ago
a33fbf8d
b0ebbdb6
+180
-115
4 changed files
expand all
collapse all
unified
split
src
api
api.js
components
posts
HiddenRepliesLink.svelte
LoadMoreLink.svelte
post_component.js
+31
src/api/api.js
···
24
24
}
25
25
}
26
26
27
27
+
export class HiddenRepliesError extends Error {
28
28
+
29
29
+
/** @param {Error} error */
30
30
+
constructor(error) {
31
31
+
super(error.message);
32
32
+
this.originalError = error;
33
33
+
}
34
34
+
}
35
35
+
27
36
28
37
/**
29
38
* Caches the mapping of handles to DIDs to avoid unnecessary API calls to resolveHandle or getProfile.
···
289
298
let postGroups = await Promise.all(batches);
290
299
291
300
return { cursor: response.cursor, posts: postGroups.flat() };
301
301
+
}
302
302
+
303
303
+
/** @param {Post} post, @returns {Promise<(json | undefined)[]>} */
304
304
+
305
305
+
async loadHiddenReplies(post) {
306
306
+
let expectedReplyURIs;
307
307
+
308
308
+
try {
309
309
+
expectedReplyURIs = await blueAPI.getReplies(post.uri);
310
310
+
} catch (error) {
311
311
+
if (error instanceof APIError && error.code == 404) {
312
312
+
throw new HiddenRepliesError(error);
313
313
+
} else {
314
314
+
throw error;
315
315
+
}
316
316
+
}
317
317
+
318
318
+
let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.some(x => x.uri === r));
319
319
+
let promises = missingReplyURIs.map(uri => this.loadThreadByAtURI(uri));
320
320
+
let responses = await Promise.allSettled(promises);
321
321
+
322
322
+
return responses.map(r => (r.status == 'fulfilled') ? r.value : undefined);
292
323
}
293
324
294
325
/**
+43
src/components/posts/HiddenRepliesLink.svelte
···
1
1
+
<script>
2
2
+
import { showBiohazardDialog } from '../../skythread.js';
3
3
+
import { account } from '../../models/account.svelte.js';
4
4
+
import { linkToPostThread } from '../../router.js';
5
5
+
import { getContext } from 'svelte';
6
6
+
7
7
+
let { onLoad, onError } = $props();
8
8
+
let { post } = getContext('post');
9
9
+
let loading = $state(false);
10
10
+
11
11
+
function onLinkClick(e) {
12
12
+
e.preventDefault();
13
13
+
14
14
+
if (account.biohazardEnabled === true) {
15
15
+
loadHiddenReplies();
16
16
+
} else {
17
17
+
showBiohazardDialog(() => {
18
18
+
loadHiddenReplies();
19
19
+
});
20
20
+
}
21
21
+
}
22
22
+
23
23
+
async function loadHiddenReplies() {
24
24
+
loading = true;
25
25
+
26
26
+
try {
27
27
+
let replies = await api.loadHiddenReplies(post);
28
28
+
loading = false;
29
29
+
onLoad(replies);
30
30
+
} catch (error) {
31
31
+
loading = false;
32
32
+
onError(error);
33
33
+
}
34
34
+
}
35
35
+
</script>
36
36
+
37
37
+
<p class="hidden-replies">
38
38
+
{#if !loading}
39
39
+
☣️ <a href={linkToPostThread(post)} onclick={onLinkClick}>Load hidden replies…</a>
40
40
+
{:else}
41
41
+
<img class="loader" src="icons/sunny.png" alt="Loading...">
42
42
+
{/if}
43
43
+
</p>
+36
src/components/posts/LoadMoreLink.svelte
···
1
1
+
<script>
2
2
+
import { parseThreadPost } from '../../models/posts.js';
3
3
+
import { linkToPostThread } from '../../router.js';
4
4
+
import { showError } from '../../utils.js';
5
5
+
import { getContext } from 'svelte';
6
6
+
7
7
+
let { onLoad } = $props();
8
8
+
let { post } = getContext('post');
9
9
+
let loading = $state(false);
10
10
+
11
11
+
async function onLinkClick(e) {
12
12
+
e.preventDefault();
13
13
+
loading = true;
14
14
+
15
15
+
try {
16
16
+
let json = await api.loadThreadByAtURI(post.uri);
17
17
+
let root = parseThreadPost(json.thread, post.pageRoot, 0, post.absoluteLevel);
18
18
+
// props.replies = root.replies;
19
19
+
20
20
+
loading = false;
21
21
+
window.subtreeRoot = root;
22
22
+
onLoad(root);
23
23
+
} catch (error) {
24
24
+
loading = false;
25
25
+
showError(error);
26
26
+
}
27
27
+
}
28
28
+
</script>
29
29
+
30
30
+
<p>
31
31
+
{#if !loading}
32
32
+
<a href={linkToPostThread(post)} onclick={onLinkClick}>Load more replies…</a>
33
33
+
{:else}
34
34
+
<img class="loader" src="icons/sunny.png" alt="Loading...">
35
35
+
{/if}
36
36
+
</p>
+70
-115
src/post_component.js
···
4
4
import { Post, BlockedPost, MissingPost, DetachedQuotePost, parseThreadPost } from './models/posts.js';
5
5
import { account } from './models/account.svelte.js';
6
6
import { InlineLinkEmbed } from './models/embeds.js';
7
7
-
import { APIError } from './api/api.js';
7
7
+
import { APIError, HiddenRepliesError } from './api/api.js';
8
8
import { linkToPostById, linkToPostThread } from './router.js';
9
9
import { showBiohazardDialog } from './skythread.js';
10
10
import { PostPresenter } from './utils/post_presenter.js';
···
13
13
import EdgeMargin from './components/posts/EdgeMargin.svelte';
14
14
import EmbedComponent from './components/embeds/EmbedComponent.svelte';
15
15
import FediSourceLink from './components/posts/FediSourceLink.svelte';
16
16
+
import HiddenRepliesLink from './components/posts/HiddenRepliesLink.svelte';
17
17
+
import LoadMoreLink from './components/posts/LoadMoreLink.svelte';
16
18
import MissingPostView from './components/posts/MissingPostView.svelte';
17
19
import PostBody from './components/posts/PostBody.svelte';
18
20
import PostHeader from './components/posts/PostHeader.svelte';
···
154
156
155
157
if (this.context == 'thread') {
156
158
if (this.post.hasMoreReplies) {
157
157
-
let loadMore = this.buildLoadMoreLink();
158
158
-
content.appendChild(loadMore);
159
159
+
this.buildLoadMoreLink(content);
159
160
} else if (this.post.hasHiddenReplies && account.biohazardEnabled !== false) {
160
161
let loadMore = this.buildHiddenRepliesLink();
161
162
content.appendChild(loadMore);
···
307
308
stats.append(quotesLink);*/
308
309
}
309
310
310
310
-
/** @returns {HTMLElement} */
311
311
+
/** @param {HTMLElement} element */
311
312
312
312
-
buildLoadMoreLink() {
313
313
-
let loadMore = $tag('p');
313
313
+
buildLoadMoreLink(element) {
314
314
+
svelte.mount(LoadMoreLink, {
315
315
+
target: element,
316
316
+
context: new Map(Object.entries({
317
317
+
post: {
318
318
+
post: this.post,
319
319
+
context: this.context
320
320
+
}
321
321
+
})),
322
322
+
props: {
323
323
+
onLoad: (newPost) => {
324
324
+
this.post.updateDataFromPost(newPost);
314
325
315
315
-
let link = $tag('a', {
316
316
-
href: linkToPostThread(this.post),
317
317
-
text: "Load more replies…"
318
318
-
});
319
319
-
320
320
-
link.addEventListener('click', (e) => {
321
321
-
e.preventDefault();
322
322
-
loadMore.innerHTML = `<img class="loader" src="icons/sunny.png">`;
323
323
-
this.loadSubtree(this.post, this.rootElement);
326
326
+
let component = new PostComponent(this.post, 'thread');
327
327
+
component.installIntoElement(this.rootElement);
328
328
+
}
329
329
+
}
324
330
});
325
325
-
326
326
-
loadMore.appendChild(link);
327
327
-
return loadMore;
328
331
}
329
332
330
333
/** @returns {HTMLElement} */
331
334
332
335
buildHiddenRepliesLink() {
333
333
-
let loadMore = $tag('p.hidden-replies');
336
336
+
let hiddenRepliesDiv = $tag('div');
337
337
+
338
338
+
svelte.mount(HiddenRepliesLink, {
339
339
+
target: hiddenRepliesDiv,
340
340
+
context: new Map(Object.entries({
341
341
+
post: {
342
342
+
post: this.post,
343
343
+
context: this.context
344
344
+
}
345
345
+
})),
346
346
+
props: {
347
347
+
onLoad: (repliesData) => {
348
348
+
let content = $(this.rootElement.querySelector('.content'));
334
349
335
335
-
let link = $tag('a', {
336
336
-
href: linkToPostThread(this.post),
337
337
-
text: "Load hidden replies…"
338
338
-
});
350
350
+
let replies = repliesData
351
351
+
.filter(v => v)
352
352
+
.map(json => parseThreadPost(json.thread, this.post.pageRoot, 1, this.post.absoluteLevel + 1));
353
353
+
354
354
+
this.post.setReplies(this.post.replies.concat(replies));
355
355
+
hiddenRepliesDiv.remove();
339
356
340
340
-
link.addEventListener('click', (e) => {
341
341
-
e.preventDefault();
357
357
+
for (let reply of replies) {
358
358
+
let component = new PostComponent(reply, 'thread');
359
359
+
let view = component.buildElement();
360
360
+
content.append(view);
361
361
+
}
342
362
343
343
-
if (account.biohazardEnabled === true) {
344
344
-
this.loadHiddenReplies(loadMore);
345
345
-
} else {
346
346
-
showBiohazardDialog(() => this.loadHiddenReplies(loadMore));
363
363
+
if (replies.length < repliesData.length) {
364
364
+
let notFoundCount = repliesData.length - replies.length;
365
365
+
let pluralizedCount = (notFoundCount > 1) ? `${notFoundCount} replies are` : '1 reply is';
366
366
+
367
367
+
let info = $tag('p.missing-replies-info', {
368
368
+
html: `<i class="fa-solid fa-ban"></i> ${pluralizedCount} missing (likely taken down by moderation)`
369
369
+
});
370
370
+
content.append(info);
371
371
+
}
372
372
+
},
373
373
+
374
374
+
onError: (error) => {
375
375
+
hiddenRepliesDiv.remove();
376
376
+
377
377
+
if (error instanceof HiddenRepliesError) {
378
378
+
let content = $(this.rootElement.querySelector('.content'));
379
379
+
let info = $tag('p.missing-replies-info', {
380
380
+
html: `<i class="fa-solid fa-ban"></i> Hidden replies not available (post too old)`
381
381
+
});
382
382
+
content.append(info);
383
383
+
} else {
384
384
+
setTimeout(() => showError(error), 1);
385
385
+
}
386
386
+
}
347
387
}
348
388
});
349
389
350
350
-
loadMore.append("☣️ ", link);
351
351
-
return loadMore;
352
352
-
}
353
353
-
354
354
-
/** @param {HTMLElement} loadMoreButton */
355
355
-
356
356
-
loadHiddenReplies(loadMoreButton) {
357
357
-
loadMoreButton.innerHTML = `<img class="loader" src="icons/sunny.png">`;
358
358
-
this.loadHiddenSubtree(this.post, this.rootElement);
390
390
+
return hiddenRepliesDiv;
359
391
}
360
392
361
393
/** @param {string} url, @param {HTMLElement} div */
···
490
522
491
523
// TODO
492
524
Array.from(div.querySelectorAll('a.link-card')).forEach(x => x.remove());
493
493
-
}
494
494
-
}
495
495
-
496
496
-
/** @param {Post} post, @param {HTMLElement} nodeToUpdate, @returns {Promise<void>} */
497
497
-
498
498
-
async loadSubtree(post, nodeToUpdate) {
499
499
-
try {
500
500
-
let json = await api.loadThreadByAtURI(post.uri);
501
501
-
502
502
-
let root = parseThreadPost(json.thread, post.pageRoot, 0, post.absoluteLevel);
503
503
-
post.updateDataFromPost(root);
504
504
-
window.subtreeRoot = post;
505
505
-
506
506
-
let component = new PostComponent(post, 'thread');
507
507
-
component.installIntoElement(nodeToUpdate);
508
508
-
} catch (error) {
509
509
-
showError(error);
510
510
-
}
511
511
-
}
512
512
-
513
513
-
/** @param {Post} post, @param {HTMLElement} nodeToUpdate, @returns {Promise<void>} */
514
514
-
515
515
-
async loadHiddenSubtree(post, nodeToUpdate) {
516
516
-
let content = $(nodeToUpdate.querySelector('.content'));
517
517
-
let hiddenRepliesDiv = $(content.querySelector(':scope > .hidden-replies'));
518
518
-
519
519
-
try {
520
520
-
var expectedReplyURIs = await blueAPI.getReplies(post.uri);
521
521
-
} catch (error) {
522
522
-
hiddenRepliesDiv.remove();
523
523
-
524
524
-
if (error instanceof APIError && error.code == 404) {
525
525
-
let info = $tag('p.missing-replies-info', {
526
526
-
html: `<i class="fa-solid fa-ban"></i> Hidden replies not available (post too old)`
527
527
-
});
528
528
-
content.append(info);
529
529
-
} else {
530
530
-
setTimeout(() => showError(error), 1);
531
531
-
}
532
532
-
533
533
-
return;
534
534
-
}
535
535
-
536
536
-
let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.some(x => x.uri === r));
537
537
-
let promises = missingReplyURIs.map(uri => api.loadThreadByAtURI(uri));
538
538
-
539
539
-
try {
540
540
-
// TODO
541
541
-
var responses = await Promise.allSettled(promises);
542
542
-
} catch (error) {
543
543
-
hiddenRepliesDiv.remove();
544
544
-
setTimeout(() => showError(error), 1);
545
545
-
return;
546
546
-
}
547
547
-
548
548
-
let replies = responses
549
549
-
.map(r => r.status == 'fulfilled' ? r.value : undefined)
550
550
-
.filter(v => v)
551
551
-
.map(json => parseThreadPost(json.thread, post.pageRoot, 1, post.absoluteLevel + 1));
552
552
-
553
553
-
post.setReplies(replies);
554
554
-
hiddenRepliesDiv.remove();
555
555
-
556
556
-
for (let reply of post.replies) {
557
557
-
let component = new PostComponent(reply, 'thread');
558
558
-
let view = component.buildElement();
559
559
-
content.append(view);
560
560
-
}
561
561
-
562
562
-
if (replies.length < responses.length) {
563
563
-
let notFoundCount = responses.length - replies.length;
564
564
-
let pluralizedCount = (notFoundCount > 1) ? `${notFoundCount} replies are` : '1 reply is';
565
565
-
566
566
-
let info = $tag('p.missing-replies-info', {
567
567
-
html: `<i class="fa-solid fa-ban"></i> ${pluralizedCount} missing (likely taken down by moderation)`
568
568
-
});
569
569
-
content.append(info);
570
525
}
571
526
}
572
527
}