replies timeline only, appview-less bluesky client

post composer highlighting

ptr.pet 9569baf1 3a4c1f8f

verified
Changed files
+48 -18
src
components
+48 -18
src/components/PostComposer.svelte
··· 7 7 import BskyPost from './BskyPost.svelte'; 8 8 import { parseCanonicalResourceUri } from '@atcute/lexicons'; 9 9 import type { ComAtprotoRepoStrongRef } from '@atcute/atproto'; 10 + import { parseToRichText } from '$lib/richtext'; 11 + import { tokenize } from '$lib/richtext/parser'; 10 12 11 13 export type State = 12 14 | { type: 'null' } ··· 32 34 cid: p.cid!, 33 35 uri: p.uri 34 36 }); 37 + 38 + // Parse rich text (mentions, links, tags) 39 + const rt = await parseToRichText(client, text); 40 + 35 41 const record: AppBskyFeedPost.Main = { 36 42 $type: 'app.bsky.feed.post', 37 - text, 43 + text: rt.text, 44 + facets: rt.facets, 38 45 reply: 39 46 _state.type === 'focused' && _state.replying 40 47 ? { ··· 117 124 /> 118 125 {/snippet} 119 126 127 + {#snippet highlighter(text: string)} 128 + {#each tokenize(text) as token, idx (idx)} 129 + {@const highlighted = 130 + token.type === 'mention' || 131 + token.type === 'topic' || 132 + token.type === 'link' || 133 + token.type === 'autolink'} 134 + <span class={highlighted ? 'text-(--nucleus-accent2)' : ''}>{token.raw}</span> 135 + {/each} 136 + {#if text.endsWith('\n')} 137 + <br /> 138 + {/if} 139 + {/snippet} 140 + 120 141 {#snippet composer(replying?: PostWithUri, quoting?: PostWithUri)} 121 142 <div class="flex items-center gap-2"> 122 143 <div class="grow"></div> ··· 144 165 {@render renderPost(replying)} 145 166 {/if} 146 167 <div class="composer space-y-2"> 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(); 154 - if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) doPost(); 155 - }} 156 - placeholder="what's on your mind?" 157 - rows="4" 158 - class="field-sizing-content resize-none" 159 - ></textarea> 168 + <div class="relative grid"> 169 + <!-- todo: replace this with a proper rich text editor --> 170 + <div 171 + class="pointer-events-none col-start-1 row-start-1 min-h-[5lh] w-full bg-transparent text-wrap break-all whitespace-pre-wrap text-(--nucleus-fg)" 172 + aria-hidden="true" 173 + > 174 + {@render highlighter(postText)} 175 + </div> 176 + 177 + <textarea 178 + bind:this={textareaEl} 179 + bind:value={postText} 180 + onfocus={() => (_state.type = 'focused')} 181 + onblur={unfocus} 182 + onkeydown={(event) => { 183 + if (event.key === 'Escape') unfocus(); 184 + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) doPost(); 185 + }} 186 + placeholder="what's on your mind?" 187 + rows="4" 188 + class="col-start-1 row-start-1 field-sizing-content min-h-[5lh] w-full resize-none overflow-hidden bg-transparent text-wrap break-all whitespace-pre-wrap text-transparent caret-(--nucleus-fg) placeholder:text-(--nucleus-fg)/45" 189 + ></textarea> 190 + </div> 191 + 160 192 {#if quoting} 161 193 {@render renderPost(quoting)} 162 194 {/if} ··· 209 241 </div> 210 242 </div> 211 243 212 - <!-- TODO: this fucking blows --> 213 244 <style> 214 245 @reference "../app.css"; 215 246 ··· 224 255 } 225 256 226 257 textarea { 227 - @apply w-full bg-transparent p-0; 258 + @apply w-full p-0; 228 259 } 229 260 230 261 input { ··· 235 266 @apply focus:scale-100; 236 267 } 237 268 238 - input::placeholder, 239 - textarea::placeholder { 269 + input::placeholder { 240 270 color: color-mix(in srgb, var(--acc-color) 45%, var(--nucleus-bg)); 241 271 } 242 272