diff options
Diffstat (limited to 'src/view/com/composer/Composer.tsx')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 570 |
1 files changed, 295 insertions, 275 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 227964907..e03c64a42 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -114,11 +114,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {createPortalGroup} from '#/components/Portal' import * as Prompt from '#/components/Prompt' import {Text as NewText} from '#/components/Typography' import {composerReducer, createComposerState} from './state/composer' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' +const Portal = createPortalGroup() + const MAX_IMAGES = 4 type CancelRef = { @@ -613,296 +616,313 @@ export const ComposePost = ({ const keyboardVerticalOffset = useKeyboardVerticalOffset() return ( - <KeyboardAvoidingView - testID="composePostView" - behavior={isIOS ? 'padding' : 'height'} - keyboardVerticalOffset={keyboardVerticalOffset} - style={a.flex_1}> - <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal> - <Animated.View - style={topBarAnimatedStyle} - layout={native(LinearTransition)}> - <View style={styles.topbarInner}> - <Button - label={_(msg`Cancel`)} - variant="ghost" - color="primary" - shape="default" - size="small" - 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> - </> - ) : ( - <View style={[styles.postBtnWrapper]}> - <LabelsBtn - labels={labels} - onChange={setLabels} - hasMedia={hasMedia} - /> - {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> + <Portal.Provider> + <KeyboardAvoidingView + testID="composePostView" + behavior={isIOS ? 'padding' : 'height'} + keyboardVerticalOffset={keyboardVerticalOffset} + style={a.flex_1}> + <View + style={[a.flex_1, viewStyles]} + aria-modal + accessibilityViewIsModal> + <Animated.View + style={topBarAnimatedStyle} + layout={native(LinearTransition)}> + <View style={styles.topbarInner}> + <Button + label={_(msg`Cancel`)} + variant="ghost" + color="primary" + shape="default" + size="small" + 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> - )} - </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 style={[styles.postBtnWrapper]}> + <LabelsBtn + labels={labels} + onChange={setLabels} + hasMedia={hasMedia} + /> + {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> + )} </View> - )} - <ErrorBanner - error={error} - videoState={videoState} - clearError={() => setError('')} - clearVideo={clearVideo} - /> - </Animated.View> - <Animated.ScrollView - layout={native(LinearTransition)} - onScroll={scrollHandler} - style={styles.scrollView} - keyboardShouldPersistTaps="always" - onContentSizeChange={onScrollViewContentSizeChange} - onLayout={onScrollViewLayout}> - {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} - <View - style={[ - styles.textInputLayout, - isNative && styles.textInputLayoutMobile, - ]}> - <UserAvatar - avatar={currentProfile?.avatar} - size={50} - type={currentProfile?.associated?.labeler ? 'labeler' : 'user'} - /> - <TextInput - ref={textInput} - richtext={richtext} - placeholder={selectTextInputPlaceholder} - autoFocus - setRichText={setRichText} - onPhotoPasted={onPhotoPasted} - onPressPublish={() => onPressPublish()} - onNewLink={onNewLink} - onError={setError} - accessible={true} - accessibilityLabel={_(msg`Write post`)} - accessibilityHint={_( - msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`, - )} + {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 + error={error} + videoState={videoState} + clearError={() => setError('')} + clearVideo={clearVideo} /> - </View> - - <Gallery images={images} dispatch={dispatch} /> - {images.length === 0 && extLink && ( - <View style={a.relative}> - <ExternalEmbed - link={extLink} - gif={extGif} - onRemove={() => { - if (extGif) { - dispatch({type: 'embed_remove_gif'}) - } else { - dispatch({type: 'embed_remove_link'}) - } - setExtLink(undefined) - setExtGif(undefined) - }} + </Animated.View> + <Animated.ScrollView + layout={native(LinearTransition)} + onScroll={scrollHandler} + style={styles.scrollView} + keyboardShouldPersistTaps="always" + onContentSizeChange={onScrollViewContentSizeChange} + onLayout={onScrollViewLayout}> + {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} + + <View + style={[ + styles.textInputLayout, + isNative && styles.textInputLayoutMobile, + ]}> + <UserAvatar + avatar={currentProfile?.avatar} + size={50} + type={currentProfile?.associated?.labeler ? 'labeler' : 'user'} /> - <GifAltText - link={extLink} - gif={extGif} - onSubmit={handleChangeGifAltText} + <TextInput + ref={textInput} + richtext={richtext} + placeholder={selectTextInputPlaceholder} + autoFocus + setRichText={setRichText} + onPhotoPasted={onPhotoPasted} + onPressPublish={() => onPressPublish()} + onNewLink={onNewLink} + onError={setError} + accessible={true} + accessibilityLabel={_(msg`Write post`)} + accessibilityHint={_( + msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`, + )} /> </View> - )} - <LayoutAnimationConfig skipExiting> - {hasVideo && ( - <Animated.View - style={[a.w_full, a.mt_lg]} - entering={native(ZoomIn)} - exiting={native(ZoomOut)}> - {videoState.asset && - (videoState.status === 'compressing' ? ( - <VideoTranscodeProgress - asset={videoState.asset} - progress={videoState.progress} - clear={clearVideo} - /> - ) : videoState.video ? ( - <VideoPreview - asset={videoState.asset} - video={videoState.video} - setDimensions={updateVideoDimensions} - clear={clearVideo} - /> - ) : null)} - <SubtitleDialogBtn - defaultAltText={videoState.altText} - saveAltText={altText => - dispatch({ - type: 'embed_update_video', - videoAction: { - type: 'update_alt_text', - altText, - signal: videoState.abortController.signal, - }, - }) - } - captions={videoState.captions} - setCaptions={updater => { - dispatch({ - type: 'embed_update_video', - videoAction: { - type: 'update_captions', - updater, - signal: videoState.abortController.signal, - }, - }) + + <Gallery + images={images} + dispatch={dispatch} + Portal={Portal.Portal} + /> + {images.length === 0 && extLink && ( + <View style={a.relative}> + <ExternalEmbed + link={extLink} + gif={extGif} + onRemove={() => { + if (extGif) { + dispatch({type: 'embed_remove_gif'}) + } else { + dispatch({type: 'embed_remove_link'}) + } + setExtLink(undefined) + setExtGif(undefined) }} /> - </Animated.View> + <GifAltText + link={extLink} + gif={extGif} + onSubmit={handleChangeGifAltText} + Portal={Portal.Portal} + /> + </View> )} - </LayoutAnimationConfig> - <View style={!hasVideo ? [a.mt_md] : []}> - {quote ? ( - <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> - <View style={{pointerEvents: 'none'}}> - <QuoteEmbed quote={quote} /> - </View> - {quote.uri !== initQuote?.uri && ( - <QuoteX - onRemove={() => { - dispatch({type: 'embed_remove_quote'}) - setQuote(undefined) + <LayoutAnimationConfig skipExiting> + {hasVideo && ( + <Animated.View + style={[a.w_full, a.mt_lg]} + entering={native(ZoomIn)} + exiting={native(ZoomOut)}> + {videoState.asset && + (videoState.status === 'compressing' ? ( + <VideoTranscodeProgress + asset={videoState.asset} + progress={videoState.progress} + clear={clearVideo} + /> + ) : videoState.video ? ( + <VideoPreview + asset={videoState.asset} + video={videoState.video} + setDimensions={updateVideoDimensions} + clear={clearVideo} + /> + ) : null)} + <SubtitleDialogBtn + defaultAltText={videoState.altText} + saveAltText={altText => + dispatch({ + type: 'embed_update_video', + videoAction: { + type: 'update_alt_text', + altText, + signal: videoState.abortController.signal, + }, + }) + } + captions={videoState.captions} + setCaptions={updater => { + dispatch({ + type: 'embed_update_video', + videoAction: { + type: 'update_captions', + updater, + signal: videoState.abortController.signal, + }, + }) }} + Portal={Portal.Portal} /> - )} - </View> - ) : null} - </View> - </Animated.ScrollView> - <SuggestedLanguage text={richtext.text} /> - - {replyTo ? null : ( - <ThreadgateBtn - postgate={postgate} - onChangePostgate={setPostgate} - threadgateAllowUISettings={threadgateAllowUISettings} - onChangeThreadgateAllowUISettings={ - onChangeThreadgateAllowUISettings - } - style={bottomBarAnimatedStyle} - /> - )} - <View - style={[ - t.atoms.bg, - t.atoms.border_contrast_medium, - styles.bottomBar, - ]}> - {videoState.status !== 'idle' && videoState.status !== 'done' ? ( - <VideoUploadToolbar state={videoState} /> - ) : ( - <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> - <SelectPhotoBtn - size={images.length} - disabled={!canSelectImages} - onAdd={onImageAdd} - /> - <SelectVideoBtn - onSelectVideo={selectVideo} - disabled={!canSelectImages || images?.length > 0} - setError={setError} - /> - <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> - <SelectGifBtn - onClose={focusTextInput} - onSelectGif={onSelectGif} - disabled={hasMedia} - /> - {!isMobile ? ( - <Button - onPress={onEmojiButtonPress} - style={a.p_sm} - label={_(msg`Open emoji picker`)} - accessibilityHint={_(msg`Open emoji picker`)} - variant="ghost" - shape="round" - color="primary"> - <EmojiSmile size="lg" /> - </Button> + </Animated.View> + )} + </LayoutAnimationConfig> + <View style={!hasVideo ? [a.mt_md] : []}> + {quote ? ( + <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> + <View style={{pointerEvents: 'none'}}> + <QuoteEmbed quote={quote} /> + </View> + {quote.uri !== initQuote?.uri && ( + <QuoteX + onRemove={() => { + dispatch({type: 'embed_remove_quote'}) + setQuote(undefined) + }} + /> + )} + </View> ) : null} - </ToolbarWrapper> + </View> + </Animated.ScrollView> + <SuggestedLanguage text={richtext.text} /> + + {replyTo ? null : ( + <ThreadgateBtn + postgate={postgate} + onChangePostgate={setPostgate} + threadgateAllowUISettings={threadgateAllowUISettings} + onChangeThreadgateAllowUISettings={ + onChangeThreadgateAllowUISettings + } + style={bottomBarAnimatedStyle} + Portal={Portal.Portal} + /> )} - <View style={a.flex_1} /> - <SelectLangBtn /> - <CharProgress count={graphemeLength} /> + <View + style={[ + t.atoms.bg, + t.atoms.border_contrast_medium, + styles.bottomBar, + ]}> + {videoState.status !== 'idle' && videoState.status !== 'done' ? ( + <VideoUploadToolbar state={videoState} /> + ) : ( + <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> + <SelectPhotoBtn + size={images.length} + disabled={!canSelectImages} + onAdd={onImageAdd} + /> + <SelectVideoBtn + onSelectVideo={selectVideo} + disabled={!canSelectImages || images?.length > 0} + setError={setError} + /> + <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> + <SelectGifBtn + onClose={focusTextInput} + onSelectGif={onSelectGif} + disabled={hasMedia} + Portal={Portal.Portal} + /> + {!isMobile ? ( + <Button + onPress={onEmojiButtonPress} + style={a.p_sm} + label={_(msg`Open emoji picker`)} + accessibilityHint={_(msg`Open emoji picker`)} + variant="ghost" + shape="round" + color="primary"> + <EmojiSmile size="lg" /> + </Button> + ) : null} + </ToolbarWrapper> + )} + <View style={a.flex_1} /> + <SelectLangBtn /> + <CharProgress count={graphemeLength} /> + </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> + <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" + Portal={Portal.Portal} + /> + </KeyboardAvoidingView> + <Portal.Outlet /> + </Portal.Provider> ) } |