diff options
Diffstat (limited to 'src/view')
19 files changed, 414 insertions, 395 deletions
diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx index fb69e1d9c..74b0d2315 100644 --- a/src/view/com/auth/server-input/index.tsx +++ b/src/view/com/auth/server-input/index.tsx @@ -66,12 +66,8 @@ export function ServerInputDialog({ ]) return ( - <Dialog.Outer - control={control} - nativeOptions={{sheet: {snapPoints: ['100%']}}} - onClose={onClose}> + <Dialog.Outer control={control} onClose={onClose}> <Dialog.Handle /> - <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" accessibilityLabelledBy="dialog-title"> 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> ) } diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index a05607c76..3479fb973 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -20,6 +20,7 @@ import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' import {GifEmbed} from '../util/post-embeds/GifEmbed' import {AltTextReminder} from './photos/Gallery' @@ -28,10 +29,12 @@ export function GifAltText({ link: linkProp, gif, onSubmit, + Portal, }: { link: ExternalEmbedDraft gif?: Gif onSubmit: (alt: string) => void + Portal: PortalComponent }) { const control = Dialog.useDialogControl() const {_} = useLingui() @@ -96,9 +99,7 @@ export function GifAltText({ <AltTextReminder /> - <Dialog.Outer - control={control} - nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}> + <Dialog.Outer control={control} Portal={Portal}> <Dialog.Handle /> <AltTextInner onSubmit={onPressSubmit} @@ -185,6 +186,8 @@ function AltTextInner({ </View> </View> <Dialog.Close /> + {/* Maybe fix this later -h */} + {isAndroid ? <View style={{height: 300}} /> : null} </Dialog.ScrollableInner> ) } diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx index 0afb83ed9..ebe528abc 100644 --- a/src/view/com/composer/photos/EditImageDialog.web.tsx +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -20,6 +20,7 @@ import {EditImageDialogProps} from './EditImageDialog' export const EditImageDialog = (props: EditImageDialogProps) => { return ( <Dialog.Outer control={props.control}> + <Dialog.Handle /> <EditImageInner key={props.image.source.id} {...props} /> </Dialog.Outer> ) diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 5ff7042bc..3958a85c0 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {PortalComponent} from '#/components/Portal' import {ComposerAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' @@ -30,6 +31,7 @@ const IMAGE_GAP = 8 interface GalleryProps { images: ComposerImage[] dispatch: (action: ComposerAction) => void + Portal: PortalComponent } export let Gallery = (props: GalleryProps): React.ReactNode => { @@ -57,7 +59,12 @@ interface GalleryInnerProps extends GalleryProps { containerInfo: Dimensions } -const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { +const GalleryInner = ({ + images, + containerInfo, + dispatch, + Portal, +}: GalleryInnerProps) => { const {isMobile} = useWebMediaQueries() const {altTextControlStyle, imageControlsStyle, imageStyle} = @@ -111,6 +118,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { onRemove={() => { dispatch({type: 'embed_remove_image', image}) }} + Portal={Portal} /> ) })} @@ -127,6 +135,7 @@ type GalleryItemProps = { imageStyle?: ViewStyle onChange: (next: ComposerImage) => void onRemove: () => void + Portal: PortalComponent } const GalleryItem = ({ @@ -136,6 +145,7 @@ const GalleryItem = ({ imageStyle, onChange, onRemove, + Portal, }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() @@ -230,6 +240,7 @@ const GalleryItem = ({ control={altTextControl} image={image} onChange={onChange} + Portal={Portal} /> <EditImageDialog diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index 123e1066a..16ce4351a 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -5,25 +5,26 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' -import {isWeb} from '#/platform/detection' +import {isAndroid, isWeb} from '#/platform/detection' import {ComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' +import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' type Props = { control: Dialog.DialogOuterProps['control'] image: ComposerImage onChange: (next: ComposerImage) => void + Portal: PortalComponent } export const ImageAltTextDialog = (props: Props): React.ReactNode => { return ( - <Dialog.Outer control={props.control}> + <Dialog.Outer control={props.control} Portal={props.Portal}> <Dialog.Handle /> - <ImageAltTextInner {...props} /> </Dialog.Outer> ) @@ -116,6 +117,8 @@ const ImageAltTextInner = ({ </ButtonText> </Button> </View> + {/* Maybe fix this later -h */} + {isAndroid ? <View style={{height: 300}} /> : null} </Dialog.ScrollableInner> ) } diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx index d13df0a11..d482e0783 100644 --- a/src/view/com/composer/photos/SelectGifBtn.tsx +++ b/src/view/com/composer/photos/SelectGifBtn.tsx @@ -9,14 +9,16 @@ import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {GifSelectDialog} from '#/components/dialogs/GifSelect' import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' +import {PortalComponent} from '#/components/Portal' type Props = { onClose: () => void onSelectGif: (gif: Gif) => void disabled?: boolean + Portal?: PortalComponent } -export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { +export function SelectGifBtn({onClose, onSelectGif, disabled, Portal}: Props) { const {_} = useLingui() const ref = useRef<{open: () => void}>(null) const t = useTheme() @@ -46,6 +48,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { controlRef={ref} onClose={onClose} onSelectGif={onSelectGif} + Portal={Portal} /> </> ) diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index 34ead3d9a..37bfbafe6 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -9,6 +9,7 @@ import {isNative} from '#/platform/detection' import {ComposerImage, createComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' +import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' type Props = { @@ -21,23 +22,26 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) { const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const t = useTheme() + const sheetWrapper = useSheetWrapper() const onPressSelectPhotos = useCallback(async () => { if (isNative && !(await requestPhotoAccessIfNeeded())) { return } - const images = await openPicker({ - selectionLimit: 4 - size, - allowsMultipleSelection: true, - }) + const images = await sheetWrapper( + openPicker({ + selectionLimit: 4 - size, + allowsMultipleSelection: true, + }), + ) const results = await Promise.all( images.map(img => createComposerImage(img)), ) onAdd(results) - }, [requestPhotoAccessIfNeeded, size, onAdd]) + }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper]) return ( <Button diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index b0806180c..7e57a57d4 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -13,6 +13,7 @@ import * as Dialog from '#/components/Dialog' import {PostInteractionSettingsControlledDialog} from '#/components/dialogs/PostInteractionSettingsDialog' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' +import {PortalComponent} from '#/components/Portal' export function ThreadgateBtn({ postgate, @@ -20,6 +21,7 @@ export function ThreadgateBtn({ threadgateAllowUISettings, onChangeThreadgateAllowUISettings, style, + Portal, }: { postgate: AppBskyFeedPostgate.Record onChangePostgate: (v: AppBskyFeedPostgate.Record) => void @@ -28,6 +30,8 @@ export function ThreadgateBtn({ onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void style?: StyleProp<AnimatedStyle<ViewStyle>> + + Portal: PortalComponent }) { const {_} = useLingui() const t = useTheme() @@ -77,6 +81,7 @@ export function ThreadgateBtn({ onChangePostgate={onChangePostgate} threadgateAllowUISettings={threadgateAllowUISettings} onChangeThreadgateAllowUISettings={onChangeThreadgateAllowUISettings} + Portal={Portal} /> </> ) diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx index f66684d4e..04522ee1d 100644 --- a/src/view/com/composer/videos/SubtitleDialog.tsx +++ b/src/view/com/composer/videos/SubtitleDialog.tsx @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers' import {LANGUAGES} from '#/locale/languages' -import {isAndroid, isWeb} from '#/platform/detection' +import {isWeb} from '#/platform/detection' import {useLanguagePrefs} from '#/state/preferences' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -17,6 +17,7 @@ import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC' import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {PortalComponent} from '#/components/Portal' import {Text} from '#/components/Typography' import {SubtitleFilePicker} from './SubtitleFilePicker' @@ -29,6 +30,7 @@ interface Props { captions: CaptionsTrack[] saveAltText: (altText: string) => void setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void + Portal: PortalComponent } export function SubtitleDialogBtn(props: Props) { @@ -56,9 +58,7 @@ export function SubtitleDialogBtn(props: Props) { {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} </ButtonText> </Button> - <Dialog.Outer - control={control} - nativeOptions={isAndroid ? {sheet: {snapPoints: ['60%']}} : {}}> + <Dialog.Outer control={control} Portal={props.Portal}> <Dialog.Handle /> <SubtitleDialogInner {...props} /> </Dialog.Outer> diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 2b4376b69..43555ccb4 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -20,6 +20,7 @@ import {isAndroid, isNative, isWeb} from '#/platform/detection' import {precacheProfile} from '#/state/queries/profile' import {HighPriorityImage} from '#/view/com/util/images/Image' import {tokens, useTheme} from '#/alf' +import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import { Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, Camera_Stroke2_Corner0_Rounded as Camera, @@ -271,6 +272,7 @@ let EditableUserAvatar = ({ const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const sheetWrapper = useSheetWrapper() const aviStyle = useMemo(() => { if (type === 'algo' || type === 'list') { @@ -306,9 +308,11 @@ let EditableUserAvatar = ({ return } - const items = await openPicker({ - aspect: [1, 1], - }) + const items = await sheetWrapper( + openPicker({ + aspect: [1, 1], + }), + ) const item = items[0] if (!item) { return @@ -332,7 +336,7 @@ let EditableUserAvatar = ({ logger.error('Failed to crop banner', {error: e}) } } - }, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) + }, [onSelectNewAvatar, requestPhotoAccessIfNeeded, sheetWrapper]) const onRemoveAvatar = React.useCallback(() => { onSelectNewAvatar(null) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 13f4081fc..622cb2129 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -17,6 +17,7 @@ import {logger} from '#/logger' import {isAndroid, isNative} from '#/platform/detection' import {EventStopper} from '#/view/com/util/EventStopper' import {tokens, useTheme as useAlfTheme} from '#/alf' +import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import { Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, Camera_Stroke2_Corner0_Rounded as Camera, @@ -43,6 +44,7 @@ export function UserBanner({ const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const sheetWrapper = useSheetWrapper() const onOpenCamera = React.useCallback(async () => { if (!(await requestCameraAccessIfNeeded())) { @@ -60,7 +62,7 @@ export function UserBanner({ if (!(await requestPhotoAccessIfNeeded())) { return } - const items = await openPicker() + const items = await sheetWrapper(openPicker()) if (!items[0]) { return } @@ -80,7 +82,7 @@ export function UserBanner({ logger.error('Failed to crop banner', {error: e}) } } - }, [onSelectNewBanner, requestPhotoAccessIfNeeded]) + }, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper]) const onRemoveBanner = React.useCallback(() => { onSelectNewBanner?.(null) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 33287564a..cd1f2d3de 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -240,8 +240,8 @@ let PostDropdownBtn = ({ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') }, [_, richText]) - const onPressTranslate = React.useCallback(() => { - openLink(translatorUrl) + const onPressTranslate = React.useCallback(async () => { + await openLink(translatorUrl) }, [openLink, translatorUrl]) const onHidePost = React.useCallback(() => { @@ -439,7 +439,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownSendViaDMBtn" label={_(msg`Send via direct message`)} - onPress={sendViaChatControl.open}> + onPress={() => sendViaChatControl.open()}> <Menu.ItemText> <Trans>Send via direct message</Trans> </Menu.ItemText> @@ -467,7 +467,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownEmbedBtn" label={_(msg`Embed post`)} - onPress={embedPostControl.open}> + onPress={() => embedPostControl.open()}> <Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText> <Menu.ItemIcon icon={CodeBrackets} position="right" /> </Menu.Item> @@ -542,7 +542,7 @@ let PostDropdownBtn = ({ ? _(msg`Hide reply for me`) : _(msg`Hide post for me`) } - onPress={hidePromptControl.open}> + onPress={() => hidePromptControl.open()}> <Menu.ItemText> {isReply ? _(msg`Hide reply for me`) @@ -630,7 +630,9 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownEditPostInteractions" label={_(msg`Edit interaction settings`)} - onPress={postInteractionSettingsDialogControl.open} + onPress={() => + postInteractionSettingsDialogControl.open() + } {...(isAuthor ? Platform.select({ web: { @@ -649,7 +651,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownDeleteBtn" label={_(msg`Delete post`)} - onPress={deletePromptControl.open}> + onPress={() => deletePromptControl.open()}> <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> <Menu.ItemIcon icon={Trash} position="right" /> </Menu.Item> diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 0ecdf25b9..9be72ae23 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -86,7 +86,9 @@ let RepostButton = ({ </Text> ) : undefined} </Button> - <Dialog.Outer control={dialogControl}> + <Dialog.Outer + control={dialogControl} + nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <Dialog.Inner label={_(msg`Repost or quote post`)}> <View style={a.gap_xl}> @@ -155,7 +157,6 @@ let RepostButton = ({ </View> <Button label={_(msg`Cancel quote post`)} - onAccessibilityEscape={close} onPress={close} size="large" variant="solid" diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx index a27cff9a3..e4341fcd2 100644 --- a/src/view/screens/Settings/DisableEmail2FADialog.tsx +++ b/src/view/screens/Settings/DisableEmail2FADialog.tsx @@ -79,7 +79,6 @@ export function DisableEmail2FADialog({ return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" accessibilityLabelledBy="dialog-title"> diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx index a6ddb3820..1d8d26471 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -53,7 +53,6 @@ export function ExportCarDialog({ return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" accessibilityLabelledBy="dialog-title"> diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 3a9f67de8..a0a2a2755 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -2,8 +2,8 @@ import React from 'react' import {View} from 'react-native' import {useNavigation} from '@react-navigation/native' +import {NavigationProp} from '#/lib/routes/types' import {useDialogStateControlContext} from '#/state/dialogs' -import {NavigationProp} from 'lib/routes/types' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -179,19 +179,13 @@ export function Dialogs() { </Prompt.Outer> <Dialog.Outer control={basic}> - <Dialog.Handle /> - <Dialog.Inner label="test"> <H3 nativeID="dialog-title">Dialog</H3> <P nativeID="dialog-description">A basic dialog</P> </Dialog.Inner> </Dialog.Outer> - <Dialog.Outer - control={scrollable} - nativeOptions={{sheet: {snapPoints: ['100%']}}}> - <Dialog.Handle /> - + <Dialog.Outer control={scrollable}> <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" accessibilityLabelledBy="dialog-title"> @@ -230,8 +224,6 @@ export function Dialogs() { </Dialog.Outer> <Dialog.Outer control={testDialog}> - <Dialog.Handle /> - <Dialog.ScrollableInner accessibilityDescribedBy="dialog-description" accessibilityLabelledBy="dialog-title"> @@ -356,8 +348,6 @@ export function Dialogs() { {shouldRenderUnmountTest && ( <Dialog.Outer control={unmountTestDialog}> - <Dialog.Handle /> - <Dialog.Inner label="test"> <H3 nativeID="dialog-title">Unmount Test Dialog</H3> <P nativeID="dialog-description">Will unmount in about 5 seconds</P> diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 18410bf39..02efad878 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -1,19 +1,28 @@ -import React, {useLayoutEffect} from 'react' +import React from 'react' import {Modal, View} from 'react-native' -import {StatusBar} from 'expo-status-bar' -import * as SystemUI from 'expo-system-ui' +import {useDialogStateControlContext} from '#/state/dialogs' import {useComposerState} from '#/state/shell/composer' import {atoms as a, useTheme} from '#/alf' -import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme' import {ComposePost, useComposerCancelRef} from '../com/composer/Composer' export function Composer({}: {winHeight: number}) { + const {setFullyExpandedCount} = useDialogStateControlContext() const t = useTheme() const state = useComposerState() const ref = useComposerCancelRef() const open = !!state + const prevOpen = React.useRef(open) + + React.useEffect(() => { + if (open && !prevOpen.current) { + setFullyExpandedCount(c => c + 1) + } else if (!open && prevOpen.current) { + setFullyExpandedCount(c => c - 1) + } + prevOpen.current = open + }, [open, setFullyExpandedCount]) return ( <Modal @@ -24,56 +33,18 @@ export function Composer({}: {winHeight: number}) { animationType="slide" onRequestClose={() => ref.current?.onPressCancel()}> <View style={[t.atoms.bg, a.flex_1]}> - <Providers open={open}> - <ComposePost - cancelRef={ref} - replyTo={state?.replyTo} - onPost={state?.onPost} - quote={state?.quote} - quoteCount={state?.quoteCount} - mention={state?.mention} - text={state?.text} - imageUris={state?.imageUris} - videoUri={state?.videoUri} - /> - </Providers> + <ComposePost + cancelRef={ref} + replyTo={state?.replyTo} + onPost={state?.onPost} + quote={state?.quote} + quoteCount={state?.quoteCount} + mention={state?.mention} + text={state?.text} + imageUris={state?.imageUris} + videoUri={state?.videoUri} + /> </View> </Modal> ) } - -function Providers({ - children, - open, -}: { - children: React.ReactNode - open: boolean -}) { - // on iOS, it's a native formSheet. We use FullWindowOverlay to make - // the dialogs appear over it - return ( - <> - {children} - <IOSModalBackground active={open} /> - </> - ) -} - -// Generally, the backdrop of the app is the theme color, but when this is open -// we want it to be black due to the modal being a form sheet. -function IOSModalBackground({active}: {active: boolean}) { - const theme = useThemeName() - - useLayoutEffect(() => { - SystemUI.setBackgroundColorAsync('black') - - return () => { - SystemUI.setBackgroundColorAsync(getBackgroundColor(theme)) - } - }, [theme]) - - // Set the status bar to light - however, only if the modal is active - // If we rely on this component being mounted to set this, - // there'll be a delay before it switches back to default. - return active ? <StatusBar style="light" animated /> : null -} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index aed92cbb7..8bc3de24d 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,6 +13,14 @@ import * as NavigationBar from 'expo-navigation-bar' import {StatusBar} from 'expo-status-bar' import {useNavigation, useNavigationState} from '@react-navigation/native' +import {useDedupe} from '#/lib/hooks/useDedupe' +import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' +import {usePalette} from '#/lib/hooks/usePalette' +import {useNotificationsRegistration} from '#/lib/notifications/notifications' +import {isStateAtTabRoot} from '#/lib/routes/helpers' +import {useTheme} from '#/lib/ThemeContext' +import {isAndroid, isIOS} from '#/platform/detection' +import {useDialogStateControlContext} from '#/state/dialogs' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -20,17 +28,9 @@ import { useSetDrawerOpen, } from '#/state/shell' import {useCloseAnyActiveElement} from '#/state/util' -import {useDedupe} from 'lib/hooks/useDedupe' -import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler' -import {usePalette} from 'lib/hooks/usePalette' -import {useNotificationsRegistration} from 'lib/notifications/notifications' -import {isStateAtTabRoot} from 'lib/routes/helpers' -import {useTheme} from 'lib/ThemeContext' -import {isAndroid} from 'platform/detection' -import {useDialogStateContext} from 'state/dialogs' -import {Lightbox} from 'view/com/lightbox/Lightbox' -import {ModalsContainer} from 'view/com/modals/Modal' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {Lightbox} from '#/view/com/lightbox/Lightbox' +import {ModalsContainer} from '#/view/com/modals/Modal' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' import {Outlet as PortalOutlet} from '#/components/Portal' @@ -61,7 +61,6 @@ function ShellInner() { const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const {hasSession} = useSession() const closeAnyActiveElement = useCloseAnyActiveElement() - const {importantForAccessibility} = useDialogStateContext() useNotificationsRegistration() useNotificationsHandler() @@ -101,9 +100,7 @@ function ShellInner() { return ( <> - <Animated.View - style={containerPadding} - importantForAccessibility={importantForAccessibility}> + <Animated.View style={containerPadding}> <ErrorBoundary> <Drawer renderDrawerContent={renderDrawerContent} @@ -127,6 +124,7 @@ function ShellInner() { } export const Shell: React.FC = function ShellImpl() { + const {fullyExpandedCount} = useDialogStateControlContext() const pal = usePalette('default') const theme = useTheme() React.useEffect(() => { @@ -140,7 +138,14 @@ export const Shell: React.FC = function ShellImpl() { }, [theme]) return ( <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> - <StatusBar style={theme.colorScheme === 'dark' ? 'light' : 'dark'} /> + <StatusBar + style={ + theme.colorScheme === 'dark' || (isIOS && fullyExpandedCount > 0) + ? 'light' + : 'dark' + } + animated + /> <RoutesContainer> <ShellInner /> </RoutesContainer> |