Split composer into smaller components (#5941)

* Extract ComposerTopBar

* Rename state variables to align with props

* Extract ComposerEmbeds

* Extract ComposerPills

* Extract ComposerFooter

* Tweak condition to be simpler

* Extract ComposerPost
This commit is contained in:
dan
2024-10-25 21:36:54 +01:00
committed by GitHub
parent a729efc3b6
commit d520dd95b9
3 changed files with 523 additions and 359 deletions
+373 -209
View File
@@ -42,6 +42,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedGetPostThread, AppBskyFeedGetPostThread,
BskyAgent, BskyAgent,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
@@ -112,8 +113,11 @@ import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import { import {
ComposerAction,
ComposerDraft,
composerReducer, composerReducer,
createComposerState, createComposerState,
EmbedDraft,
MAX_IMAGES, MAX_IMAGES,
} from './state/composer' } from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
@@ -142,10 +146,7 @@ export const ComposePost = ({
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const currentDid = currentAccount!.did const currentDid = currentAccount!.did
const {data: currentProfile} = useProfileQuery({did: currentDid})
const {closeComposer} = useComposerControls() const {closeComposer} = useComposerControls()
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const {_} = useLingui() const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled() const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
@@ -154,11 +155,10 @@ export const ComposePost = ({
const discardPromptControl = Prompt.usePromptControl() const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
const {closeAllModals} = useModalControls() const {closeAllModals} = useModalControls()
const t = useTheme()
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [processingState, setProcessingState] = useState('') const [publishingStage, setPublishingStage] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [draft, dispatch] = useReducer( const [draft, dispatch] = useReducer(
@@ -222,22 +222,6 @@ export const ComposePost = ({
dispatch({type: 'embed_remove_video'}) dispatch({type: 'embed_remove_video'})
}, [videoState.abortController, dispatch]) }, [videoState.abortController, dispatch])
const updateVideoDimensions = useCallback(
(width: number, height: number) => {
dispatch({
type: 'embed_update_video',
videoAction: {
type: 'update_dimensions',
width,
height,
signal: videoState.abortController.signal,
},
})
},
[videoState.abortController],
)
const hasVideo = Boolean(videoState.asset || videoState.video)
const [publishOnUpload, setPublishOnUpload] = useState(false) const [publishOnUpload, setPublishOnUpload] = useState(false)
const onClose = useCallback(() => { const onClose = useCallback(() => {
@@ -299,32 +283,6 @@ export const ComposePost = ({
} }
}, [onPressCancel, closeAllDialogs, closeAllModals]) }, [onPressCancel, closeAllDialogs, closeAllModals])
const onNewLink = useCallback((uri: string) => {
dispatch({type: 'embed_add_uri', uri})
}, [])
const onImageAdd = useCallback(
(next: ComposerImage[]) => {
dispatch({
type: 'embed_add_images',
images: next,
})
},
[dispatch],
)
const onPhotoPasted = useCallback(
async (uri: string) => {
if (uri.startsWith('data:video/')) {
selectVideo({uri, type: 'video', height: 0, width: 0})
} else {
const res = await pasteImage(uri)
onImageAdd([res])
}
},
[selectVideo, onImageAdd],
)
const isAltTextRequiredAndMissing = useMemo(() => { const isAltTextRequiredAndMissing = useMemo(() => {
if (!requireAltTextEnabled) return false if (!requireAltTextEnabled) return false
@@ -336,8 +294,8 @@ export const ComposePost = ({
}, [images, extGifAlt, extGif, requireAltTextEnabled]) }, [images, extGifAlt, extGif, requireAltTextEnabled])
const onPressPublish = React.useCallback( const onPressPublish = React.useCallback(
async (finishedUploading?: boolean) => { async (finishedUploading: boolean) => {
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { if (isPublishing || graphemeLength > MAX_GRAPHEME_LENGTH) {
return return
} }
@@ -368,7 +326,7 @@ export const ComposePost = ({
return return
} }
setIsProcessing(true) setIsPublishing(true)
let postUri let postUri
try { try {
@@ -376,7 +334,7 @@ export const ComposePost = ({
await apilib.post(agent, queryClient, { await apilib.post(agent, queryClient, {
draft: draft, draft: draft,
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
onStateChange: setProcessingState, onStateChange: setPublishingStage,
langs: toPostLanguages(langPrefs.postLanguage), langs: toPostLanguages(langPrefs.postLanguage),
}) })
).uri ).uri
@@ -406,7 +364,7 @@ export const ComposePost = ({
err = _(msg`This post's author has disabled quote posts.`) err = _(msg`This post's author has disabled quote posts.`)
} }
setError(err) setError(err)
setIsProcessing(false) setIsPublishing(false)
return return
} finally { } finally {
if (postUri) { if (postUri) {
@@ -456,7 +414,7 @@ export const ComposePost = ({
images, images,
graphemeLength, graphemeLength,
isAltTextRequiredAndMissing, isAltTextRequiredAndMissing,
isProcessing, isPublishing,
langPrefs.postLanguage, langPrefs.postLanguage,
onClose, onClose,
onPost, onPost,
@@ -484,28 +442,11 @@ export const ComposePost = ({
() => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing, () => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing,
[graphemeLength, isAltTextRequiredAndMissing], [graphemeLength, isAltTextRequiredAndMissing],
) )
const selectTextInputPlaceholder = replyTo
? _(msg`Write your reply`)
: _(msg`What's up?`)
const canSelectImages =
images.length < MAX_IMAGES &&
videoState.status === 'idle' &&
!videoState.video
const hasMedia = images.length > 0 || Boolean(videoState.video)
const onEmojiButtonPress = useCallback(() => { const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition()) openEmojiPicker?.(textInput.current?.getCursorPosition())
}, [openEmojiPicker]) }, [openEmojiPicker])
const onSelectGif = useCallback((gif: Gif) => {
dispatch({type: 'embed_add_gif', gif})
}, [])
const handleChangeGifAltText = useCallback((altText: string) => {
dispatch({type: 'embed_update_gif', alt: altText})
}, [])
const { const {
scrollHandler, scrollHandler,
onScrollViewContentSizeChange, onScrollViewContentSizeChange,
@@ -527,86 +468,24 @@ export const ComposePost = ({
style={[a.flex_1, viewStyles]} style={[a.flex_1, viewStyles]}
aria-modal aria-modal
accessibilityViewIsModal> accessibilityViewIsModal>
<Animated.View <ComposerTopBar
style={topBarAnimatedStyle} canPost={canPost}
layout={native(LinearTransition)}> isReply={!!replyTo}
<View style={styles.topbarInner}> isPublishQueued={videoState.status !== 'idle' && publishOnUpload}
<Button isPublishing={isPublishing}
label={_(msg`Cancel`)} publishingStage={publishingStage}
variant="ghost" topBarAnimatedStyle={topBarAnimatedStyle}
color="primary" onCancel={onPressCancel}
shape="default" onPublish={() => onPressPublish(false)}>
size="small" {isAltTextRequiredAndMissing && <AltTextReminder />}
style={[
a.rounded_full,
a.py_sm,
{paddingLeft: 7, paddingRight: 7},
]}
onPress={onPressCancel}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}>
<ButtonText style={[a.text_md]}>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
<View style={a.flex_1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : canPost ? (
<Button
testID="composerPublishBtn"
label={replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)}
variant="solid"
color="primary"
shape="default"
size="small"
style={[a.rounded_full, a.py_sm]}
onPress={() => onPressPublish()}
disabled={videoState.status !== 'idle' && publishOnUpload}>
<ButtonText style={[a.text_md]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</ButtonText>
</Button>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
<ErrorBanner <ErrorBanner
error={error} error={error}
videoState={videoState} videoState={videoState}
clearError={() => setError('')} clearError={() => setError('')}
clearVideo={clearVideo} clearVideo={clearVideo}
/> />
</Animated.View> </ComposerTopBar>
<Animated.ScrollView <Animated.ScrollView
layout={native(LinearTransition)} layout={native(LinearTransition)}
onScroll={scrollHandler} onScroll={scrollHandler}
@@ -615,7 +494,112 @@ export const ComposePost = ({
onContentSizeChange={onScrollViewContentSizeChange} onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}> onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<ComposerPost
draft={draft}
dispatch={dispatch}
textInput={textInput}
isReply={!!replyTo}
canRemoveQuote={!initQuote}
onSelectVideo={selectVideo}
onClearVideo={clearVideo}
onPublish={() => onPressPublish(false)}
onError={setError}
/>
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
<ComposerPills
isReply={!!replyTo}
draft={draft}
dispatch={dispatch}
bottomBarAnimatedStyle={bottomBarAnimatedStyle}
/>
<ComposerFooter
draft={draft}
graphemeLength={graphemeLength}
dispatch={dispatch}
onError={setError}
onEmojiButtonPress={onEmojiButtonPress}
onSelectVideo={selectVideo}
/>
</View>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={onClose}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
/>
</KeyboardAvoidingView>
</BottomSheetPortalProvider>
)
}
function ComposerPost({
draft,
dispatch,
textInput,
isReply,
canRemoveQuote,
onClearVideo,
onSelectVideo,
onError,
onPublish,
}: {
draft: ComposerDraft
dispatch: (action: ComposerAction) => void
textInput: React.Ref<TextInputRef>
isReply: boolean
canRemoveQuote: boolean
onClearVideo: () => void
onSelectVideo: (asset: ImagePickerAsset) => void
onError: (error: string) => void
onPublish: (richtext: RichText) => void
}) {
const {currentAccount} = useSession()
const currentDid = currentAccount!.did
const {_} = useLingui()
const {data: currentProfile} = useProfileQuery({did: currentDid})
const richtext = draft.richtext
const selectTextInputPlaceholder = isReply
? _(msg`Write your reply`)
: _(msg`What's up?`)
const onImageAdd = useCallback(
(next: ComposerImage[]) => {
dispatch({
type: 'embed_add_images',
images: next,
})
},
[dispatch],
)
const onNewLink = useCallback(
(uri: string) => {
dispatch({type: 'embed_add_uri', uri})
},
[dispatch],
)
const onPhotoPasted = useCallback(
async (uri: string) => {
if (uri.startsWith('data:video/')) {
onSelectVideo({uri, type: 'video', height: 0, width: 0})
} else {
const res = await pasteImage(uri)
onImageAdd([res])
}
},
[onSelectVideo, onImageAdd],
)
return (
<>
<View <View
style={[ style={[
styles.textInputLayout, styles.textInputLayout,
@@ -635,9 +619,9 @@ export const ComposePost = ({
dispatch({type: 'update_richtext', richtext: rt}) dispatch({type: 'update_richtext', richtext: rt})
}} }}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}
onPressPublish={() => onPressPublish()}
onNewLink={onNewLink} onNewLink={onNewLink}
onError={setError} onError={onError}
onPressPublish={onPublish}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Write post`)} accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_( accessibilityHint={_(
@@ -646,77 +630,209 @@ export const ComposePost = ({
/> />
</View> </View>
<Gallery images={images} dispatch={dispatch} /> <ComposerEmbeds
canRemoveQuote={canRemoveQuote}
embed={draft.embed}
dispatch={dispatch}
clearVideo={onClearVideo}
/>
</>
)
}
{extGif && ( function ComposerTopBar({
<View style={a.relative} key={extGif.url}> canPost,
isReply,
isPublishQueued,
isPublishing,
publishingStage,
onCancel,
onPublish,
topBarAnimatedStyle,
children,
}: {
isPublishing: boolean
publishingStage: string
canPost: boolean
isReply: boolean
isPublishQueued: boolean
onCancel: () => void
onPublish: () => void
topBarAnimatedStyle: StyleProp<ViewStyle>
children?: React.ReactNode
}) {
const pal = usePalette('default')
return (
<Animated.View
style={topBarAnimatedStyle}
layout={native(LinearTransition)}>
<View style={styles.topbarInner}>
<Button
label="Cancel"
variant="ghost"
color="primary"
shape="default"
size="small"
style={[a.rounded_full, a.py_sm, {paddingLeft: 7, paddingRight: 7}]}
onPress={onCancel}
accessibilityHint="Closes post composer and discards post draft">
<ButtonText style={[a.text_md]}>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
<View style={a.flex_1} />
{isPublishing ? (
<>
<Text>{publishingStage}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : canPost ? (
<Button
testID="composerPublishBtn"
label={isReply ? 'Publish reply' : 'Publish post'}
variant="solid"
color="primary"
shape="default"
size="small"
style={[a.rounded_full, a.py_sm]}
onPress={onPublish}
disabled={isPublishQueued}>
<ButtonText style={[a.text_md]}>
{isReply ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</ButtonText>
</Button>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</View>
{children}
</Animated.View>
)
}
function AltTextReminder() {
const pal = usePalette('default')
return (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)
}
function ComposerEmbeds({
embed,
dispatch,
clearVideo,
canRemoveQuote,
}: {
embed: EmbedDraft
dispatch: (action: ComposerAction) => void
clearVideo: () => void
canRemoveQuote: boolean
}) {
const video = embed.media?.type === 'video' ? embed.media.video : null
return (
<>
{embed.media?.type === 'images' && (
<Gallery images={embed.media.images} dispatch={dispatch} />
)}
{embed.media?.type === 'gif' && (
<View style={a.relative} key={embed.media.gif.url}>
<ExternalEmbedGif <ExternalEmbedGif
gif={extGif} gif={embed.media.gif}
onRemove={() => { onRemove={() => dispatch({type: 'embed_remove_gif'})}
dispatch({type: 'embed_remove_gif'})
}}
/> />
<GifAltTextDialog <GifAltTextDialog
gif={extGif} gif={embed.media.gif}
altText={extGifAlt ?? ''} altText={embed.media.alt ?? ''}
onSubmit={handleChangeGifAltText} onSubmit={(altText: string) => {
dispatch({type: 'embed_update_gif', alt: altText})
}}
/> />
</View> </View>
)} )}
{!draft.embed.media && extLink && ( {!embed.media && embed.link && (
<View style={a.relative} key={extLink}> <View style={a.relative} key={embed.link.uri}>
<ExternalEmbedLink <ExternalEmbedLink
uri={extLink} uri={embed.link.uri}
hasQuote={!!quote} hasQuote={!!embed.quote}
onRemove={() => { onRemove={() => dispatch({type: 'embed_remove_link'})}
dispatch({type: 'embed_remove_link'})
}}
/> />
</View> </View>
)} )}
<LayoutAnimationConfig skipExiting> <LayoutAnimationConfig skipExiting>
{hasVideo && ( {video && (
<Animated.View <Animated.View
style={[a.w_full, a.mt_lg]} style={[a.w_full, a.mt_lg]}
entering={native(ZoomIn)} entering={native(ZoomIn)}
exiting={native(ZoomOut)}> exiting={native(ZoomOut)}>
{videoState.asset && {video.asset &&
(videoState.status === 'compressing' ? ( (video.status === 'compressing' ? (
<VideoTranscodeProgress <VideoTranscodeProgress
asset={videoState.asset} asset={video.asset}
progress={videoState.progress} progress={video.progress}
clear={clearVideo} clear={clearVideo}
/> />
) : videoState.video ? ( ) : video.video ? (
<VideoPreview <VideoPreview
asset={videoState.asset} asset={video.asset}
video={videoState.video} video={video.video}
setDimensions={updateVideoDimensions} setDimensions={(width: number, height: number) => {
dispatch({
type: 'embed_update_video',
videoAction: {
type: 'update_dimensions',
width,
height,
signal: video.abortController.signal,
},
})
}}
clear={clearVideo} clear={clearVideo}
/> />
) : null)} ) : null)}
<SubtitleDialogBtn <SubtitleDialogBtn
defaultAltText={videoState.altText} defaultAltText={video.altText}
saveAltText={altText => saveAltText={altText =>
dispatch({ dispatch({
type: 'embed_update_video', type: 'embed_update_video',
videoAction: { videoAction: {
type: 'update_alt_text', type: 'update_alt_text',
altText, altText,
signal: videoState.abortController.signal, signal: video.abortController.signal,
}, },
}) })
} }
captions={videoState.captions} captions={video.captions}
setCaptions={updater => { setCaptions={updater => {
dispatch({ dispatch({
type: 'embed_update_video', type: 'embed_update_video',
videoAction: { videoAction: {
type: 'update_captions', type: 'update_captions',
updater, updater,
signal: videoState.abortController.signal, signal: video.abortController.signal,
}, },
}) })
}} }}
@@ -724,25 +840,39 @@ export const ComposePost = ({
</Animated.View> </Animated.View>
)} )}
</LayoutAnimationConfig> </LayoutAnimationConfig>
<View style={!hasVideo ? [a.mt_md] : []}>
{quote ? ( <View style={!video ? [a.mt_md] : []}>
{embed.quote?.uri ? (
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}> <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}> <View style={{pointerEvents: 'none'}}>
<LazyQuoteEmbed uri={quote} /> <LazyQuoteEmbed uri={embed.quote.uri} />
</View> </View>
{!initQuote && ( {canRemoveQuote && (
<QuoteX <QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} />
onRemove={() => {
dispatch({type: 'embed_remove_quote'})
}}
/>
)} )}
</View> </View>
) : null} ) : null}
</View> </View>
</Animated.ScrollView> </>
<SuggestedLanguage text={richtext.text} /> )
}
function ComposerPills({
isReply,
draft,
dispatch,
bottomBarAnimatedStyle,
}: {
isReply: boolean
draft: ComposerDraft
dispatch: (action: ComposerAction) => void
bottomBarAnimatedStyle: StyleProp<ViewStyle>
}) {
const t = useTheme()
const media = draft.embed.media
const hasMedia = media?.type === 'images' || media?.type === 'video'
const hasLink = !!draft.embed.link
return (
<Animated.View <Animated.View
style={[a.flex_row, a.p_sm, t.atoms.bg, bottomBarAnimatedStyle]}> style={[a.flex_row, a.p_sm, t.atoms.bg, bottomBarAnimatedStyle]}>
<ScrollView <ScrollView
@@ -750,7 +880,7 @@ export const ComposePost = ({
horizontal={true} horizontal={true}
bounces={false} bounces={false}
showsHorizontalScrollIndicator={false}> showsHorizontalScrollIndicator={false}>
{replyTo ? null : ( {isReply ? null : (
<ThreadgateBtn <ThreadgateBtn
postgate={draft.postgate} postgate={draft.postgate}
onChangePostgate={nextPostgate => { onChangePostgate={nextPostgate => {
@@ -771,10 +901,55 @@ export const ComposePost = ({
onChange={nextLabels => { onChange={nextLabels => {
dispatch({type: 'update_labels', labels: nextLabels}) dispatch({type: 'update_labels', labels: nextLabels})
}} }}
hasMedia={hasMedia || Boolean(extLink)} hasMedia={hasMedia || hasLink}
/> />
</ScrollView> </ScrollView>
</Animated.View> </Animated.View>
)
}
function ComposerFooter({
draft,
dispatch,
graphemeLength,
onEmojiButtonPress,
onError,
onSelectVideo,
}: {
draft: ComposerDraft
dispatch: (action: ComposerAction) => void
graphemeLength: number
onEmojiButtonPress: () => void
onError: (error: string) => void
onSelectVideo: (asset: ImagePickerAsset) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const media = draft.embed.media
const images = media?.type === 'images' ? media.images : []
const video = media?.type === 'video' ? media.video : null
const isMaxImages = images.length >= MAX_IMAGES
const onImageAdd = useCallback(
(next: ComposerImage[]) => {
dispatch({
type: 'embed_add_images',
images: next,
})
},
[dispatch],
)
const onSelectGif = useCallback(
(gif: Gif) => {
dispatch({type: 'embed_add_gif', gif})
},
[dispatch],
)
return (
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@@ -787,25 +962,25 @@ export const ComposePost = ({
a.justify_between, a.justify_between,
]}> ]}>
<View style={[a.flex_row, a.align_center]}> <View style={[a.flex_row, a.align_center]}>
{videoState.status !== 'idle' && videoState.status !== 'done' ? ( {video && video.status !== 'done' ? (
<VideoUploadToolbar state={videoState} /> <VideoUploadToolbar state={video} />
) : ( ) : (
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn <SelectPhotoBtn
size={images.length} size={images.length}
disabled={!canSelectImages} disabled={media?.type === 'images' ? isMaxImages : !!media}
onAdd={onImageAdd} onAdd={onImageAdd}
/> />
<SelectVideoBtn <SelectVideoBtn
onSelectVideo={selectVideo} onSelectVideo={onSelectVideo}
disabled={!canSelectImages || images?.length > 0} disabled={!!media}
setError={setError} setError={onError}
/> />
<OpenCameraBtn <OpenCameraBtn
disabled={!canSelectImages} disabled={media?.type === 'images' ? isMaxImages : !!media}
onAdd={onImageAdd} onAdd={onImageAdd}
/> />
<SelectGifBtn onSelectGif={onSelectGif} disabled={hasMedia} /> <SelectGifBtn onSelectGif={onSelectGif} disabled={!!media} />
{!isMobile ? ( {!isMobile ? (
<Button <Button
onPress={onEmojiButtonPress} onPress={onEmojiButtonPress}
@@ -826,17 +1001,6 @@ export const ComposePost = ({
<CharProgress count={graphemeLength} style={{width: 65}} /> <CharProgress count={graphemeLength} style={{width: 65}} />
</View> </View>
</View> </View>
</View>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={onClose}
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
/>
</KeyboardAvoidingView>
</BottomSheetPortalProvider>
) )
} }
@@ -45,7 +45,7 @@ interface TextInputProps extends ComponentProps<typeof RNTextInput> {
placeholder: string placeholder: string
setRichText: (v: RichText) => void setRichText: (v: RichText) => void
onPhotoPasted: (uri: string) => void onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void> onPressPublish: (richtext: RichText) => void
onNewLink: (uri: string) => void onNewLink: (uri: string) => void
onError: (err: string) => void onError: (err: string) => void
} }
@@ -13,15 +13,15 @@ import {Text as TiptapText} from '@tiptap/extension-text'
import {generateJSON} from '@tiptap/html' import {generateJSON} from '@tiptap/html'
import {EditorContent, JSONContent, useEditor} from '@tiptap/react' import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {blobToDataUri, isUriImage} from '#/lib/media/util'
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {blobToDataUri, isUriImage} from 'lib/media/util'
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import { import {
LinkFacetMatch, LinkFacetMatch,
suggestLinkCardUri, suggestLinkCardUri,
} from 'view/com/composer/text-input/text-input-util' } from '#/view/com/composer/text-input/text-input-util'
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import {atoms as a, useAlf} from '#/alf' import {atoms as a, useAlf} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {normalizeTextStyles} from '#/components/Typography' import {normalizeTextStyles} from '#/components/Typography'
@@ -43,7 +43,7 @@ interface TextInputProps {
suggestedLinks: Set<string> suggestedLinks: Set<string>
setRichText: (v: RichText | ((v: RichText) => RichText)) => void setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void onPhotoPasted: (uri: string) => void
onPressPublish: (richtext: RichText) => Promise<void> onPressPublish: (richtext: RichText) => void
onNewLink: (uri: string) => void onNewLink: (uri: string) => void
onError: (err: string) => void onError: (err: string) => void
} }