Skip to content

Commit

Permalink
Track links and embeds in the composer reducer (#5593)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gaearon authored Oct 4, 2024
1 parent 6382fe4 commit 282db85
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 17 deletions.
17 changes: 15 additions & 2 deletions src/view/com/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const ComposePost = ({
// TODO: Move more state here.
const [composerState, dispatch] = useReducer(
composerReducer,
{initImageUris},
{initImageUris, initQuoteUri: initQuote?.uri},
createComposerState,
)

Expand Down Expand Up @@ -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})
},
Expand Down Expand Up @@ -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,
Expand All @@ -600,6 +602,7 @@ export const ComposePost = ({

const handleChangeGifAltText = useCallback(
(altText: string) => {
dispatch({type: 'embed_update_gif', alt: altText})
setExtLink(ext =>
ext && ext.meta
? {
Expand Down Expand Up @@ -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)
}}
Expand Down Expand Up @@ -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}
Expand Down
151 changes: 136 additions & 15 deletions src/view/com/composer/state/composer.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
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 = {
type: 'video'
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 =
Expand All @@ -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

Expand All @@ -60,7 +77,6 @@ export function composerReducer(
nextMedia = {
type: 'images',
images: action.images.slice(0, MAX_IMAGES),
labels: [],
}
} else if (prevMedia.type === 'images') {
nextMedia = {
Expand Down Expand Up @@ -171,29 +187,134 @@ 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
}
}

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,
},
}
}

0 comments on commit 282db85

Please sign in to comment.