From a09b98a1c38cf163e39b4d399287750fcf2b7da8 Mon Sep 17 00:00:00 2001 From: Kuba Suder Date: Sun, 24 May 2026 21:54:33 +0200 Subject: [PATCH] show more info about missing hidden replies --- src/api.ts | 4 + src/api/bluesky_api.ts | 33 +++++++-- src/components/posts/HiddenRepliesLink.svelte | 29 +++++++- src/components/posts/PostComponent.svelte | 73 ++++++++++++++----- 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/src/api.ts b/src/api.ts index 92c2e95..7da3089 100644 --- a/src/api.ts +++ b/src/api.ts @@ -18,6 +18,7 @@ declare global { appView: BlueskyAPI; blueAPI: Minisky; constellationAPI: ConstellationAPI; + slingshotAPI: Minisky; accountAPI: AuthenticatedAPI; } } @@ -27,6 +28,8 @@ let appViewHost = 'api.bsky.app'; export let appView = new BlueskyAPI(appViewHost); export let blueAPI = new BluefeedsAPI('blue.mackuba.eu'); export let constellationAPI = new ConstellationAPI('constellation.microcosm.blue'); +export let slingshotAPI = new Minisky("slingshot.microcosm.blue"); + export let accountAPI = new AuthenticatedAPI({ proxiedAppView: appViewHost }); export let api: BlueskyAPI; @@ -45,3 +48,4 @@ window.appView = appView; window.blueAPI = blueAPI; window.accountAPI = accountAPI; window.constellationAPI = constellationAPI; +window.slingshotAPI = slingshotAPI; diff --git a/src/api/bluesky_api.ts b/src/api/bluesky_api.ts index 1a5d2dc..3ee9516 100644 --- a/src/api/bluesky_api.ts +++ b/src/api/bluesky_api.ts @@ -1,5 +1,5 @@ import { HandleCache } from './handle_cache.js'; -import { appView, constellationAPI } from '../api.js'; +import { appView, constellationAPI, slingshotAPI } from '../api.js'; import { APIError, Minisky, type FetchAllOnPageLoad, type MiniskyConfig, type MiniskyOptions, type MiniskyRequestOptions } from './minisky.js'; import { atURI, feedPostTime } from '../utils.js'; import { Post } from '../models/posts.js'; @@ -126,9 +126,9 @@ export class BlueskyAPI extends Minisky { return await this.getRequest('app.bsky.feed.searchPosts', params); } - async loadHiddenReplies(post: Post): Promise<(json | null)[]> { + async loadHiddenReplyURIs(post: Post): Promise { let expectedReplyURIs = await constellationAPI.getReplies(post.uri); - let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.some(x => x.uri === r)); + let missingReplyURIs = expectedReplyURIs.filter(r => !post.replies.find(x => x.uri === r)); missingReplyURIs.sort((a, b) => { let arkey = a.split('/').at(-1)! @@ -136,10 +136,31 @@ export class BlueskyAPI extends Minisky { return arkey.localeCompare(brkey); }); - let promises = missingReplyURIs.map(uri => this.loadThreadByAtURI(uri)); - let responses = await Promise.allSettled(promises); + return missingReplyURIs; + } + + async loadMiniDocWithStatus(did: string): Promise { + let doc = await slingshotAPI.getRequest("blue.microcosm.identity.resolveMiniDoc", { identifier: did }); + + try { + let pds = new Minisky(doc.pds); + + let loadInfo = pds.getRequest('com.atproto.sync.getRepoStatus', { did: did }); + let loadProfile = appView.loadUserProfile(did).catch(() => null); + + let [info, profile] = await Promise.all([loadInfo, loadProfile]); + + doc.active = info.active; + doc.status = info.status; + + if (profile) { + doc.profile = profile; + } + } catch (error) { + doc.pdsError = error; + } - return responses.map(r => (r.status == 'fulfilled') ? r.value : null); + return doc; } async loadUserTimeline( diff --git a/src/components/posts/HiddenRepliesLink.svelte b/src/components/posts/HiddenRepliesLink.svelte index 37b771e..97e034a 100644 --- a/src/components/posts/HiddenRepliesLink.svelte +++ b/src/components/posts/HiddenRepliesLink.svelte @@ -5,9 +5,10 @@ import { parseThreadPost } from '../../models/posts.js'; import { linkToPostThread } from '../../router.js'; import { getPostContext } from './PostComponent.svelte'; + import { atURI } from '../../utils.js'; type Props = { - onLoad: (posts: (AnyPost | null)[]) => void, + onLoad: (posts: AnyPost[], missing: [string, json][]) => void, onError: (error: Error) => void } @@ -27,14 +28,34 @@ } } + function threadsFromPromises(responses: PromiseSettledResult[]): AnyPost[] { + return responses.flatMap(r => { + if (r.status == 'fulfilled') { + let json = r.value; + let subthread = parseThreadPost(json.thread, post.pageRoot, 1, post.absoluteLevel + 1); + return [subthread]; + } else { + return []; + } + }); + } + async function loadHiddenReplies() { loading = true; try { - let repliesData = await api.loadHiddenReplies(post); - let replies = repliesData.map(x => x && parseThreadPost(x.thread, post.pageRoot, 1, post.absoluteLevel + 1)); + let missingReplyURIs = await api.loadHiddenReplyURIs(post); + + let promises = missingReplyURIs.map(uri => api.loadThreadByAtURI(uri)); + let responses = await Promise.allSettled(promises); + let replies = threadsFromPromises(responses); + + let unavailableURIs = missingReplyURIs.filter(x => !replies.find(r => r.uri == x)); + let unavailablePromises = unavailableURIs.map(uri => api.loadMiniDocWithStatus(atURI(uri).repo)); + let unavailableResponses = await Promise.all(unavailablePromises); + loading = false; - onLoad(replies); + onLoad(replies, unavailableResponses.map((v, i) => [unavailableURIs[i], v])); } catch (error) { loading = false; onError(error); diff --git a/src/components/posts/PostComponent.svelte b/src/components/posts/PostComponent.svelte index a00f636..0360f8a 100644 --- a/src/components/posts/PostComponent.svelte +++ b/src/components/posts/PostComponent.svelte @@ -43,7 +43,7 @@ let collapsed = $state(false); let replies: AnyPost[] = $state(post.replies); let repliesLoaded = $state(false); - let missingHiddenReplies: number | undefined = $state(); + let missingHiddenReplies: [string, json][] | undefined = $state(); setPostContext({ post, placement }); @@ -79,20 +79,38 @@ replies = post.replies; } - function onHiddenRepliesLoaded(newReplies: (AnyPost | null)[]) { - let okReplies = newReplies.filter(x => x !== null); - replies.push(...okReplies); + function onHiddenRepliesLoaded(newReplies: AnyPost[], missingData: [string, json][]) { + replies.push(...newReplies); post.replies = replies; - if (okReplies.length === newReplies.length && okReplies.length > 0) { + if (newReplies.length > 0 && missingData.length === 0) { + // there were some hidden replies but we loaded them, everything is ok missingHiddenReplies = undefined; + } else if (newReplies.length === 0 && missingData.length === 0) { + // we didn't get any URIs at all, something is sus + missingHiddenReplies = []; } else { - missingHiddenReplies = newReplies.length - okReplies.length; + // we got some info about the unavailable replies + missingHiddenReplies = missingData; } repliesLoaded = true; } + function missingReplyStatus(data: json) { + if (data.profile) { + return "account is active"; + } else if (data.pdsError) { + return "PDS unavailable"; + } else if (data.active) { + return "account active on its PDS"; + } else if (data.status == "takendown") { + return "account taken down"; + } else { + return `account ${data.status}`; + } + } + function onRepliesLoadingError(error: Error) { showError(error); } @@ -157,17 +175,27 @@ {/if} {#if missingHiddenReplies !== undefined} -

- - {#if missingHiddenReplies > 1} - {missingHiddenReplies} replies are missing - {:else if missingHiddenReplies == 1} - 1 reply is missing - {:else} - Some replies are missing - {/if} - (likely taken down by moderation) -

+ {#if missingHiddenReplies.length > 0} +

+ + {#if missingHiddenReplies.length > 1} + {missingHiddenReplies.length} replies are unavailable: + {:else if missingHiddenReplies.length == 1} + 1 reply is unavailable: + {/if} +

+ + + {:else} +

+ Some replies are missing (might be taken down by moderation) +

+ {/if} {/if} @@ -215,6 +243,17 @@ margin-top: 25px; } + .missing-replies-links { + padding-left: 10px; + list-style-type: none; + font-size: 11pt; + color: #666; + } + + .missing-replies-links li { + margin-block: 5px; + } + .post :global(img.loader) { width: 24px; animation: rotation 3s infinite linear; -- 2.51.2