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:
@@ -190,7 +190,7 @@ export const ComposePost = ({
|
||||
// TODO: Move more state here.
|
||||
const [composerState, dispatch] = useReducer(
|
||||
composerReducer,
|
||||
{initImageUris},
|
||||
{initImageUris, initQuoteUri: initQuote?.uri},
|
||||
createComposerState,
|
||||
)
|
||||
|
||||
@@ -337,6 +337,7 @@ export const ComposePost = ({
|
||||
|
||||
const onNewLink = useCallback(
|
||||
(uri: string) => {
|
||||
dispatch({type: 'embed_add_uri', uri})
|
||||
if (extLink != null) return
|
||||
setExtLink({uri, isLoading: true})
|
||||
},
|
||||
@@ -582,6 +583,7 @@ export const ComposePost = ({
|
||||
|
||||
const onSelectGif = useCallback(
|
||||
(gif: Gif) => {
|
||||
dispatch({type: 'embed_add_gif', gif})
|
||||
setExtLink({
|
||||
uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
|
||||
isLoading: true,
|
||||
@@ -600,6 +602,7 @@ export const ComposePost = ({
|
||||
|
||||
const handleChangeGifAltText = useCallback(
|
||||
(altText: string) => {
|
||||
dispatch({type: 'embed_update_gif', alt: altText})
|
||||
setExtLink(ext =>
|
||||
ext && ext.meta
|
||||
? {
|
||||
@@ -770,6 +773,11 @@ export const ComposePost = ({
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onRemove={() => {
|
||||
if (extGif) {
|
||||
dispatch({type: 'embed_remove_gif'})
|
||||
} else {
|
||||
dispatch({type: 'embed_remove_link'})
|
||||
}
|
||||
setExtLink(undefined)
|
||||
setExtGif(undefined)
|
||||
}}
|
||||
@@ -818,7 +826,12 @@ export const ComposePost = ({
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
<QuoteX
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_quote'})
|
||||
setQuote(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
|
||||
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
|
||||
import {ComposerImage, createInitialImages} from '#/state/gallery'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
|
||||
|
||||
type PostRecord = {
|
||||
uri: string
|
||||
}
|
||||
|
||||
type ImagesMedia = {
|
||||
type: 'images'
|
||||
images: ComposerImage[]
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
type VideoMedia = {
|
||||
@@ -19,16 +16,30 @@ type VideoMedia = {
|
||||
video: VideoState
|
||||
}
|
||||
|
||||
type ComposerEmbed = {
|
||||
// TODO: Other record types.
|
||||
record: PostRecord | undefined
|
||||
// TODO: Other media types.
|
||||
media: ImagesMedia | VideoMedia | undefined
|
||||
type GifMedia = {
|
||||
type: 'gif'
|
||||
gif: Gif
|
||||
alt: string
|
||||
}
|
||||
|
||||
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 = {
|
||||
// TODO: Other draft data.
|
||||
embed: ComposerEmbed
|
||||
embed: EmbedDraft
|
||||
}
|
||||
|
||||
export type ComposerAction =
|
||||
@@ -42,6 +53,12 @@ export type ComposerAction =
|
||||
}
|
||||
| {type: 'embed_remove_video'}
|
||||
| {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
|
||||
|
||||
@@ -60,7 +77,6 @@ export function composerReducer(
|
||||
nextMedia = {
|
||||
type: 'images',
|
||||
images: action.images.slice(0, MAX_IMAGES),
|
||||
labels: [],
|
||||
}
|
||||
} else if (prevMedia.type === 'images') {
|
||||
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:
|
||||
return state
|
||||
}
|
||||
@@ -178,22 +290,31 @@ export function composerReducer(
|
||||
|
||||
export function createComposerState({
|
||||
initImageUris,
|
||||
initQuoteUri,
|
||||
}: {
|
||||
initImageUris: ComposerOpts['imageUris']
|
||||
initQuoteUri: string | undefined
|
||||
}): ComposerState {
|
||||
let media: ImagesMedia | undefined
|
||||
if (initImageUris?.length) {
|
||||
media = {
|
||||
type: 'images',
|
||||
images: createInitialImages(initImageUris),
|
||||
labels: [],
|
||||
}
|
||||
}
|
||||
// TODO: initial video.
|
||||
let quote: Link | undefined
|
||||
if (initQuoteUri) {
|
||||
quote = {
|
||||
type: 'link',
|
||||
uri: initQuoteUri,
|
||||
}
|
||||
}
|
||||
// TODO: Other initial content.
|
||||
return {
|
||||
embed: {
|
||||
record: undefined,
|
||||
quote,
|
||||
media,
|
||||
link: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user