diff --git a/src/api/api.js b/src/api/api.js
index 45dfe8a..138acdc 100644
--- a/src/api/api.js
+++ b/src/api/api.js
@@ -24,6 +24,15 @@ export class URLError extends Error {
}
}
+export class HiddenRepliesError extends Error {
+
+ /** @param {Error} error */
+ constructor(error) {
+ super(error.message);
+ this.originalError = error;
+ }
+}
+
/**
* Caches the mapping of handles to DIDs to avoid unnecessary API calls to resolveHandle or getProfile.
@@ -291,6 +300,28 @@ export class BlueskyAPI extends Minisky {
return { cursor: response.cursor, posts: postGroups.flat() };
}
+ /** @param {Post} post, @returns {Promise<(json | undefined)[]>} */
+
+ async loadHiddenReplies(post) {
+ let expectedReplyURIs;
+
+ try {
+ expectedReplyURIs = await blueAPI.getReplies(post.uri);
+ } catch (error) {
+ if (error instanceof APIError && error.code == 404) {
+ throw new HiddenRepliesError(error);
+ } else {
+ throw error;
+ }
+ }
+
+ let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.some(x => x.uri === r));
+ let promises = missingReplyURIs.map(uri => this.loadThreadByAtURI(uri));
+ let responses = await Promise.allSettled(promises);
+
+ return responses.map(r => (r.status == 'fulfilled') ? r.value : undefined);
+ }
+
/**
* @param {number} days
* @param {{ onPageLoad?: FetchAllOnPageLoad, keepLastPage?: boolean }} [options]
diff --git a/src/components/posts/HiddenRepliesLink.svelte b/src/components/posts/HiddenRepliesLink.svelte
new file mode 100644
index 0000000..cf069ae
--- /dev/null
+++ b/src/components/posts/HiddenRepliesLink.svelte
@@ -0,0 +1,43 @@
+
+
+
+ {#if !loading}
+ ☣️ Load hidden replies…
+ {:else}
+
+ {/if}
+
diff --git a/src/components/posts/LoadMoreLink.svelte b/src/components/posts/LoadMoreLink.svelte
new file mode 100644
index 0000000..c6526de
--- /dev/null
+++ b/src/components/posts/LoadMoreLink.svelte
@@ -0,0 +1,36 @@
+
+
+
+ {#if !loading}
+ Load more replies…
+ {:else}
+
+ {/if}
+
diff --git a/src/post_component.js b/src/post_component.js
index 5503119..3aeebd3 100644
--- a/src/post_component.js
+++ b/src/post_component.js
@@ -4,7 +4,7 @@ import { $tag } from './utils_ts.js';
import { Post, BlockedPost, MissingPost, DetachedQuotePost, parseThreadPost } from './models/posts.js';
import { account } from './models/account.svelte.js';
import { InlineLinkEmbed } from './models/embeds.js';
-import { APIError } from './api/api.js';
+import { APIError, HiddenRepliesError } from './api/api.js';
import { linkToPostById, linkToPostThread } from './router.js';
import { showBiohazardDialog } from './skythread.js';
import { PostPresenter } from './utils/post_presenter.js';
@@ -13,6 +13,8 @@ import BlockedPostView from './components/posts/BlockedPostView.svelte';
import EdgeMargin from './components/posts/EdgeMargin.svelte';
import EmbedComponent from './components/embeds/EmbedComponent.svelte';
import FediSourceLink from './components/posts/FediSourceLink.svelte';
+import HiddenRepliesLink from './components/posts/HiddenRepliesLink.svelte';
+import LoadMoreLink from './components/posts/LoadMoreLink.svelte';
import MissingPostView from './components/posts/MissingPostView.svelte';
import PostBody from './components/posts/PostBody.svelte';
import PostHeader from './components/posts/PostHeader.svelte';
@@ -154,8 +156,7 @@ export class PostComponent {
if (this.context == 'thread') {
if (this.post.hasMoreReplies) {
- let loadMore = this.buildLoadMoreLink();
- content.appendChild(loadMore);
+ this.buildLoadMoreLink(content);
} else if (this.post.hasHiddenReplies && account.biohazardEnabled !== false) {
let loadMore = this.buildHiddenRepliesLink();
content.appendChild(loadMore);
@@ -307,55 +308,86 @@ export class PostComponent {
stats.append(quotesLink);*/
}
- /** @returns {HTMLElement} */
-
- buildLoadMoreLink() {
- let loadMore = $tag('p');
+ /** @param {HTMLElement} element */
- let link = $tag('a', {
- href: linkToPostThread(this.post),
- text: "Load more replies…"
- });
+ buildLoadMoreLink(element) {
+ svelte.mount(LoadMoreLink, {
+ target: element,
+ context: new Map(Object.entries({
+ post: {
+ post: this.post,
+ context: this.context
+ }
+ })),
+ props: {
+ onLoad: (newPost) => {
+ this.post.updateDataFromPost(newPost);
- link.addEventListener('click', (e) => {
- e.preventDefault();
- loadMore.innerHTML = `
`;
- this.loadSubtree(this.post, this.rootElement);
+ let component = new PostComponent(this.post, 'thread');
+ component.installIntoElement(this.rootElement);
+ }
+ }
});
-
- loadMore.appendChild(link);
- return loadMore;
}
/** @returns {HTMLElement} */
buildHiddenRepliesLink() {
- let loadMore = $tag('p.hidden-replies');
+ let hiddenRepliesDiv = $tag('div');
- let link = $tag('a', {
- href: linkToPostThread(this.post),
- text: "Load hidden replies…"
- });
+ svelte.mount(HiddenRepliesLink, {
+ target: hiddenRepliesDiv,
+ context: new Map(Object.entries({
+ post: {
+ post: this.post,
+ context: this.context
+ }
+ })),
+ props: {
+ onLoad: (repliesData) => {
+ let content = $(this.rootElement.querySelector('.content'));
- link.addEventListener('click', (e) => {
- e.preventDefault();
+ let replies = repliesData
+ .filter(v => v)
+ .map(json => parseThreadPost(json.thread, this.post.pageRoot, 1, this.post.absoluteLevel + 1));
- if (account.biohazardEnabled === true) {
- this.loadHiddenReplies(loadMore);
- } else {
- showBiohazardDialog(() => this.loadHiddenReplies(loadMore));
- }
- });
+ this.post.setReplies(this.post.replies.concat(replies));
+ hiddenRepliesDiv.remove();
- loadMore.append("☣️ ", link);
- return loadMore;
- }
+ for (let reply of replies) {
+ let component = new PostComponent(reply, 'thread');
+ let view = component.buildElement();
+ content.append(view);
+ }
+
+ if (replies.length < repliesData.length) {
+ let notFoundCount = repliesData.length - replies.length;
+ let pluralizedCount = (notFoundCount > 1) ? `${notFoundCount} replies are` : '1 reply is';
- /** @param {HTMLElement} loadMoreButton */
+ let info = $tag('p.missing-replies-info', {
+ html: ` ${pluralizedCount} missing (likely taken down by moderation)`
+ });
+ content.append(info);
+ }
+ },
+
+ onError: (error) => {
+ hiddenRepliesDiv.remove();
+
+ if (error instanceof HiddenRepliesError) {
+ let content = $(this.rootElement.querySelector('.content'));
+ let info = $tag('p.missing-replies-info', {
+ html: ` Hidden replies not available (post too old)`
+ });
+ content.append(info);
+ } else {
+ setTimeout(() => showError(error), 1);
+ }
+ }
+ }
+ });
- loadHiddenReplies(loadMoreButton) {
- loadMoreButton.innerHTML = `
`;
- this.loadHiddenSubtree(this.post, this.rootElement);
+ return hiddenRepliesDiv;
}
/** @param {string} url, @param {HTMLElement} div */
@@ -492,81 +524,4 @@ export class PostComponent {
Array.from(div.querySelectorAll('a.link-card')).forEach(x => x.remove());
}
}
-
- /** @param {Post} post, @param {HTMLElement} nodeToUpdate, @returns {Promise} */
-
- async loadSubtree(post, nodeToUpdate) {
- try {
- let json = await api.loadThreadByAtURI(post.uri);
-
- let root = parseThreadPost(json.thread, post.pageRoot, 0, post.absoluteLevel);
- post.updateDataFromPost(root);
- window.subtreeRoot = post;
-
- let component = new PostComponent(post, 'thread');
- component.installIntoElement(nodeToUpdate);
- } catch (error) {
- showError(error);
- }
- }
-
- /** @param {Post} post, @param {HTMLElement} nodeToUpdate, @returns {Promise} */
-
- async loadHiddenSubtree(post, nodeToUpdate) {
- let content = $(nodeToUpdate.querySelector('.content'));
- let hiddenRepliesDiv = $(content.querySelector(':scope > .hidden-replies'));
-
- try {
- var expectedReplyURIs = await blueAPI.getReplies(post.uri);
- } catch (error) {
- hiddenRepliesDiv.remove();
-
- if (error instanceof APIError && error.code == 404) {
- let info = $tag('p.missing-replies-info', {
- html: ` Hidden replies not available (post too old)`
- });
- content.append(info);
- } else {
- setTimeout(() => showError(error), 1);
- }
-
- return;
- }
-
- let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.some(x => x.uri === r));
- let promises = missingReplyURIs.map(uri => api.loadThreadByAtURI(uri));
-
- try {
- // TODO
- var responses = await Promise.allSettled(promises);
- } catch (error) {
- hiddenRepliesDiv.remove();
- setTimeout(() => showError(error), 1);
- return;
- }
-
- let replies = responses
- .map(r => r.status == 'fulfilled' ? r.value : undefined)
- .filter(v => v)
- .map(json => parseThreadPost(json.thread, post.pageRoot, 1, post.absoluteLevel + 1));
-
- post.setReplies(replies);
- hiddenRepliesDiv.remove();
-
- for (let reply of post.replies) {
- let component = new PostComponent(reply, 'thread');
- let view = component.buildElement();
- content.append(view);
- }
-
- if (replies.length < responses.length) {
- let notFoundCount = responses.length - replies.length;
- let pluralizedCount = (notFoundCount > 1) ? `${notFoundCount} replies are` : '1 reply is';
-
- let info = $tag('p.missing-replies-info', {
- html: ` ${pluralizedCount} missing (likely taken down by moderation)`
- });
- content.append(info);
- }
- }
}