Thread viewer for Bluesky

refactored how different post types are passed to components

+119 -99
+2 -2
src/components/embeds/QuoteComponent.svelte
··· 6 6 import { atURI } from '../../utils.js'; 7 7 8 8 import FeedGeneratorView from '../embeds/FeedGeneratorView.svelte'; 9 - import PostComponent from '../posts/PostComponent.svelte'; 9 + import PostWrapper from '../posts/PostWrapper.svelte'; 10 10 import StarterPackView from '../embeds/StarterPackView.svelte'; 11 11 import UserListView from '../embeds/UserListView.svelte'; 12 12 ··· 56 56 {#snippet quoteContent(record: ATProtoRecord)} 57 57 {#if record instanceof BasePost} 58 58 <div class="quote-embed"> 59 - <PostComponent post={record} context="quote" /> 59 + <PostWrapper post={record} context="quote" /> 60 60 </div> 61 61 62 62 {:else if record instanceof FeedGeneratorRecord}
+14 -8
src/components/posts/BlockedPostView.svelte
··· 1 1 <script lang="ts"> 2 - import { getContext } from 'svelte'; 3 2 import { account } from '../../models/account.svelte.js'; 4 - import { Post } from '../../models/posts.js'; 3 + import { BlockedPost, DetachedQuotePost, MissingPost, Post } from '../../models/posts.js'; 5 4 6 5 import BlockedPostContent from './BlockedPostContent.svelte'; 7 6 import MissingPostView from './MissingPostView.svelte'; 8 7 import PostSubtreeLink from './PostSubtreeLink.svelte'; 9 8 import ReferencedPostAuthorLink from './ReferencedPostAuthorLink.svelte'; 10 9 11 - let { reason }: { reason: string } = $props(); 12 - let { post, context }: { post: Post, context: PostContext } = getContext('post'); 10 + type Props = { 11 + reason: string; 12 + post: BlockedPost | DetachedQuotePost; 13 + context: PostContext; 14 + } 15 + 16 + let { reason, post, context }: Props = $props(); 13 17 14 18 let biohazardEnabled = $derived(account.biohazardEnabled !== false); 15 19 let loading = $state(false); ··· 30 34 } 31 35 32 36 function blockStatus() { 33 - if (post.blockedByUser) { 37 + if (post instanceof DetachedQuotePost) { 38 + return undefined; 39 + } else if (post.blockedByUser) { 34 40 return "has blocked you"; 35 41 } else if (post.blocksUser) { 36 42 return "you've blocked them"; ··· 45 51 <i class="fa-solid fa-ban"></i> <span>{reason}</span> 46 52 47 53 {#if biohazardEnabled} 48 - <ReferencedPostAuthorLink status={blockStatus()} /> 54 + <ReferencedPostAuthorLink {post} status={blockStatus()} /> 49 55 {/if} 50 56 </p> 51 57 ··· 62 68 <p class="blocked-header"> 63 69 <i class="fa-solid fa-ban"></i> <span>{reason}</span> 64 70 65 - <ReferencedPostAuthorLink status={blockStatus()} /> 71 + <ReferencedPostAuthorLink {post} status={blockStatus()} /> 66 72 67 73 {#if !(reloadedPost.author.viewer.blockedBy || reloadedPost.author.viewer.blocking)} 68 74 <span class="separator">&bull;</span> ··· 73 79 74 80 <BlockedPostContent post={reloadedPost} {context} /> 75 81 {:else} 76 - <MissingPostView /> 82 + <MissingPostView post={new MissingPost(post.data)} /> 77 83 {/if}
+3 -4
src/components/posts/MissingPostView.svelte
··· 1 1 <script lang="ts"> 2 - import { getContext } from 'svelte'; 3 - import { Post } from '../../models/posts'; 2 + import { MissingPost } from '../../models/posts'; 4 3 import ReferencedPostAuthorLink from './ReferencedPostAuthorLink.svelte'; 5 4 6 - let { post }: { post: Post } = getContext('post'); 5 + let { post }: { post: MissingPost } = $props(); 7 6 </script> 8 7 9 8 <p class="blocked-header"> 10 9 <i class="fa-solid fa-ban"></i> <span>Deleted post</span> 11 - <ReferencedPostAuthorLink /> 10 + <ReferencedPostAuthorLink {post} /> 12 11 </p>
+1 -1
src/components/posts/PostBody.svelte
··· 7 7 const highlightID = 'search-results'; 8 8 9 9 let { post }: { post: Post } = getContext('post'); 10 - let { highlightedMatches = undefined }: { highlightedMatches?: string[] } = $props(); 10 + let { highlightedMatches = undefined }: { highlightedMatches?: string[] | undefined } = $props(); 11 11 12 12 let bodyElement: HTMLElement | undefined = $state(); 13 13
+56 -62
src/components/posts/PostComponent.svelte
··· 2 2 import { setContext } from 'svelte'; 3 3 import { HiddenRepliesError } from '../../api/bluesky_api.js'; 4 4 import { account } from '../../models/account.svelte.js'; 5 - import { Post, BlockedPost, DetachedQuotePost } from '../../models/posts.js'; 5 + import { Post, BlockedPost } from '../../models/posts.js'; 6 6 import { Embed, InlineLinkEmbed } from '../../models/embeds.js'; 7 7 import { isValidURL, showError } from '../../utils.js'; 8 8 9 - import BlockedPostView from './BlockedPostView.svelte'; 10 9 import EdgeMargin from './EdgeMargin.svelte'; 11 10 import FediSourceLink from './FediSourceLink.svelte'; 12 11 import HiddenRepliesLink from './HiddenRepliesLink.svelte'; 13 12 import LoadMoreLink from './LoadMoreLink.svelte'; 14 - import MissingPostView from './MissingPostView.svelte'; 15 13 import PostBody from './PostBody.svelte'; 16 14 import PostComponent from './PostComponent.svelte'; 17 15 import PostHeader from './PostHeader.svelte'; 18 16 import PostTagsRow from './PostTagsRow.svelte'; 19 17 import PostFooter from './PostFooter.svelte'; 18 + import PostWrapper from './PostWrapper.svelte'; 20 19 21 20 import EmbedComponent from '../embeds/EmbedComponent.svelte'; 22 21 ··· 29 28 - feed - a post on the hashtag feed page 30 29 */ 31 30 32 - let { post, context, highlightedMatches = undefined, ...props } = $props(); 31 + type Props = { 32 + post: Post, 33 + context: PostContext, 34 + highlightedMatches?: string[] | undefined, 35 + class?: string | undefined 36 + } 37 + 38 + let { post, context, highlightedMatches = undefined, ...props }: Props = $props(); 33 39 34 40 let collapsed = $state(false); 35 41 let replies: AnyPost[] = $state(post.replies); ··· 116 122 {/if} 117 123 {/snippet} 118 124 119 - {#if post instanceof Post} 120 - <div class="post post-{context} {props.class || ''}" class:muted={post.muted} class:collapsed={collapsed}> 121 - <PostHeader /> 125 + <div class="post post-{context} {props.class || ''}" class:muted={post.muted} class:collapsed={collapsed}> 126 + <PostHeader /> 122 127 123 - {#if context == 'thread' && !post.isPageRoot} 124 - <EdgeMargin bind:collapsed /> 125 - {/if} 128 + {#if context == 'thread' && !post.isPageRoot} 129 + <EdgeMargin bind:collapsed /> 130 + {/if} 126 131 127 - <div class="content"> 128 - {#if post.muted} 129 - <details> 130 - <summary>{post.muteList ? `Muted (${post.muteList})` : 'Muted - click to show'}</summary> 132 + <div class="content"> 133 + {#if post.muted} 134 + <details> 135 + <summary>{post.muteList ? `Muted (${post.muteList})` : 'Muted - click to show'}</summary> 131 136 132 - {@render body()} 133 - </details> 134 - {:else} 135 137 {@render body()} 136 - {/if} 137 - 138 - {#if post.replyCount == 1 && (replies[0] instanceof Post) && replies[0].author.did == post.author.did} 139 - <PostComponent post={replies[0]} context="thread" class="flat" /> 140 - {:else} 141 - {#each replies as reply (reply.uri)} 142 - {#if shouldRenderReply(reply)} 143 - <PostComponent post={reply} context="thread" /> 144 - {/if} 145 - {/each} 146 - {/if} 138 + </details> 139 + {:else} 140 + {@render body()} 141 + {/if} 147 142 148 - {#if context == 'thread' && !repliesLoaded} 149 - {#if post.hasMoreReplies} 150 - <LoadMoreLink onLoad={onMoreRepliesLoaded} onError={onRepliesLoadingError} /> 151 - {:else if post.hasHiddenReplies && account.biohazardEnabled !== false} 152 - <HiddenRepliesLink onLoad={onHiddenRepliesLoaded} onError={onRepliesLoadingError} /> 143 + {#if post.replyCount == 1 && (replies[0] instanceof Post) && replies[0].author.did == post.author.did} 144 + <PostComponent post={replies[0]} context="thread" class="flat" /> 145 + {:else} 146 + {#each replies as reply (reply.uri)} 147 + {#if shouldRenderReply(reply)} 148 + <PostWrapper post={reply} context="thread" /> 153 149 {/if} 150 + {/each} 151 + {/if} 152 + 153 + {#if context == 'thread' && !repliesLoaded} 154 + {#if post.hasMoreReplies} 155 + <LoadMoreLink onLoad={onMoreRepliesLoaded} onError={onRepliesLoadingError} /> 156 + {:else if post.hasHiddenReplies && account.biohazardEnabled !== false} 157 + <HiddenRepliesLink onLoad={onHiddenRepliesLoaded} onError={onRepliesLoadingError} /> 154 158 {/if} 159 + {/if} 155 160 156 - {#if missingHiddenReplies !== undefined} 157 - <p class="missing-replies-info"> 158 - <i class="fa-solid fa-ban"></i> 159 - {#if missingHiddenReplies > 1} 160 - {missingHiddenReplies} replies are missing 161 - {:else if missingHiddenReplies == 1} 162 - 1 reply is missing 163 - {:else} 164 - Some replies are missing 165 - {/if} 166 - (likely taken down by moderation) 167 - </p> 168 - {/if} 161 + {#if missingHiddenReplies !== undefined} 162 + <p class="missing-replies-info"> 163 + <i class="fa-solid fa-ban"></i> 164 + {#if missingHiddenReplies > 1} 165 + {missingHiddenReplies} replies are missing 166 + {:else if missingHiddenReplies == 1} 167 + 1 reply is missing 168 + {:else} 169 + Some replies are missing 170 + {/if} 171 + (likely taken down by moderation) 172 + </p> 173 + {/if} 169 174 170 - {#if hiddenRepliesError} 171 - <p class="missing-replies-info"> 172 - <i class="fa-solid fa-ban"></i> Hidden replies not available (post too old) 173 - </p> 174 - {/if} 175 - </div> 176 - </div> 177 - {:else} 178 - <div class="post post-{context} blocked"> 179 - {#if post instanceof BlockedPost} 180 - <BlockedPostView reason="Blocked post" /> 181 - {:else if post instanceof DetachedQuotePost} 182 - <BlockedPostView reason="Hidden quote" /> 183 - {:else} 184 - <MissingPostView /> 175 + {#if hiddenRepliesError} 176 + <p class="missing-replies-info"> 177 + <i class="fa-solid fa-ban"></i> Hidden replies not available (post too old) 178 + </p> 185 179 {/if} 186 180 </div> 187 - {/if} 181 + </div>
+32
src/components/posts/PostWrapper.svelte
··· 1 + <script lang="ts"> 2 + import { Post, BlockedPost, DetachedQuotePost } from '../../models/posts.js'; 3 + 4 + import BlockedPostView from './BlockedPostView.svelte'; 5 + import MissingPostView from './MissingPostView.svelte'; 6 + import PostComponent from './PostComponent.svelte'; 7 + 8 + /** 9 + Contexts: 10 + - thread - a post in the thread tree 11 + - parent - parent reference above the thread root 12 + - quote - a quote embed 13 + - quotes - a post on the quotes page 14 + - feed - a post on the hashtag feed page 15 + */ 16 + 17 + let { post, context }: { post: AnyPost, context: PostContext } = $props(); 18 + </script> 19 + 20 + {#if post instanceof Post} 21 + <PostComponent {post} {context} /> 22 + {:else} 23 + <div class="post post-{context} blocked"> 24 + {#if post instanceof BlockedPost} 25 + <BlockedPostView {post} {context} reason="Blocked post" /> 26 + {:else if post instanceof DetachedQuotePost} 27 + <BlockedPostView {post} {context} reason="Hidden quote" /> 28 + {:else} 29 + <MissingPostView {post} /> 30 + {/if} 31 + </div> 32 + {/if}
+5 -16
src/components/posts/ReferencedPostAuthorLink.svelte
··· 1 1 <script lang="ts"> 2 - import { getContext } from 'svelte'; 3 2 import { Post } from '../../models/posts.js'; 4 3 import { atURI } from '../../utils.js'; 5 4 6 - let { status = undefined }: { status?: string | undefined } = $props(); 7 - let { post }: { post: Post } = getContext('post'); 5 + let { post, status = undefined }: { post: AnyPost, status?: string | undefined } = $props(); 8 6 9 7 let handle: string | undefined = $state(); 10 8 11 9 $effect(() => { 12 - loadAuthorHandle(post); 13 - }); 14 - 15 - async function loadAuthorHandle(post: Post) { 16 10 let did = atURI(post.uri).repo; 17 - let loadedHandle = await api.fetchHandleForDid(did); 18 11 19 - if (post.author) { 20 - post.author.handle = loadedHandle; 21 - } else { 22 - post.author = { did: did, handle: loadedHandle }; 23 - } 24 - 25 - handle = loadedHandle; 26 - } 12 + api.fetchHandleForDid(did).then(loadedHandle => { 13 + handle = loadedHandle; 14 + }); 15 + }); 27 16 </script> 28 17 29 18 {#if status}
+1 -4
src/components/posts/ThreadRootParent.svelte
··· 1 1 <script lang="ts"> 2 2 import { Post, BlockedPost, MissingPost } from '../../models/posts.js'; 3 3 import { linkToPostThread } from '../../router.js'; 4 - import { setContext } from 'svelte'; 5 4 import BlockedPostView from './BlockedPostView.svelte'; 6 5 7 6 let { post }: { post: AnyPost } = $props(); 8 - 9 - setContext('post', { post: post, context: 'parent' }); 10 7 </script> 11 8 12 9 {#if post instanceof BlockedPost} 13 10 <div class="back"> 14 - <BlockedPostView reason="Parent post blocked" /> 11 + <BlockedPostView {post} context="parent" reason="Parent post blocked" /> 15 12 </div> 16 13 {:else if post instanceof MissingPost} 17 14 <p class="back">
+5 -2
src/pages/ThreadPage.svelte
··· 3 3 import { showError } from '../utils.js'; 4 4 import MainLoader from '../components/MainLoader.svelte'; 5 5 import PostComponent from '../components/posts/PostComponent.svelte'; 6 + import PostWrapper from '../components/posts/PostWrapper.svelte'; 6 7 import ThreadRootParent from '../components/posts/ThreadRootParent.svelte'; 7 8 import ThreadRootParentRaw from '../components/posts/ThreadRootParentRaw.svelte'; 8 9 ··· 64 65 {:else if post.parentReference} 65 66 <ThreadRootParentRaw uri={post.parentReference.uri} /> 66 67 {/if} 67 - {/if} 68 68 69 - <PostComponent {post} context="thread" bind:this={rootComponent} /> 69 + <PostComponent {post} context="thread" bind:this={rootComponent} /> 70 + {:else} 71 + <PostWrapper {post} context="thread" /> 72 + {/if} 70 73 {:else if !loadingFailed} 71 74 <MainLoader /> 72 75 {/if}