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.
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}
+136 -15
View File
@@ -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,
},
}
}