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