replies timeline only, appview-less bluesky client

only show scroll to top if post composer isnt focused

ptr.pet ae5f88ad 58e54caf

verified
Changed files
+40 -45
src
+3 -3
src/components/BskyPost.svelte
··· 81 81 const p = await client.getProfile(did); 82 82 if (!p.ok) return; 83 83 profile = p.value; 84 - console.log(profile.description); 84 + // console.log(profile.description); 85 85 }); 86 86 // const replies = replyBacklinks 87 87 // ? Promise.resolve(ok(replyBacklinks)) ··· 97 97 98 98 const scrollToAndPulse = (targetUri: ResourceUri) => { 99 99 const targetId = `timeline-post-${targetUri}-0`; 100 - console.log(`Scrolling to ${targetId}`); 100 + // console.log(`Scrolling to ${targetId}`); 101 101 const element = document.getElementById(targetId); 102 102 if (!element) return; 103 103 ··· 171 171 // reply: backlinks[2], 172 172 // quote: backlinks[3] 173 173 }; 174 - console.log('findAllBacklinks', did, aturi, actions); 174 + // console.log('findAllBacklinks', did, aturi, actions); 175 175 postActions.set(`${did}:${aturi}`, actions); 176 176 }; 177 177 onMount(() => {
+30 -34
src/components/PostComposer.svelte
··· 8 8 import { parseCanonicalResourceUri } from '@atcute/lexicons'; 9 9 import type { ComAtprotoRepoStrongRef } from '@atcute/atproto'; 10 10 11 + export type State = 12 + | { type: 'null' } 13 + | { type: 'focused'; quoting?: PostWithUri; replying?: PostWithUri }; 14 + 11 15 interface Props { 12 16 client: AtpClient; 13 17 onPostSent: (post: PostWithUri) => void; 14 - quoting?: PostWithUri; 15 - replying?: PostWithUri; 18 + _state: State; 16 19 } 17 20 18 - let { 19 - client, 20 - onPostSent, 21 - quoting = $bindable(undefined), 22 - replying = $bindable(undefined) 23 - }: Props = $props(); 21 + let { client, onPostSent, _state = $bindable({ type: 'null' }) }: Props = $props(); 24 22 25 - let color = $derived( 23 + const isFocused = $derived(_state.type === 'focused'); 24 + 25 + const color = $derived( 26 26 client.user?.did ? generateColorForDid(client.user?.did) : 'var(--nucleus-accent2)' 27 27 ); 28 28 ··· 35 35 const record: AppBskyFeedPost.Main = { 36 36 $type: 'app.bsky.feed.post', 37 37 text, 38 - reply: replying 39 - ? { 40 - root: replying.record.reply?.root ?? strongRef(replying), 41 - parent: strongRef(replying) 42 - } 43 - : undefined, 44 - embed: quoting 45 - ? { 46 - $type: 'app.bsky.embed.record', 47 - record: strongRef(quoting) 48 - } 49 - : undefined, 38 + reply: 39 + _state.type === 'focused' && _state.replying 40 + ? { 41 + root: _state.replying.record.reply?.root ?? strongRef(_state.replying), 42 + parent: strongRef(_state.replying) 43 + } 44 + : undefined, 45 + embed: 46 + _state.type === 'focused' && _state.quoting 47 + ? { 48 + $type: 'app.bsky.embed.record', 49 + record: strongRef(_state.quoting) 50 + } 51 + : undefined, 50 52 createdAt: new Date().toISOString() 51 53 }; 52 54 ··· 75 77 76 78 let postText = $state(''); 77 79 let info = $state(''); 78 - let isFocused = $state(false); 79 80 let textareaEl: HTMLTextAreaElement | undefined = $state(); 80 81 81 82 const unfocus = () => { 82 - isFocused = false; 83 - quoting = undefined; 84 - replying = undefined; 83 + _state.type = 'null'; 85 84 }; 86 85 87 86 const doPost = () => { ··· 104 103 $effect(() => { 105 104 document.documentElement.style.setProperty('--acc-color', color); 106 105 if (isFocused && textareaEl) textareaEl.focus(); 107 - if (quoting || replying) isFocused = true; 108 106 }); 109 107 </script> 110 108 ··· 119 117 /> 120 118 {/snippet} 121 119 122 - {#snippet composer()} 120 + {#snippet composer(replying?: PostWithUri, quoting?: PostWithUri)} 123 121 <div class="flex items-center gap-2"> 124 122 <div class="grow"></div> 125 123 <span ··· 149 147 <textarea 150 148 bind:this={textareaEl} 151 149 bind:value={postText} 152 - onfocus={() => (isFocused = true)} 150 + onfocus={() => (_state.type = 'focused')} 153 151 onblur={unfocus} 154 152 onkeydown={(event) => { 155 153 if (event.key === 'Escape') unfocus(); ··· 174 172 <!-- svelte-ignore a11y_no_static_element_interactions --> 175 173 <div 176 174 onmousedown={(e) => { 177 - if (isFocused) { 178 - e.preventDefault(); 179 - } 175 + if (isFocused) e.preventDefault(); 180 176 }} 181 177 class="flex max-w-full rounded-sm border-2 shadow-lg transition-all duration-300 182 178 {!isFocused ? 'min-h-13 items-center' : ''} ··· 196 192 </div> 197 193 {:else} 198 194 <div class="flex flex-col gap-2"> 199 - {#if isFocused} 200 - {@render composer()} 195 + {#if _state.type === 'focused'} 196 + {@render composer(_state.replying, _state.quoting)} 201 197 {:else} 202 198 <input 203 199 bind:value={postText} 204 - onfocus={() => (isFocused = true)} 200 + onfocus={() => (_state = { type: 'focused' })} 205 201 type="text" 206 202 placeholder="what's on your mind?" 207 203 class="flex-1"
+7 -8
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import BskyPost from '$components/BskyPost.svelte'; 3 - import PostComposer from '$components/PostComposer.svelte'; 3 + import PostComposer, { type State as PostComposerState } from '$components/PostComposer.svelte'; 4 4 import AccountSelector from '$components/AccountSelector.svelte'; 5 5 import SettingsPopup from '$components/SettingsPopup.svelte'; 6 6 import { AtpClient, type NotificationsStreamEvent } from '$lib/at/client'; ··· 23 23 24 24 const { data: loadData }: PageProps = $props(); 25 25 26 + // svelte-ignore state_referenced_locally 26 27 let errors = $state(loadData.client.ok ? [] : [loadData.client.error]); 27 28 let errorsOpen = $state(false); 28 29 ··· 72 73 73 74 const threads = $derived(filterThreads(buildThreads(posts), $accounts, { viewOwnPosts })); 74 75 75 - let quoting = $state<PostWithUri | undefined>(undefined); 76 - let replying = $state<PostWithUri | undefined>(undefined); 76 + let postComposerState = $state<PostComposerState>({ type: 'null' }); 77 77 78 78 const expandedThreads = new SvelteSet<ResourceUri>(); 79 79 ··· 330 330 <PostComposer 331 331 client={selectedClient} 332 332 onPostSent={(post) => posts.get(selectedDid!)?.set(post.uri, post)} 333 - bind:quoting 334 - bind:replying 333 + bind:_state={postComposerState} 335 334 /> 336 335 </div> 337 336 {:else} ··· 342 341 </div> 343 342 {/if} 344 343 345 - {#if showScrollToTop} 344 + {#if postComposerState.type === 'null' && showScrollToTop} 346 345 {@render appButton(scrollToTop, 'heroicons:arrow-up-16-solid', 'scroll to top')} 347 346 {/if} 348 347 </div> ··· 415 414 <div class="mb-1.5"> 416 415 <BskyPost 417 416 client={selectedClient ?? viewClient} 418 - onQuote={(post) => (quoting = post)} 419 - onReply={(post) => (replying = post)} 417 + onQuote={(post) => (postComposerState = { type: 'focused', quoting: post })} 418 + onReply={(post) => (postComposerState = { type: 'focused', replying: post })} 420 419 {...post} 421 420 /> 422 421 </div>