use KeyboardStickyView from r-n-keyboard-controller

This commit is contained in:
Samuel Newman
2024-04-16 23:53:09 +01:00
parent a5a85a6202
commit 426c812904
3 changed files with 232 additions and 202 deletions
+4 -1
View File
@@ -4,6 +4,7 @@ import 'view/icons'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler' import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {RootSiblingParent} from 'react-native-root-siblings' import {RootSiblingParent} from 'react-native-root-siblings'
import { import {
initialWindowMetrics, initialWindowMetrics,
@@ -156,7 +157,9 @@ function App() {
<LightboxStateProvider> <LightboxStateProvider>
<I18nProvider> <I18nProvider>
<PortalProvider> <PortalProvider>
<InnerApp /> <KeyboardProvider>
<InnerApp />
</KeyboardProvider>
</PortalProvider> </PortalProvider>
</I18nProvider> </I18nProvider>
</LightboxStateProvider> </LightboxStateProvider>
+194 -201
View File
@@ -43,14 +43,13 @@ import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import * as apilib from 'lib/api/index' import * as apilib from 'lib/api/index'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants' import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {insertMentionAt} from 'lib/strings/mention-manip' import {insertMentionAt} from 'lib/strings/mention-manip'
import {shortenLinks} from 'lib/strings/rich-text-manip' import {shortenLinks} from 'lib/strings/rich-text-manip'
import {colors, gradients, s} from 'lib/styles' import {colors, gradients, s} from 'lib/styles'
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection' import {isNative, isWeb} from 'platform/detection'
import {useDialogStateControlContext} from 'state/dialogs' import {useDialogStateControlContext} from 'state/dialogs'
import {GalleryModel} from 'state/models/media/gallery' import {GalleryModel} from 'state/models/media/gallery'
import {ComposerOpts} from 'state/shell/composer' import {ComposerOpts} from 'state/shell/composer'
@@ -66,6 +65,7 @@ import {UserAvatar} from '../util/UserAvatar'
import {CharProgress} from './char-progress/CharProgress' import {CharProgress} from './char-progress/CharProgress'
import {ExternalEmbed} from './ExternalEmbed' import {ExternalEmbed} from './ExternalEmbed'
import {GifAltText} from './GifAltText' import {GifAltText} from './GifAltText'
import {KeyboardAccessory} from './KeyboardAccessory'
import {LabelsBtn} from './labels/LabelsBtn' import {LabelsBtn} from './labels/LabelsBtn'
import {Gallery} from './photos/Gallery' import {Gallery} from './photos/Gallery'
import {OpenCameraBtn} from './photos/OpenCameraBtn' import {OpenCameraBtn} from './photos/OpenCameraBtn'
@@ -112,7 +112,6 @@ export const ComposePost = observer(function ComposePost({
const discardPromptControl = Prompt.usePromptControl() const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('') const [processingState, setProcessingState] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -150,11 +149,10 @@ export const ComposePost = observer(function ComposePost({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const viewStyles = useMemo( const viewStyles = useMemo(
() => ({ () => ({
paddingBottom: paddingBottom: isNative ? insets.bottom + 80 : 0,
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
paddingTop: isMobile && isWeb ? 15 : 0, paddingTop: isMobile && isWeb ? 15 : 0,
}), }),
[insets, isKeyboardVisible, isMobile], [insets, isMobile],
) )
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
@@ -355,199 +353,202 @@ export const ComposePost = observer(function ComposePost({
) )
return ( return (
<KeyboardAvoidingView <>
testID="composePostView" <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} testID="composePostView"
style={styles.outer}> behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
<View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal> style={styles.outer}>
<View style={[styles.topbar, isDesktop && styles.topbarDesktop]}> <View style={[s.flex1, viewStyles]} aria-modal accessibilityViewIsModal>
<TouchableOpacity <View style={[styles.topbar, isDesktop && styles.topbarDesktop]}>
testID="composerDiscardButton" <TouchableOpacity
onPress={onPressCancel} testID="composerDiscardButton"
onAccessibilityEscape={onPressCancel} onPress={onPressCancel}
accessibilityRole="button" onAccessibilityEscape={onPressCancel}
accessibilityLabel={_(msg`Cancel`)} accessibilityRole="button"
accessibilityHint={_( accessibilityLabel={_(msg`Cancel`)}
msg`Closes post composer and discards post draft`,
)}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{replyTo ? null : (
<ThreadgateBtn
threadgate={threadgate}
onChange={setThreadgate}
/>
)}
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</>
)}
</View>
{requireAltTextEnabled && gallery.needsAltText && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, s.flex1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, s.flex1]}>{error}</Text>
</View>
)}
<ScrollView
style={styles.scrollView}
keyboardShouldPersistTaps="always">
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View
style={[
pal.border,
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={true}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
onNewLink={onNewLink}
onError={setError}
accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_( accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`, msg`Closes post composer and discards post draft`,
)} )}>
/> <Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<>
<Text style={pal.textLight}>{processingState}</Text>
<View style={styles.postBtn}>
<ActivityIndicator />
</View>
</>
) : (
<>
<LabelsBtn
labels={labels}
onChange={setLabels}
hasMedia={hasMedia}
/>
{replyTo ? null : (
<ThreadgateBtn
threadgate={threadgate}
onChange={setThreadgate}
/>
)}
{canPost ? (
<TouchableOpacity
testID="composerPublishBtn"
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
colors={[
gradients.blueLight.start,
gradients.blueLight.end,
]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</>
)}
</View> </View>
{requireAltTextEnabled && gallery.needsAltText && (
<Gallery gallery={gallery} /> <View style={[styles.reminderLine, pal.viewLight]}>
{gallery.isEmpty && extLink && ( <View style={styles.errorIcon}>
<View style={a.relative}> <FontAwesomeIcon
<ExternalEmbed icon="exclamation"
link={extLink} style={{color: colors.red4}}
gif={extGif} size={10}
onRemove={() => { />
setExtLink(undefined) </View>
setExtGif(undefined) <Text style={[pal.text, s.flex1]}>
}} <Trans>One or more images is missing alt text.</Trans>
/> </Text>
<GifAltText
link={extLink}
gif={extGif}
onSubmit={handleChangeGifAltText}
/>
</View> </View>
)} )}
{quote ? ( {error !== '' && (
<View style={[s.mt5, isWeb && s.mb10]}> <View style={styles.errorLine}>
<View style={{pointerEvents: 'none'}}> <View style={styles.errorIcon}>
<QuoteEmbed quote={quote} /> <FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View> </View>
{quote.uri !== initQuote?.uri && ( <Text style={[s.red4, s.flex1]}>{error}</Text>
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View> </View>
) : undefined} )}
</ScrollView> <ScrollView
<SuggestedLanguage text={richtext.text} /> style={styles.scrollView}
<View style={[pal.border, styles.bottomBar]}> keyboardShouldPersistTaps="always">
<View style={[a.flex_row, a.align_center, a.gap_xs]}> {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> <View
<SelectGifBtn style={[
onClose={focusTextInput} pal.border,
onSelectGif={onSelectGif} styles.textInputLayout,
disabled={hasMedia} isNative && styles.textInputLayoutMobile,
/> ]}>
{!isMobile ? ( <UserAvatar
<Button avatar={currentProfile?.avatar}
onPress={onEmojiButtonPress} size={50}
style={a.p_sm} type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
label={_(msg`Open emoji picker`)} />
accessibilityHint={_(msg`Open emoji picker`)} <TextInput
variant="ghost" ref={textInput}
shape="round" richtext={richtext}
color="primary"> placeholder={selectTextInputPlaceholder}
<EmojiSmile size="lg" /> autoFocus={true}
</Button> setRichText={setRichText}
) : null} onPhotoPasted={onPhotoPasted}
</View> onPressPublish={onPressPublish}
<View style={s.flex1} /> onNewLink={onNewLink}
<SelectLangBtn /> onError={setError}
<CharProgress count={graphemeLength} /> accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
)}
/>
</View>
<Gallery gallery={gallery} />
{gallery.isEmpty && extLink && (
<View style={a.relative}>
<ExternalEmbed
link={extLink}
gif={extGif}
onRemove={() => {
setExtLink(undefined)
setExtGif(undefined)
}}
/>
<GifAltText
link={extLink}
gif={extGif}
onSubmit={handleChangeGifAltText}
/>
</View>
)}
{quote ? (
<View style={[s.mt5, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
{quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View>
) : undefined}
</ScrollView>
<SuggestedLanguage text={richtext.text} />
</View> </View>
</View> </KeyboardAvoidingView>
<KeyboardAccessory>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
<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}
</View>
<View style={s.flex1} />
<SelectLangBtn />
<CharProgress count={graphemeLength} />
</KeyboardAccessory>
<Prompt.Basic <Prompt.Basic
control={discardPromptControl} control={discardPromptControl}
@@ -557,7 +558,7 @@ export const ComposePost = observer(function ComposePost({
confirmButtonCta={_(msg`Discard`)} confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative" confirmButtonColor="negative"
/> />
</KeyboardAvoidingView> </>
) )
}) })
@@ -639,12 +640,4 @@ const styles = StyleSheet.create({
marginHorizontal: 10, marginHorizontal: 10,
marginBottom: 4, marginBottom: 4,
}, },
bottomBar: {
flexDirection: 'row',
paddingVertical: 4,
paddingLeft: 15,
paddingRight: 20,
alignItems: 'center',
borderTopWidth: 1,
},
}) })
@@ -0,0 +1,34 @@
import React from 'react'
import {View} from 'react-native'
import {KeyboardStickyView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {isWeb} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
export function KeyboardAccessory({children}: {children: React.ReactNode}) {
const t = useTheme()
const {bottom} = useSafeAreaInsets()
const style = [
a.flex_row,
{paddingVertical: 10},
a.pl_lg,
a.pr_xl,
a.align_center,
a.border_t,
t.atoms.border_contrast_medium,
t.atoms.bg,
]
// todo: when iPad support is added, it should also not use the KeyboardStickyView
if (isWeb) {
return <View style={style}>{children}</View>
}
return (
<KeyboardStickyView offset={{closed: -bottom}} style={style}>
{children}
</KeyboardStickyView>
)
}