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
This commit is contained in:
dan
2024-10-05 03:39:33 +09:00
committed by GitHub
parent 6382fe4535
commit 282db85c06
2 changed files with 151 additions and 17 deletions
+15 -2
View File
@@ -190,7 +190,7 @@ export const ComposePost = ({
// TODO: Move more state here. // TODO: Move more state here.
const [composerState, dispatch] = useReducer( const [composerState, dispatch] = useReducer(
composerReducer, composerReducer,
{initImageUris}, {initImageUris, initQuoteUri: initQuote?.uri},
createComposerState, createComposerState,
) )
@@ -337,6 +337,7 @@ export const ComposePost = ({
const onNewLink = useCallback( const onNewLink = useCallback(
(uri: string) => { (uri: string) => {
dispatch({type: 'embed_add_uri', uri})
if (extLink != null) return if (extLink != null) return
setExtLink({uri, isLoading: true}) setExtLink({uri, isLoading: true})
}, },
@@ -582,6 +583,7 @@ export const ComposePost = ({
const onSelectGif = useCallback( const onSelectGif = useCallback(
(gif: Gif) => { (gif: Gif) => {
dispatch({type: 'embed_add_gif', gif})
setExtLink({ setExtLink({
uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`, uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
isLoading: true, isLoading: true,
@@ -600,6 +602,7 @@ export const ComposePost = ({
const handleChangeGifAltText = useCallback( const handleChangeGifAltText = useCallback(
(altText: string) => { (altText: string) => {
dispatch({type: 'embed_update_gif', alt: altText})
setExtLink(ext => setExtLink(ext =>
ext && ext.meta ext && ext.meta
? { ? {
@@ -770,6 +773,11 @@ export const ComposePost = ({
link={extLink} link={extLink}
gif={extGif} gif={extGif}
onRemove={() => { onRemove={() => {
if (extGif) {
dispatch({type: 'embed_remove_gif'})
} else {
dispatch({type: 'embed_remove_link'})
}
setExtLink(undefined) setExtLink(undefined)
setExtGif(undefined) setExtGif(undefined)
}} }}
@@ -818,7 +826,12 @@ export const ComposePost = ({
<QuoteEmbed quote={quote} /> <QuoteEmbed quote={quote} />
</View> </View>
{quote.uri !== initQuote?.uri && ( {quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} /> <QuoteX
onRemove={() => {
dispatch({type: 'embed_remove_quote'})
setQuote(undefined)
}}
/>
)} )}
</View> </View>
) : null} ) : null}
+136 -15
View File
@@ -1,17 +1,14 @@
import {ImagePickerAsset} from 'expo-image-picker' import {ImagePickerAsset} from 'expo-image-picker'
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
import {ComposerImage, createInitialImages} from '#/state/gallery' import {ComposerImage, createInitialImages} from '#/state/gallery'
import {Gif} from '#/state/queries/tenor'
import {ComposerOpts} from '#/state/shell/composer' import {ComposerOpts} from '#/state/shell/composer'
import {createVideoState, VideoAction, videoReducer, VideoState} from './video' import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
type PostRecord = {
uri: string
}
type ImagesMedia = { type ImagesMedia = {
type: 'images' type: 'images'
images: ComposerImage[] images: ComposerImage[]
labels: string[]
} }
type VideoMedia = { type VideoMedia = {
@@ -19,16 +16,30 @@ type VideoMedia = {
video: VideoState video: VideoState
} }
type ComposerEmbed = { type GifMedia = {
// TODO: Other record types. type: 'gif'
record: PostRecord | undefined gif: Gif
// TODO: Other media types. alt: string
media: ImagesMedia | VideoMedia | undefined }
type Link = {
type: 'link'
uri: string
}
// This structure doesn't exactly correspond to the data model.
// Instead, it maps to how the UI is organized, and how we present a post.
type EmbedDraft = {
// We'll always submit quote and actual media (images, video, gifs) chosen by the user.
quote: Link | undefined
media: ImagesMedia | VideoMedia | GifMedia | undefined
// This field may end up ignored if we have more important things to display than a link card:
link: Link | undefined
} }
export type ComposerState = { export type ComposerState = {
// TODO: Other draft data. // TODO: Other draft data.
embed: ComposerEmbed embed: EmbedDraft
} }
export type ComposerAction = export type ComposerAction =
@@ -42,6 +53,12 @@ export type ComposerAction =
} }
| {type: 'embed_remove_video'} | {type: 'embed_remove_video'}
| {type: 'embed_update_video'; videoAction: VideoAction} | {type: 'embed_update_video'; videoAction: VideoAction}
| {type: 'embed_add_uri'; uri: string}
| {type: 'embed_remove_quote'}
| {type: 'embed_remove_link'}
| {type: 'embed_add_gif'; gif: Gif}
| {type: 'embed_update_gif'; alt: string}
| {type: 'embed_remove_gif'}
const MAX_IMAGES = 4 const MAX_IMAGES = 4
@@ -60,7 +77,6 @@ export function composerReducer(
nextMedia = { nextMedia = {
type: 'images', type: 'images',
images: action.images.slice(0, MAX_IMAGES), images: action.images.slice(0, MAX_IMAGES),
labels: [],
} }
} else if (prevMedia.type === 'images') { } else if (prevMedia.type === 'images') {
nextMedia = { nextMedia = {
@@ -171,6 +187,102 @@ export function composerReducer(
}, },
} }
} }
case 'embed_add_uri': {
const prevQuote = state.embed.quote
const prevLink = state.embed.link
let nextQuote = prevQuote
let nextLink = prevLink
if (isBskyPostUrl(action.uri)) {
if (!prevQuote) {
nextQuote = {
type: 'link',
uri: action.uri,
}
}
} else {
if (!prevLink) {
nextLink = {
type: 'link',
uri: action.uri,
}
}
}
return {
...state,
embed: {
...state.embed,
quote: nextQuote,
link: nextLink,
},
}
}
case 'embed_remove_link': {
return {
...state,
embed: {
...state.embed,
link: undefined,
},
}
}
case 'embed_remove_quote': {
return {
...state,
embed: {
...state.embed,
quote: undefined,
},
}
}
case 'embed_add_gif': {
const prevMedia = state.embed.media
let nextMedia = prevMedia
if (!prevMedia) {
nextMedia = {
type: 'gif',
gif: action.gif,
alt: '',
}
}
return {
...state,
embed: {
...state.embed,
media: nextMedia,
},
}
}
case 'embed_update_gif': {
const prevMedia = state.embed.media
let nextMedia = prevMedia
if (prevMedia?.type === 'gif') {
nextMedia = {
...prevMedia,
alt: action.alt,
}
}
return {
...state,
embed: {
...state.embed,
media: nextMedia,
},
}
}
case 'embed_remove_gif': {
const prevMedia = state.embed.media
let nextMedia = prevMedia
if (prevMedia?.type === 'gif') {
nextMedia = undefined
}
return {
...state,
embed: {
...state.embed,
media: nextMedia,
},
}
}
default: default:
return state return state
} }
@@ -178,22 +290,31 @@ export function composerReducer(
export function createComposerState({ export function createComposerState({
initImageUris, initImageUris,
initQuoteUri,
}: { }: {
initImageUris: ComposerOpts['imageUris'] initImageUris: ComposerOpts['imageUris']
initQuoteUri: string | undefined
}): ComposerState { }): ComposerState {
let media: ImagesMedia | undefined let media: ImagesMedia | undefined
if (initImageUris?.length) { if (initImageUris?.length) {
media = { media = {
type: 'images', type: 'images',
images: createInitialImages(initImageUris), images: createInitialImages(initImageUris),
labels: [],
} }
} }
// TODO: initial video. let quote: Link | undefined
if (initQuoteUri) {
quote = {
type: 'link',
uri: initQuoteUri,
}
}
// TODO: Other initial content.
return { return {
embed: { embed: {
record: undefined, quote,
media, media,
link: undefined,
}, },
} }
} }