Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Track links and embeds in the composer reducer (#5593)

* Scaffold embed draft types

These don't map 1:1 to the record structure. Rather, we select data from the draft on posting.

* Prefill initial quote

* Implement the reducer

authored by

dan and committed by
GitHub
282db85c 6382fe45

+151 -17
+15 -2
src/view/com/composer/Composer.tsx
··· 190 190 // TODO: Move more state here. 191 191 const [composerState, dispatch] = useReducer( 192 192 composerReducer, 193 - {initImageUris}, 193 + {initImageUris, initQuoteUri: initQuote?.uri}, 194 194 createComposerState, 195 195 ) 196 196 ··· 337 337 338 338 const onNewLink = useCallback( 339 339 (uri: string) => { 340 + dispatch({type: 'embed_add_uri', uri}) 340 341 if (extLink != null) return 341 342 setExtLink({uri, isLoading: true}) 342 343 }, ··· 582 583 583 584 const onSelectGif = useCallback( 584 585 (gif: Gif) => { 586 + dispatch({type: 'embed_add_gif', gif}) 585 587 setExtLink({ 586 588 uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`, 587 589 isLoading: true, ··· 600 602 601 603 const handleChangeGifAltText = useCallback( 602 604 (altText: string) => { 605 + dispatch({type: 'embed_update_gif', alt: altText}) 603 606 setExtLink(ext => 604 607 ext && ext.meta 605 608 ? { ··· 770 773 link={extLink} 771 774 gif={extGif} 772 775 onRemove={() => { 776 + if (extGif) { 777 + dispatch({type: 'embed_remove_gif'}) 778 + } else { 779 + dispatch({type: 'embed_remove_link'}) 780 + } 773 781 setExtLink(undefined) 774 782 setExtGif(undefined) 775 783 }} ··· 818 826 <QuoteEmbed quote={quote} /> 819 827 </View> 820 828 {quote.uri !== initQuote?.uri && ( 821 - <QuoteX onRemove={() => setQuote(undefined)} /> 829 + <QuoteX 830 + onRemove={() => { 831 + dispatch({type: 'embed_remove_quote'}) 832 + setQuote(undefined) 833 + }} 834 + /> 822 835 )} 823 836 </View> 824 837 ) : null}
+136 -15
src/view/com/composer/state/composer.ts
··· 1 1 import {ImagePickerAsset} from 'expo-image-picker' 2 2 3 + import {isBskyPostUrl} from '#/lib/strings/url-helpers' 3 4 import {ComposerImage, createInitialImages} from '#/state/gallery' 5 + import {Gif} from '#/state/queries/tenor' 4 6 import {ComposerOpts} from '#/state/shell/composer' 5 7 import {createVideoState, VideoAction, videoReducer, VideoState} from './video' 6 8 7 - type PostRecord = { 8 - uri: string 9 - } 10 - 11 9 type ImagesMedia = { 12 10 type: 'images' 13 11 images: ComposerImage[] 14 - labels: string[] 15 12 } 16 13 17 14 type VideoMedia = { ··· 19 16 video: VideoState 20 17 } 21 18 22 - type ComposerEmbed = { 23 - // TODO: Other record types. 24 - record: PostRecord | undefined 25 - // TODO: Other media types. 26 - media: ImagesMedia | VideoMedia | undefined 19 + type GifMedia = { 20 + type: 'gif' 21 + gif: Gif 22 + alt: string 23 + } 24 + 25 + type Link = { 26 + type: 'link' 27 + uri: string 28 + } 29 + 30 + // This structure doesn't exactly correspond to the data model. 31 + // Instead, it maps to how the UI is organized, and how we present a post. 32 + type EmbedDraft = { 33 + // We'll always submit quote and actual media (images, video, gifs) chosen by the user. 34 + quote: Link | undefined 35 + media: ImagesMedia | VideoMedia | GifMedia | undefined 36 + // This field may end up ignored if we have more important things to display than a link card: 37 + link: Link | undefined 27 38 } 28 39 29 40 export type ComposerState = { 30 41 // TODO: Other draft data. 31 - embed: ComposerEmbed 42 + embed: EmbedDraft 32 43 } 33 44 34 45 export type ComposerAction = ··· 42 53 } 43 54 | {type: 'embed_remove_video'} 44 55 | {type: 'embed_update_video'; videoAction: VideoAction} 56 + | {type: 'embed_add_uri'; uri: string} 57 + | {type: 'embed_remove_quote'} 58 + | {type: 'embed_remove_link'} 59 + | {type: 'embed_add_gif'; gif: Gif} 60 + | {type: 'embed_update_gif'; alt: string} 61 + | {type: 'embed_remove_gif'} 45 62 46 63 const MAX_IMAGES = 4 47 64 ··· 60 77 nextMedia = { 61 78 type: 'images', 62 79 images: action.images.slice(0, MAX_IMAGES), 63 - labels: [], 64 80 } 65 81 } else if (prevMedia.type === 'images') { 66 82 nextMedia = { ··· 171 187 }, 172 188 } 173 189 } 190 + case 'embed_add_uri': { 191 + const prevQuote = state.embed.quote 192 + const prevLink = state.embed.link 193 + let nextQuote = prevQuote 194 + let nextLink = prevLink 195 + if (isBskyPostUrl(action.uri)) { 196 + if (!prevQuote) { 197 + nextQuote = { 198 + type: 'link', 199 + uri: action.uri, 200 + } 201 + } 202 + } else { 203 + if (!prevLink) { 204 + nextLink = { 205 + type: 'link', 206 + uri: action.uri, 207 + } 208 + } 209 + } 210 + return { 211 + ...state, 212 + embed: { 213 + ...state.embed, 214 + quote: nextQuote, 215 + link: nextLink, 216 + }, 217 + } 218 + } 219 + case 'embed_remove_link': { 220 + return { 221 + ...state, 222 + embed: { 223 + ...state.embed, 224 + link: undefined, 225 + }, 226 + } 227 + } 228 + case 'embed_remove_quote': { 229 + return { 230 + ...state, 231 + embed: { 232 + ...state.embed, 233 + quote: undefined, 234 + }, 235 + } 236 + } 237 + case 'embed_add_gif': { 238 + const prevMedia = state.embed.media 239 + let nextMedia = prevMedia 240 + if (!prevMedia) { 241 + nextMedia = { 242 + type: 'gif', 243 + gif: action.gif, 244 + alt: '', 245 + } 246 + } 247 + return { 248 + ...state, 249 + embed: { 250 + ...state.embed, 251 + media: nextMedia, 252 + }, 253 + } 254 + } 255 + case 'embed_update_gif': { 256 + const prevMedia = state.embed.media 257 + let nextMedia = prevMedia 258 + if (prevMedia?.type === 'gif') { 259 + nextMedia = { 260 + ...prevMedia, 261 + alt: action.alt, 262 + } 263 + } 264 + return { 265 + ...state, 266 + embed: { 267 + ...state.embed, 268 + media: nextMedia, 269 + }, 270 + } 271 + } 272 + case 'embed_remove_gif': { 273 + const prevMedia = state.embed.media 274 + let nextMedia = prevMedia 275 + if (prevMedia?.type === 'gif') { 276 + nextMedia = undefined 277 + } 278 + return { 279 + ...state, 280 + embed: { 281 + ...state.embed, 282 + media: nextMedia, 283 + }, 284 + } 285 + } 174 286 default: 175 287 return state 176 288 } ··· 178 290 179 291 export function createComposerState({ 180 292 initImageUris, 293 + initQuoteUri, 181 294 }: { 182 295 initImageUris: ComposerOpts['imageUris'] 296 + initQuoteUri: string | undefined 183 297 }): ComposerState { 184 298 let media: ImagesMedia | undefined 185 299 if (initImageUris?.length) { 186 300 media = { 187 301 type: 'images', 188 302 images: createInitialImages(initImageUris), 189 - labels: [], 190 303 } 191 304 } 192 - // TODO: initial video. 305 + let quote: Link | undefined 306 + if (initQuoteUri) { 307 + quote = { 308 + type: 'link', 309 + uri: initQuoteUri, 310 + } 311 + } 312 + // TODO: Other initial content. 193 313 return { 194 314 embed: { 195 - record: undefined, 315 + quote, 196 316 media, 317 + link: undefined, 197 318 }, 198 319 } 199 320 }