mattermost-mobile/app/context/keyboard_animation.tsx
Daniel Espino García cff12f7182
Enable exhaustive deps in the repo (#9508)
* Enable exhaustive deps in the repo

* Add tests for the new hooks

* Update claude.md

* Remove pre-commit overwrite
2026-02-18 11:56:16 +01:00

197 lines
8 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {createContext, useContext, useMemo, useRef, useCallback, type ReactNode} from 'react';
import {useSharedValue, type SharedValue} from 'react-native-reanimated';
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
interface KeyboardAnimationContextType {
keyboardTranslateY: SharedValue<number>;
bottomInset: SharedValue<number>;
scrollOffset: SharedValue<number>;
keyboardHeight: SharedValue<number>;
scrollPosition: SharedValue<number>;
onScroll: (event: unknown) => void;
postInputContainerHeight: number;
inputRef: React.MutableRefObject<PasteInputRef | undefined>;
blurInput: () => void;
focusInput: () => void;
blurAndDismissKeyboard: () => Promise<void>;
isKeyboardFullyOpen: SharedValue<boolean>;
isKeyboardFullyClosed: SharedValue<boolean>;
isKeyboardInTransition: SharedValue<boolean>;
showInputAccessoryView: boolean;
isInputAccessoryViewMode: SharedValue<boolean>;
setShowInputAccessoryView: (show: boolean) => void;
lastKeyboardHeight: number;
inputAccessoryViewAnimatedHeight: SharedValue<number>;
isTransitioningFromCustomView: SharedValue<boolean>;
closeInputAccessoryView: () => void;
scrollToEnd: () => void;
isEmojiSearchFocused: boolean;
setIsEmojiSearchFocused: (focused: boolean) => void;
cursorPositionRef: React.MutableRefObject<number>;
registerCursorPosition: (cursorPosition: number, valueLength?: number) => void;
preserveCursorPositionForEmojiPicker: () => void;
clearCursorPositionPreservation: () => void;
isInEmojiPickerTransition: () => boolean;
getPreservedCursorPosition: () => number | null;
updateValue: React.Dispatch<React.SetStateAction<string>> | null;
updateCursorPosition: React.Dispatch<React.SetStateAction<number>> | null;
registerPostInputCallbacks: (
updateValueFn: React.Dispatch<React.SetStateAction<string>>,
updateCursorPositionFn: React.Dispatch<React.SetStateAction<number>>
) => void;
}
const KeyboardAnimationContext = createContext<KeyboardAnimationContextType | null>(null);
export const KeyboardAnimationProvider = ({
children,
value,
}: {
children: ReactNode;
value: KeyboardAnimationContextType;
}) => {
return (
<KeyboardAnimationContext.Provider value={value}>
{children}
</KeyboardAnimationContext.Provider>
);
};
const DEFAULT_POST_INPUT_HEIGHT = 91;
export const useKeyboardAnimationContext = () => {
const context = useContext(KeyboardAnimationContext);
// Always create default values (hooks must be called unconditionally)
const defaultKeyboardTranslateY = useSharedValue(0);
const defaultBottomInset = useSharedValue(0);
const defaultScrollOffset = useSharedValue(0);
const defaultKeyboardHeight = useSharedValue(0);
const defaultScrollPosition = useSharedValue(0);
const defaultIsKeyboardFullyOpen = useSharedValue(false);
const defaultIsKeyboardFullyClosed = useSharedValue(true);
const defaultIsKeyboardInTransition = useSharedValue(false);
const defaultInputRef = useRef<PasteInputRef | undefined>(undefined);
const defaultIsInputAccessoryViewMode = useSharedValue(false);
const defaultIsTransitioningFromCustomView = useSharedValue(false);
const defaultInputAccessoryViewAnimatedHeight = useSharedValue(0);
const defaultOnScroll = useCallback(() => {
// No-op fallback
}, []);
const defaultBlurInput = useCallback(() => {
// No-op fallback
}, []);
const defaultFocusInput = useCallback(() => {
// No-op fallback
}, []);
const defaultBlurAndDismissKeyboard = useCallback(async () => {
// No-op fallback
}, []);
const defaultSetShowInputAccessoryView = useCallback(() => {
// No-op fallback
}, []);
const defaultCloseInputAccessoryView = useCallback(() => {
// No-op fallback
}, []);
const defaultScrollToEnd = useCallback(() => {
// No-op fallback
}, []);
const defaultSetIsEmojiSearchFocused = useCallback(() => {
// No-op fallback
}, []);
const defaultCursorPositionRef = useRef<number>(0);
const defaultRegisterCursorPosition = useCallback(() => {
// No-op fallback
}, []);
const defaultPreserveCursorPositionForEmojiPicker = useCallback(() => {
// No-op fallback
}, []);
const defaultClearCursorPositionPreservation = useCallback(() => {
// No-op fallback
}, []);
const defaultIsInEmojiPickerTransition = useCallback(() => false, []);
const defaultGetPreservedCursorPosition = useCallback(() => null, []);
const defaultUpdateValue = useRef<React.Dispatch<React.SetStateAction<string>> | null>(null);
const defaultUpdateCursorPosition = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null);
const defaultRegisterPostInputCallbacks = useCallback(() => {
// No-op fallback
}, []);
const fallbackValue = useMemo(() => ({
keyboardTranslateY: defaultKeyboardTranslateY,
bottomInset: defaultBottomInset,
scrollOffset: defaultScrollOffset,
keyboardHeight: defaultKeyboardHeight,
scrollPosition: defaultScrollPosition,
onScroll: defaultOnScroll,
postInputContainerHeight: DEFAULT_POST_INPUT_HEIGHT,
inputRef: defaultInputRef,
blurInput: defaultBlurInput,
focusInput: defaultFocusInput,
blurAndDismissKeyboard: defaultBlurAndDismissKeyboard,
isKeyboardFullyOpen: defaultIsKeyboardFullyOpen,
isKeyboardFullyClosed: defaultIsKeyboardFullyClosed,
isKeyboardInTransition: defaultIsKeyboardInTransition,
setShowInputAccessoryView: defaultSetShowInputAccessoryView,
showInputAccessoryView: false,
lastKeyboardHeight: 0,
isInputAccessoryViewMode: defaultIsInputAccessoryViewMode,
inputAccessoryViewAnimatedHeight: defaultInputAccessoryViewAnimatedHeight,
isTransitioningFromCustomView: defaultIsTransitioningFromCustomView,
closeInputAccessoryView: defaultCloseInputAccessoryView,
scrollToEnd: defaultScrollToEnd,
isEmojiSearchFocused: false,
setIsEmojiSearchFocused: defaultSetIsEmojiSearchFocused,
cursorPositionRef: defaultCursorPositionRef,
registerCursorPosition: defaultRegisterCursorPosition,
preserveCursorPositionForEmojiPicker: defaultPreserveCursorPositionForEmojiPicker,
clearCursorPositionPreservation: defaultClearCursorPositionPreservation,
isInEmojiPickerTransition: defaultIsInEmojiPickerTransition,
getPreservedCursorPosition: defaultGetPreservedCursorPosition,
updateValue: defaultUpdateValue.current,
updateCursorPosition: defaultUpdateCursorPosition.current,
registerPostInputCallbacks: defaultRegisterPostInputCallbacks,
}), [
defaultKeyboardTranslateY,
defaultBottomInset,
defaultScrollOffset,
defaultKeyboardHeight,
defaultScrollPosition,
defaultOnScroll,
defaultBlurInput,
defaultFocusInput,
defaultBlurAndDismissKeyboard,
defaultIsKeyboardFullyOpen,
defaultIsKeyboardFullyClosed,
defaultIsKeyboardInTransition,
defaultSetShowInputAccessoryView,
defaultIsInputAccessoryViewMode,
defaultInputAccessoryViewAnimatedHeight,
defaultIsTransitioningFromCustomView,
defaultCloseInputAccessoryView,
defaultScrollToEnd,
defaultSetIsEmojiSearchFocused,
defaultRegisterCursorPosition,
defaultPreserveCursorPositionForEmojiPicker,
defaultClearCursorPositionPreservation,
defaultIsInEmojiPickerTransition,
defaultGetPreservedCursorPosition,
defaultUpdateValue,
defaultUpdateCursorPosition,
defaultRegisterPostInputCallbacks,
]);
// If context exists, return it; otherwise return fallback
return context || fallbackValue;
};