diff options
Diffstat (limited to 'src/view')
26 files changed, 795 insertions, 690 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 f4e290ca8..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 = { @@ -184,13 +187,10 @@ export const ComposePost = ({ initQuote, ) - const [videoAltText, setVideoAltText] = useState('') - const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) - // 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}) }, @@ -421,10 +422,9 @@ export const ComposePost = ({ try { postUri = ( await apilib.post(agent, { - composerState, // TODO: not used yet. + composerState, // TODO: move more state here. rawText: richtext.text, replyTo: replyTo?.uri, - images, quote, extLink, labels, @@ -432,18 +432,6 @@ export const ComposePost = ({ postgate, onStateChange: setProcessingState, langs: toPostLanguages(langPrefs.postLanguage), - video: - videoState.status === 'done' - ? { - blobRef: videoState.pendingPublish.blobRef, - altText: videoAltText, - captions: captions, - aspectRatio: { - width: videoState.asset.width, - height: videoState.asset.height, - }, - } - : undefined, }) ).uri try { @@ -521,7 +509,6 @@ export const ComposePost = ({ [ _, agent, - captions, composerState, extLink, images, @@ -540,9 +527,7 @@ export const ComposePost = ({ setExtLink, setLangPrefs, threadgateAllowUISettings, - videoAltText, videoState.asset, - videoState.pendingPublish, videoState.status, ], ) @@ -582,6 +567,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 +586,7 @@ export const ComposePost = ({ const handleChangeGifAltText = useCallback( (altText: string) => { + dispatch({type: 'embed_update_gif', alt: altText}) setExtLink(ext => ext && ext.meta ? { @@ -629,268 +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 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> + + {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> )} - </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`, + )} + /> + </View> - {isAltTextRequiredAndMissing && ( - <View style={[styles.reminderLine, pal.viewLight]}> - <View style={styles.errorIcon}> - <FontAwesomeIcon - icon="exclamation" - style={{color: colors.red4}} - size={10} + <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) + }} + /> + <GifAltText + link={extLink} + gif={extGif} + onSubmit={handleChangeGifAltText} + Portal={Portal.Portal} /> </View> - <Text style={[pal.text, a.flex_1]}> - <Trans>One or more images is missing alt text.</Trans> - </Text> + )} + <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} + /> + </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} </View> + </Animated.ScrollView> + <SuggestedLanguage text={richtext.text} /> + + {replyTo ? null : ( + <ThreadgateBtn + postgate={postgate} + onChangePostgate={setPostgate} + threadgateAllowUISettings={threadgateAllowUISettings} + onChangeThreadgateAllowUISettings={ + onChangeThreadgateAllowUISettings + } + style={bottomBarAnimatedStyle} + Portal={Portal.Portal} + /> )} - <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, + t.atoms.bg, + t.atoms.border_contrast_medium, + styles.bottomBar, ]}> - <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`, - )} - /> - </View> - - <Gallery images={images} dispatch={dispatch} /> - {images.length === 0 && extLink && ( - <View style={a.relative}> - <ExternalEmbed - link={extLink} - gif={extGif} - onRemove={() => { - setExtLink(undefined) - setExtGif(undefined) - }} - /> - <GifAltText - link={extLink} - gif={extGif} - onSubmit={handleChangeGifAltText} - /> - </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={videoAltText} - saveAltText={setVideoAltText} - captions={captions} - setCaptions={setCaptions} + {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} /> - </Animated.View> + <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> )} - </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={() => setQuote(undefined)} /> - )} - </View> - ) : null} + <View style={a.flex_1} /> + <SelectLangBtn /> + <CharProgress count={graphemeLength} /> </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> - ) : null} - </ToolbarWrapper> - )} - <View style={a.flex_1} /> - <SelectLangBtn /> - <CharProgress count={graphemeLength} /> </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/state/composer.ts b/src/view/com/composer/state/composer.ts index a23a5d8c8..769a0521d 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -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, }, } } diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts index 269505657..e29687200 100644 --- a/src/view/com/composer/state/video.ts +++ b/src/view/com/composer/state/video.ts @@ -4,8 +4,6 @@ import {JobStatus} from '@atproto/api/dist/client/types/app/bsky/video/defs' import {I18n} from '@lingui/core' import {msg} from '@lingui/macro' -import {createVideoAgent} from '#/lib/media/video/util' -import {uploadVideo} from '#/lib/media/video/upload' import {AbortError} from '#/lib/async/cancelable' import {compressVideo} from '#/lib/media/video/compress' import { @@ -14,8 +12,12 @@ import { VideoTooLargeError, } from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' +import {uploadVideo} from '#/lib/media/video/upload' +import {createVideoAgent} from '#/lib/media/video/util' import {logger} from '#/logger' +type CaptionsTrack = {lang: string; file: File} + export type VideoAction = | { type: 'compressing_to_uploading' @@ -41,6 +43,16 @@ export type VideoAction = signal: AbortSignal } | { + type: 'update_alt_text' + altText: string + signal: AbortSignal + } + | { + type: 'update_captions' + updater: (prev: CaptionsTrack[]) => CaptionsTrack[] + signal: AbortSignal + } + | { type: 'update_job_status' jobStatus: AppBskyVideoDefs.JobStatus signal: AbortSignal @@ -57,6 +69,8 @@ export const NO_VIDEO = Object.freeze({ video: undefined, jobId: undefined, pendingPublish: undefined, + altText: '', + captions: [], }) export type NoVideoState = typeof NO_VIDEO @@ -70,6 +84,8 @@ type ErrorState = { jobId: string | null error: string pendingPublish?: undefined + altText: string + captions: CaptionsTrack[] } type CompressingState = { @@ -80,6 +96,8 @@ type CompressingState = { video?: undefined jobId?: undefined pendingPublish?: undefined + altText: string + captions: CaptionsTrack[] } type UploadingState = { @@ -90,6 +108,8 @@ type UploadingState = { video: CompressedVideo jobId?: undefined pendingPublish?: undefined + altText: string + captions: CaptionsTrack[] } type ProcessingState = { @@ -101,6 +121,8 @@ type ProcessingState = { jobId: string jobStatus: AppBskyVideoDefs.JobStatus | null pendingPublish?: undefined + altText: string + captions: CaptionsTrack[] } type DoneState = { @@ -111,6 +133,8 @@ type DoneState = { video: CompressedVideo jobId?: undefined pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean} + altText: string + captions: CaptionsTrack[] } export type VideoState = @@ -129,6 +153,8 @@ export function createVideoState( progress: 0, abortController, asset, + altText: '', + captions: [], } } @@ -149,6 +175,8 @@ export function videoReducer( asset: state.asset ?? null, video: state.video ?? null, jobId: state.jobId ?? null, + altText: state.altText, + captions: state.captions, } } else if (action.type === 'update_progress') { if (state.status === 'compressing' || state.status === 'uploading') { @@ -164,6 +192,16 @@ export function videoReducer( asset: {...state.asset, width: action.width, height: action.height}, } } + } else if (action.type === 'update_alt_text') { + return { + ...state, + altText: action.altText, + } + } else if (action.type === 'update_captions') { + return { + ...state, + captions: action.updater(state.captions), + } } else if (action.type === 'compressing_to_uploading') { if (state.status === 'compressing') { return { @@ -172,6 +210,8 @@ export function videoReducer( abortController: state.abortController, asset: state.asset, video: action.video, + altText: state.altText, + captions: state.captions, } } return state @@ -185,6 +225,8 @@ export function videoReducer( video: state.video, jobId: action.jobId, jobStatus: null, + altText: state.altText, + captions: state.captions, } } } else if (action.type === 'update_job_status') { @@ -210,6 +252,8 @@ export function videoReducer( blobRef: action.blobRef, mutableProcessed: false, }, + altText: state.altText, + captions: state.captions, } } } 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 c07fdfc56..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,18 +17,20 @@ 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' const MAX_NUM_CAPTIONS = 1 +type CaptionsTrack = {lang: string; file: File} + interface Props { defaultAltText: string - captions: {lang: string; file: File}[] + captions: CaptionsTrack[] saveAltText: (altText: string) => void - setCaptions: React.Dispatch< - React.SetStateAction<{lang: string; file: File}[]> - > + 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> @@ -198,9 +198,7 @@ function SubtitleFileRow({ language: string file: File otherLanguages: {code2: string; code3: string; name: string}[] - setCaptions: React.Dispatch< - React.SetStateAction<{lang: string; file: File}[]> - > + setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void style: StyleProp<ViewStyle> }) { const {_} = useLingui() diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index 5ad4c256d..c5582922a 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -48,7 +48,7 @@ export function PostThreadComposePrompt({ accessibilityHint={_(msg`Opens composer`)} style={[ gtMobile ? a.py_xs : {paddingTop: 8, paddingBottom: 11}, - gtMobile ? {paddingLeft: 6, paddingRight: 6} : a.px_sm, + a.px_sm, a.border_t, t.atoms.border_contrast_low, t.atoms.bg, diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx index b75731f6f..1808e91a3 100644 --- a/src/view/com/post-thread/PostThreadFollowBtn.tsx +++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx @@ -1,14 +1,9 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {s} from '#/lib/styles' import {logger} from '#/logger' import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import { @@ -16,8 +11,11 @@ import { useProfileQuery, } from '#/state/queries/profile' import {useRequireAuth} from '#/state/session' -import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useBreakpoints} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' export function PostThreadFollowBtn({did}: {did: string}) { const {data: profile, isLoading} = useProfileQuery({did}) @@ -36,9 +34,7 @@ function PostThreadFollowBtnLoaded({ }) { const navigation = useNavigation() const {_} = useLingui() - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {isTabletOrDesktop} = useWebMediaQueries() + const {gtMobile} = useBreakpoints() const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> = useProfileShadow(profileUnshadowed) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( @@ -113,51 +109,32 @@ function PostThreadFollowBtnLoaded({ if (!showFollowBtn) return null return ( - <View style={{width: isTabletOrDesktop ? 130 : 120}}> - <View style={styles.btnOuter}> - <TouchableOpacity - testID="followBtn" - onPress={onPress} - style={[styles.btn, !isFollowing ? palInverted.view : pal.viewLight]} - accessibilityRole="button" - accessibilityLabel={_(msg`Follow ${profile.handle}`)} - accessibilityHint={_( - msg`Shows posts from ${profile.handle} in your feed`, - )}> - {isTabletOrDesktop && ( - <FontAwesomeIcon - icon={!isFollowing ? 'plus' : 'check'} - style={[!isFollowing ? palInverted.text : pal.text, s.mr5]} - /> - )} - <Text - type="button" - style={[!isFollowing ? palInverted.text : pal.text, s.bold]} - numberOfLines={1}> - {!isFollowing ? ( - isFollowedBy ? ( - <Trans>Follow Back</Trans> - ) : ( - <Trans>Follow</Trans> - ) - ) : ( - <Trans>Following</Trans> - )} - </Text> - </TouchableOpacity> - </View> - </View> + <Button + testID="followBtn" + label={_(msg`Follow ${profile.handle}`)} + onPress={onPress} + size="small" + variant="solid" + color="secondary_inverted" + style={[a.rounded_full]}> + {gtMobile && ( + <ButtonIcon + icon={isFollowing ? Check : Plus} + position="left" + size="sm" + /> + )} + <ButtonText> + {!isFollowing ? ( + isFollowedBy ? ( + <Trans>Follow Back</Trans> + ) : ( + <Trans>Follow</Trans> + ) + ) : ( + <Trans>Following</Trans> + )} + </ButtonText> + </Button> ) } - -const styles = StyleSheet.create({ - btnOuter: { - marginLeft: 'auto', - }, - btn: { - flexDirection: 'row', - borderRadius: 50, - paddingVertical: 8, - paddingHorizontal: 14, - }, -}) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index ead9df116..4701f225c 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -14,14 +14,12 @@ import {useLingui} from '@lingui/react' import {MAX_POST_LINES} from '#/lib/constants' import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {countLines} from '#/lib/strings/helpers' import {niceDate} from '#/lib/strings/time' import {s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' @@ -30,9 +28,10 @@ import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {AppModerationCause} from '#/components/Pills' import {RichText} from '#/components/RichText' +import {Text as NewText} from '#/components/Typography' import {ContentHider} from '../../../components/moderation/ContentHider' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' @@ -180,6 +179,7 @@ let PostThreadItemLoaded = ({ hideTopBorder?: boolean threadgateRecord?: AppBskyFeedThreadgate.Record }): React.ReactNode => { + const t = useTheme() const pal = usePalette('default') const {_, i18n} = useLingui() const langPrefs = useLanguagePrefs() @@ -268,8 +268,14 @@ let PostThreadItemLoaded = ({ return ( <> {rootUri !== post.uri && ( - <View style={{paddingLeft: 16, flexDirection: 'row', height: 16}}> - <View style={{width: 38}}> + <View + style={[ + a.pl_lg, + a.flex_row, + a.pb_xs, + {height: a.pt_lg.paddingTop}, + ]}> + <View style={{width: 42}}> <View style={[ styles.replyLine, @@ -286,88 +292,74 @@ let PostThreadItemLoaded = ({ <View testID={`postThreadItem-by-${post.author.handle}`} style={[ - styles.outer, - styles.outerHighlighted, - pal.border, - pal.view, - rootUri === post.uri && styles.outerHighlightedRoot, - hideTopBorder && styles.noTopBorder, - ]} - accessible={false}> - <View style={[styles.layout]}> - <View style={[styles.layoutAvi, {paddingBottom: 8}]}> - <PreviewableUserAvatar - size={42} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <View style={styles.layoutContent}> - <View - style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}> - <Link style={s.flex1} href={authorHref} title={authorTitle}> - <Text - emoji - type="xl-bold" - style={[pal.text, a.self_start]} - numberOfLines={1} - lineHeight={1.2}> - {sanitizeDisplayName( - post.author.displayName || - sanitizeHandle(post.author.handle), - moderation.ui('displayName'), - )} - </Text> - </Link> - </View> - <View style={styles.meta}> - <Link style={s.flex1} href={authorHref} title={authorTitle}> - <Text - emoji - type="md" - style={[pal.textLight]} - numberOfLines={1}> - {sanitizeHandle(post.author.handle, '@')} - </Text> - </Link> - </View> + a.px_lg, + t.atoms.border_contrast_low, + // root post styles + rootUri === post.uri && [a.pt_lg], + ]}> + <View style={[a.flex_row, a.gap_md, a.pb_md]}> + <PreviewableUserAvatar + size={42} + profile={post.author} + moderation={moderation.ui('avatar')} + type={post.author.associated?.labeler ? 'labeler' : 'user'} + /> + <View style={[a.flex_1]}> + <Link style={s.flex1} href={authorHref} title={authorTitle}> + <NewText + emoji + style={[a.text_lg, a.font_bold, a.leading_snug, a.self_start]} + numberOfLines={1}> + {sanitizeDisplayName( + post.author.displayName || + sanitizeHandle(post.author.handle), + moderation.ui('displayName'), + )} + </NewText> + </Link> + <Link style={s.flex1} href={authorHref} title={authorTitle}> + <NewText + emoji + style={[ + a.text_md, + a.leading_snug, + t.atoms.text_contrast_medium, + ]} + numberOfLines={1}> + {sanitizeHandle(post.author.handle, '@')} + </NewText> + </Link> </View> {currentAccount?.did !== post.author.did && ( - <PostThreadFollowBtn did={post.author.did} /> + <View> + <PostThreadFollowBtn did={post.author.did} /> + </View> )} </View> - <View style={[s.pl10, s.pr10, s.pb10]}> - <LabelsOnMyPost post={post} /> + <View style={[a.pb_sm]}> + <LabelsOnMyPost post={post} style={[a.pb_sm]} /> <ContentHider modui={moderation.ui('contentView')} ignoreMute - style={styles.contentHider} - childContainerStyle={styles.contentHiderChild}> + childContainerStyle={[a.pt_sm]}> <PostAlerts modui={moderation.ui('contentView')} size="lg" includeMute - style={[a.pt_2xs, a.pb_sm]} + style={[a.pb_sm]} additionalCauses={additionalPostAlerts} /> {richText?.text ? ( - <View - style={[ - styles.postTextContainer, - styles.postTextLargeContainer, - ]}> - <RichText - enableTags - selectable - value={richText} - style={[a.flex_1, a.text_xl]} - authorHandle={post.author.handle} - /> - </View> + <RichText + enableTags + selectable + value={richText} + style={[a.flex_1, a.text_xl]} + authorHandle={post.author.handle} + /> ) : undefined} {post.embed && ( - <View style={[a.pb_sm]}> + <View style={[a.py_xs]}> <PostEmbeds embed={post.embed} moderation={moderation} @@ -386,68 +378,73 @@ let PostThreadItemLoaded = ({ post.likeCount !== 0 || post.quoteCount !== 0 ? ( // Show this section unless we're *sure* it has no engagement. - <View style={[styles.expandedInfo, pal.border]}> + <View + style={[ + a.flex_row, + a.align_center, + a.gap_lg, + a.border_t, + a.border_b, + a.mt_md, + a.py_md, + t.atoms.border_contrast_low, + ]}> {post.repostCount != null && post.repostCount !== 0 ? ( - <Link - style={styles.expandedInfoItem} - href={repostsHref} - title={repostsTitle}> - <Text + <Link href={repostsHref} title={repostsTitle}> + <NewText testID="repostCount-expanded" - type="lg" - style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> + style={[a.text_md, t.atoms.text_contrast_medium]}> + <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> {formatCount(i18n, post.repostCount)} - </Text>{' '} + </NewText>{' '} <Plural value={post.repostCount} one="repost" other="reposts" /> - </Text> + </NewText> </Link> ) : null} {post.quoteCount != null && post.quoteCount !== 0 && !post.viewer?.embeddingDisabled ? ( - <Link - style={styles.expandedInfoItem} - href={quotesHref} - title={quotesTitle}> - <Text + <Link href={quotesHref} title={quotesTitle}> + <NewText testID="quoteCount-expanded" - type="lg" - style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> + style={[a.text_md, t.atoms.text_contrast_medium]}> + <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> {formatCount(i18n, post.quoteCount)} - </Text>{' '} + </NewText>{' '} <Plural value={post.quoteCount} one="quote" other="quotes" /> - </Text> + </NewText> </Link> ) : null} {post.likeCount != null && post.likeCount !== 0 ? ( - <Link - style={styles.expandedInfoItem} - href={likesHref} - title={likesTitle}> - <Text + <Link href={likesHref} title={likesTitle}> + <NewText testID="likeCount-expanded" - type="lg" - style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> + style={[a.text_md, t.atoms.text_contrast_medium]}> + <NewText style={[a.text_md, a.font_bold, t.atoms.text]}> {formatCount(i18n, post.likeCount)} - </Text>{' '} + </NewText>{' '} <Plural value={post.likeCount} one="like" other="likes" /> - </Text> + </NewText> </Link> ) : null} </View> ) : null} - <View style={[s.pl10, s.pr10]}> + <View + style={[ + a.pt_sm, + a.pb_2xs, + { + marginLeft: -5, + }, + ]}> <PostCtrls big post={post} @@ -481,9 +478,8 @@ let PostThreadItemLoaded = ({ testID={`postThreadItem-by-${post.author.handle}`} href={postHref} disabled={overrideBlur} - style={[pal.view]} modui={moderation.ui('contentList')} - iconSize={isThreadedChild ? 26 : 38} + iconSize={isThreadedChild ? 24 : 42} iconStyles={ isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2} } @@ -496,7 +492,7 @@ let PostThreadItemLoaded = ({ paddingLeft: 8, height: isThreadedChildAdjacentTop ? 8 : 16, }}> - <View style={{width: 38}}> + <View style={{width: 42}}> {!isThreadedChild && showParentReplyLine && ( <View style={[ @@ -514,7 +510,9 @@ let PostThreadItemLoaded = ({ <View style={[ - styles.layout, + a.flex_row, + a.px_sm, + a.gap_md, { paddingBottom: showChildReplyLine && !isThreadedChild @@ -526,9 +524,9 @@ let PostThreadItemLoaded = ({ ]}> {/* If we are in threaded mode, the avatar is rendered in PostMeta */} {!isThreadedChild && ( - <View style={styles.layoutAvi}> + <View> <PreviewableUserAvatar - size={38} + size={42} profile={post.author} moderation={moderation.ui('avatar')} type={post.author.associated?.labeler ? 'labeler' : 'user'} @@ -549,12 +547,7 @@ let PostThreadItemLoaded = ({ </View> )} - <View - style={ - isThreadedChild - ? styles.layoutContentThreaded - : styles.layoutContent - }> + <View style={[a.flex_1]}> <PostMeta author={post.author} moderation={moderation} @@ -563,20 +556,16 @@ let PostThreadItemLoaded = ({ showAvatar={isThreadedChild} avatarModeration={moderation.ui('avatar')} avatarSize={24} - style={ - isThreadedChild && { - paddingBottom: isWeb ? 5 : 4, - } - } + style={[a.pb_xs]} /> - <LabelsOnMyPost post={post} /> + <LabelsOnMyPost post={post} style={[a.pb_xs]} /> <PostAlerts modui={moderation.ui('contentList')} - style={[a.pt_2xs, a.pb_2xs]} + style={[a.pb_2xs]} additionalCauses={additionalPostAlerts} /> {richText?.text ? ( - <View style={styles.postTextContainer}> + <View style={[a.pb_2xs, a.pr_sm]}> <RichText enableTags value={richText} @@ -659,29 +648,31 @@ function PostOuterWrapper({ hasPrecedingItem: boolean hideTopBorder?: boolean }>) { - const {isMobile} = useWebMediaQueries() - const pal = usePalette('default') + const t = useTheme() if (treeView && depth > 0) { return ( <View style={[ - pal.border, + a.flex_row, + a.px_sm, + t.atoms.border_contrast_low, styles.cursor, { flexDirection: 'row', - paddingHorizontal: isMobile ? 10 : 6, - borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0, + borderTopWidth: depth === 1 ? a.border_t.borderTopWidth : 0, }, ]}> {Array.from(Array(depth - 1)).map((_, n: number) => ( <View key={`${post.uri}-padding-${n}`} - style={{ - borderLeftWidth: 2, - borderLeftColor: pal.colors.border, - marginLeft: isMobile ? 6 : 12, - paddingLeft: isMobile ? 6 : 8, - }} + style={[ + a.ml_sm, + t.atoms.border_contrast_low, + { + borderLeftWidth: 2, + paddingLeft: a.pl_sm.paddingLeft - 2, // minus border + }, + ]} /> ))} <View style={{flex: 1}}>{children}</View> @@ -691,8 +682,9 @@ function PostOuterWrapper({ return ( <View style={[ - styles.outer, - pal.border, + a.border_t, + a.px_sm, + t.atoms.border_contrast_low, showParentReplyLine && hasPrecedingItem && styles.noTopBorder, hideTopBorder && styles.noTopBorder, styles.cursor, @@ -713,6 +705,7 @@ function ExpandedPostDetails({ needsTranslation: boolean translatorUrl: string }) { + const t = useTheme() const pal = usePalette('default') const {_, i18n} = useLingui() const openLink = useOpenLink() @@ -723,31 +716,25 @@ function ExpandedPostDetails({ }, [openLink, translatorUrl]) return ( - <View - style={[ - a.flex_row, - a.align_center, - a.flex_wrap, - a.gap_xs, - s.mt2, - s.mb10, - ]}> - <Text style={[a.text_sm, pal.textLight]}> + <View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_sm, a.pt_md]}> + <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}> {niceDate(i18n, post.indexedAt)} - </Text> + </NewText> {isRootPost && ( <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> )} {needsTranslation && ( <> - <Text style={[a.text_sm, pal.textLight]}>·</Text> + <NewText style={[a.text_sm, t.atoms.text_contrast_medium]}> + · + </NewText> - <Text + <NewText style={[a.text_sm, pal.link]} title={_(msg`Translate`)} onPress={onTranslatePress}> <Trans>Translate</Trans> - </Text> + </NewText> </> )} </View> @@ -773,31 +760,9 @@ const styles = StyleSheet.create({ borderTopWidth: StyleSheet.hairlineWidth, paddingLeft: 8, }, - outerHighlighted: { - borderTopWidth: 0, - paddingTop: 4, - paddingLeft: 8, - paddingRight: 8, - }, - outerHighlightedRoot: { - borderTopWidth: StyleSheet.hairlineWidth, - paddingTop: 16, - }, noTopBorder: { borderTopWidth: 0, }, - layout: { - flexDirection: 'row', - paddingHorizontal: 8, - }, - layoutAvi: {}, - layoutContent: { - flex: 1, - marginLeft: 10, - }, - layoutContentThreaded: { - flex: 1, - }, meta: { flexDirection: 'row', paddingVertical: 2, @@ -805,42 +770,6 @@ const styles = StyleSheet.create({ metaExpandedLine1: { paddingVertical: 0, }, - alert: { - marginBottom: 6, - }, - postTextContainer: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - paddingBottom: 4, - paddingRight: 10, - overflow: 'hidden', - }, - postTextLargeContainer: { - paddingHorizontal: 0, - paddingRight: 0, - paddingBottom: 10, - }, - translateLink: { - marginBottom: 6, - }, - contentHider: { - marginBottom: 6, - }, - contentHiderChild: { - marginTop: 6, - }, - expandedInfo: { - flexDirection: 'row', - padding: 10, - borderTopWidth: StyleSheet.hairlineWidth, - borderBottomWidth: StyleSheet.hairlineWidth, - marginTop: 5, - marginBottom: 10, - }, - expandedInfoItem: { - marginRight: 10, - }, loadMore: { flexDirection: 'row', alignItems: 'center', 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/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 64fa504eb..1d4cf8ff0 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -100,7 +100,7 @@ export function ViewHeader({ </TouchableOpacity> ) : null} <View style={styles.titleContainer} pointerEvents="none"> - <Text type="title" style={[pal.text, styles.title]}> + <Text emoji type="title" style={[pal.text, styles.title]}> {title} </Text> </View> 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/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 3d885480c..42ea79b8f 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -77,13 +77,16 @@ export function Text({ flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier } + const shared = { + uiTextView: true, + selectable, + style: flattened, + ...props, + } + return ( - <UITextView - style={flattened} - selectable={selectable} - uiTextView - {...props}> - {isIOS && emoji ? renderChildrenWithEmoji(children) : children} + <UITextView {...shared}> + {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children} </UITextView> ) } @@ -104,14 +107,16 @@ export function Text({ flattened.fontSize = flattened.fontSize * fonts.scaleMultiplier } + const shared = { + selectable, + style: flattened, + dataSet: Object.assign({tooltip: title}, dataSet || {}), + ...props, + } + return ( - <RNText - style={flattened} - // @ts-ignore web only -esb - dataSet={Object.assign({tooltip: title}, dataSet || {})} - selectable={selectable} - {...props}> - {isIOS && emoji ? renderChildrenWithEmoji(children) : children} + <RNText {...shared}> + {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children} </RNText> ) } 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> |