[Sheets] [Pt. 1] Root PR (#5557)
Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: dan <dan.abramov@gmail.com> Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
+296
-276
@@ -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 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>
|
||||
|
||||
{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}
|
||||
/>
|
||||
</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'}
|
||||
<ErrorBanner
|
||||
error={error}
|
||||
videoState={videoState}
|
||||
clearError={() => setError('')}
|
||||
clearVideo={clearVideo}
|
||||
/>
|
||||
<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>
|
||||
</Animated.View>
|
||||
<Animated.ScrollView
|
||||
layout={native(LinearTransition)}
|
||||
onScroll={scrollHandler}
|
||||
style={styles.scrollView}
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={onScrollViewContentSizeChange}
|
||||
onLayout={onScrollViewLayout}>
|
||||
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
|
||||
|
||||
<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)
|
||||
}}
|
||||
<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>
|
||||
)}
|
||||
</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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GifAltText
|
||||
link={extLink}
|
||||
gif={extGif}
|
||||
onSubmit={handleChangeGifAltText}
|
||||
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>
|
||||
)}
|
||||
<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}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -79,7 +79,6 @@ export function DisableEmail2FADialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -53,7 +53,6 @@ export function ExportCarDialog({
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
accessibilityLabelledBy="dialog-title">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+21
-16
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user