mattermost-mobile/app/context/extra_keyboard/index.tsx
Elias Nahum d25c6fe245
Extra keyboard (#8348)
* Add support to use the keyboard area with a component

* fix import

* add missing providers to involved screens

* Change the way we handle the keyboard to allow using custom components in that area

* review feedback
2024-11-29 11:58:13 +08:00

183 lines
6.3 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {Keyboard, Platform} from 'react-native';
import Animated, {KeyboardState, useAnimatedKeyboard, useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Screens} from '@constants';
import {useIsTablet} from '@hooks/device';
import NavigationStore from '@store/navigation_store';
import {preventDoubleTap} from '@utils/tap';
import type {AvailableScreens} from '@typings/screens/navigation';
export type ExtraKeyboardContextProps = {
isExtraKeyboardVisible: boolean;
component: React.ReactElement|null;
showExtraKeyboard: (component: React.ReactElement|null) => void;
hideExtraKeyboard: () => void;
registerTextInputFocus: () => void;
registerTextInputBlur: () => void;
};
// This is based on the size of the tab bar
const KEYBOARD_OFFSET = -77;
export const ExtraKeyboardContext = createContext<ExtraKeyboardContextProps|undefined>(undefined);
const useOffetForCurrentScreen = (): number => {
const [screen, setScreen] = useState<AvailableScreens|undefined>();
const [offset, setOffset] = useState(0);
const isTablet = useIsTablet();
useEffect(() => {
const sub = NavigationStore.getSubject();
const s = sub.subscribe(setScreen);
return () => s.unsubscribe();
}, []);
useEffect(() => {
if (isTablet && screen === Screens.HOME) {
setOffset(KEYBOARD_OFFSET);
}
}, [isTablet, screen]);
return offset;
};
export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement|React.ReactElement[]}) => {
const [isExtraKeyboardVisible, setExtraKeyboardVisible] = useState(false);
const [component, setComponent] = useState<React.ReactElement|null>(null);
const [isTextInputFocused, setIsTextInputFocused] = useState(false);
const showExtraKeyboard = useCallback((newComponent: React.ReactElement|null) => {
setExtraKeyboardVisible(true);
setComponent(newComponent);
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
}, []);
const hideExtraKeyboard = useCallback(() => {
setExtraKeyboardVisible(false);
setComponent(null);
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
}, []);
const registerTextInputFocus = useCallback(() => {
// If the extra keyboard is opened if we don't do this
// we get a glitch in the UI that will animate the extra keyboard down
// and immediately bring the keyboard, by doing this
// we delay hidding the extra keyboard, so that there is no animation glitch
setIsTextInputFocused(true);
setTimeout(() => {
setExtraKeyboardVisible(false);
}, 400);
}, []);
const registerTextInputBlur = useCallback(() => {
setIsTextInputFocused(false);
}, []);
useEffect(() => {
const keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
if (isTextInputFocused) {
setExtraKeyboardVisible(false);
}
});
return () => keyboardHideListener.remove();
}, [isTextInputFocused]);
return (
<ExtraKeyboardContext.Provider
value={{
isExtraKeyboardVisible,
component,
showExtraKeyboard,
hideExtraKeyboard,
registerTextInputBlur,
registerTextInputFocus,
}}
>
{children}
</ExtraKeyboardContext.Provider>
);
});
export const useExtraKeyboardContext = (): ExtraKeyboardContextProps|undefined => {
const context = useContext(ExtraKeyboardContext);
if (!context) {
throw new Error('useExtraKeyboardContext must be used within a ExtraKeyboardProvider');
}
return context;
};
export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, dependencies: React.DependencyList = []) => {
const keyboardContext = useExtraKeyboardContext();
return useCallback(preventDoubleTap((...args: any) => {
if (keyboardContext?.isExtraKeyboardVisible) {
keyboardContext.hideExtraKeyboard();
/*
/* At this point the early return is commented
/* Based on the UX we actually want to have
/* we can uncoment this and reaturn as early
/* as the custom keyboard is hidden
*/
// return;
}
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
callback(...args);
}), [keyboardContext, ...dependencies]);
};
export const ExtraKeyboard = () => {
const keyb = useAnimatedKeyboard({isStatusBarTranslucentAndroid: true});
const defaultKeyboardHeight = Platform.select({ios: 291, default: 240});
const context = useExtraKeyboardContext();
const insets = useSafeAreaInsets();
const offset = useOffetForCurrentScreen();
const maxKeyboardHeight = useDerivedValue(() => {
if (keyb.state.value === KeyboardState.OPEN) {
const keyboardOffset = keyb.height.value < 70 ? 0 : offset; // When using a hw keyboard
return keyb.height.value + keyboardOffset;
}
return defaultKeyboardHeight;
});
const animatedStyle = useAnimatedStyle(() => {
let height = keyb.height.value + offset;
if (keyb.height.value < 70) {
height = 0; // When using a hw keyboard
}
if (context?.isExtraKeyboardVisible) {
height = withTiming(maxKeyboardHeight.value, {duration: 250});
} else if (keyb.state.value === KeyboardState.CLOSED || keyb.state.value === KeyboardState.UNKNOWN) {
height = withTiming(0, {duration: 250});
}
return {
height,
marginBottom: withTiming((keyb.state.value === KeyboardState.CLOSED || keyb.state.value === KeyboardState.CLOSING || keyb.state.value === KeyboardState.UNKNOWN) ? insets.bottom : 0, {duration: 250}),
};
}, [context, insets.bottom, offset]);
return (
<Animated.View style={animatedStyle}>
{context?.isExtraKeyboardVisible && context.component}
</Animated.View>
);
};