Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: messages content overlap when bottom sheet is shown #42143

Open
wants to merge 83 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
24a9fb3
fix: messages content overlap when bottom sheet is shown
kirillzyusko Mar 13, 2024
173a19a
chore: fixed after code review
kirillzyusko Mar 15, 2024
30d22dc
fix: remove resetWorklet since it's not used right now
kirillzyusko Mar 18, 2024
a6eebcc
fix: frozen KeyboardAvoidingView when it was mounted with an opened k…
kirillzyusko Mar 19, 2024
8b0b297
chore: update keyboard-controller
kirillzyusko Mar 21, 2024
7de9847
fix: composer under keyboard when you press Edit message
kirillzyusko Mar 21, 2024
d705206
chore: use latest RNKC version
kirillzyusko Mar 27, 2024
f0ed018
post rebase changes
kirillzyusko May 14, 2024
5fe5b34
feat: update keyboard-controller to avoid crashes
kirillzyusko May 16, 2024
acfbdcb
chore: apply prettier
kirillzyusko Jun 4, 2024
abd66f5
Merge branch 'main' of github.com:margelo/expensify-app-fork into fix…
hannojg Jun 7, 2024
41044a1
chore: keep important changes from experiments
kirillzyusko Jun 20, 2024
54dee53
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Jun 20, 2024
843bf25
fix: don't mount two providers
kirillzyusko Jun 20, 2024
7d2df0a
fix: + button transitions
kirillzyusko Jun 20, 2024
0d351d2
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Jul 12, 2024
995e8b2
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Jul 16, 2024
eda5d53
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Jul 16, 2024
c0f86dc
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Jul 19, 2024
5d3c6e7
use executeOnUIRuntimeSync
perunt Jul 19, 2024
67d5dca
reset worklet on UI thread
perunt Jul 25, 2024
cbeec87
fix jumping while KEYBOARD_OPEN
perunt Jul 26, 2024
812e9ca
update controller
perunt Jul 26, 2024
05677c9
update SafeAreaPaddings
perunt Jul 26, 2024
31a7462
bump keyboard-controller
perunt Jul 26, 2024
6b205cc
lint
perunt Jul 26, 2024
c30b0e2
Merge branch 'main' of https://github.com/margelo/expensify-app-fork …
perunt Jul 26, 2024
05980ca
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Jul 26, 2024
1b96231
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Aug 7, 2024
8a27af5
add more states and actions
perunt Aug 7, 2024
1123d52
attachment popover and emoji picker without keyboard
perunt Aug 8, 2024
3e7680e
EMOJI_PICKER_WITH_KEYBOARD_OPEN
perunt Aug 8, 2024
6281b7b
popover closing
perunt Aug 8, 2024
b5c697f
state machine
perunt Aug 8, 2024
c424717
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Aug 8, 2024
426a542
keyboard-controller bump
perunt Aug 23, 2024
8e072af
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Aug 26, 2024
4974d9b
Merge branch 'main'
perunt Sep 3, 2024
1b4f2ad
fix popover measure
perunt Sep 3, 2024
3b17fd7
clean patch after RN75 bump
perunt Sep 3, 2024
f57c4f8
bump react-native-keyboard-controller
perunt Sep 3, 2024
e042032
remove patch react-native-keyboard-controller
perunt Sep 3, 2024
c1fe30a
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Sep 9, 2024
c0c2e52
fix firebase traceMap error
perunt Sep 9, 2024
8803051
bump react-native-keyboard-controller
perunt Sep 11, 2024
808d75f
left only context menu transition: clean components
perunt Sep 11, 2024
91b94bf
left only context menu transition: clean actions
perunt Sep 11, 2024
2427bbe
left only context menu transition: the rest
perunt Sep 11, 2024
195d6be
adjust ActionSheetKeyboardSpace
perunt Sep 16, 2024
9e25d7f
clean STATE_MACHINE
perunt Sep 16, 2024
6012251
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Sep 16, 2024
2eb8595
lint
perunt Sep 16, 2024
bb8eccd
Merge branch 'main' of https://github.com/Expensify/App into fix/1063…
perunt Sep 18, 2024
12f3cb0
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Oct 28, 2024
c4cb4cb
fix: ci checks
kirillzyusko Oct 28, 2024
9d271ae
fix: don't mock keyboard-controller twice
kirillzyusko Oct 28, 2024
31ba00b
fix: button jumps in Display Name page
kirillzyusko Oct 28, 2024
491c481
fix: safe area glitch on workspace selection screen
kirillzyusko Oct 28, 2024
ec57d6f
fix: emoji picker transition
kirillzyusko Oct 28, 2024
b6073e4
fix: random jump when keyboard closes
kirillzyusko Oct 29, 2024
d186f7f
fix: eslint
kirillzyusko Oct 29, 2024
3ef967f
fix: bottom sheet avoidance when keyboard is hidden
kirillzyusko Oct 31, 2024
e6de40d
fix: random transitions when popover gets closed
kirillzyusko Oct 31, 2024
34c6f03
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 14, 2024
0a2b158
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 19, 2024
cb5d8fa
fix: typescript checks
kirillzyusko Nov 19, 2024
6f71dcf
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 20, 2024
d4dd638
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 22, 2024
f375a4f
fix: web project crashes
kirillzyusko Nov 22, 2024
4e7d931
fix: prettier
kirillzyusko Nov 22, 2024
b48a247
fix: long press of a video attachment does not push up the video message
kirillzyusko Nov 22, 2024
0819868
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 26, 2024
c572404
fix: wrong displayName for ActionSheetKeyboardSpace
kirillzyusko Nov 26, 2024
f93a720
fix: remove dead code
kirillzyusko Nov 26, 2024
a415b2b
fix: revern changes that produces issue on many screens
kirillzyusko Nov 26, 2024
638eb70
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 28, 2024
fd87b02
fix: comments suggestions
kirillzyusko Nov 28, 2024
52973fa
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 28, 2024
6140d8f
fix: migrate to new reanimated API
kirillzyusko Nov 28, 2024
4cb6b6f
fix: last .value usage
kirillzyusko Nov 28, 2024
f021413
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Nov 30, 2024
386b265
Merge branch 'main' into fix/10632-when-long-press-on-message-add-spa…
kirillzyusko Dec 11, 2024
0ca998b
fix: unit tests (missing reanimated getter in mocks)
kirillzyusko Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
diff --git a/node_modules/react-native-reanimated/src/mock.ts b/node_modules/react-native-reanimated/src/mock.ts
index 3d8e3f8..5eba613 100644
--- a/node_modules/react-native-reanimated/src/mock.ts
+++ b/node_modules/react-native-reanimated/src/mock.ts
@@ -87,7 +87,12 @@ const hook = {
useAnimatedReaction: NOOP,
useAnimatedRef: () => ({ current: null }),
useAnimatedScrollHandler: NOOP_FACTORY,
- useDerivedValue: <Value>(processor: () => Value) => ({ value: processor() }),
+ // https://github.com/software-mansion/react-native-reanimated/pull/6809
+ useDerivedValue: <Value>(processor: () => Value) => {
+ const result = processor();
+
+ return { value: result, get: () => result };
+ },
useAnimatedSensor: () => ({
sensor: {
value: {
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
import * as ActionSheetAwareScrollView from './components/ActionSheetAwareScrollView';
import ActiveElementRoleProvider from './components/ActiveElementRoleProvider';
import ActiveWorkspaceContextProvider from './components/ActiveWorkspaceProvider';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
Expand Down Expand Up @@ -88,6 +89,7 @@ function App({url}: AppProps) {
CustomStatusBarAndBackgroundContextProvider,
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
ActionSheetAwareScrollView.ActionSheetAwareScrollViewProvider,
ReportIDsContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import noop from 'lodash/noop';
import PropTypes from 'prop-types';
import type {PropsWithChildren} from 'react';
import React, {createContext, useMemo} from 'react';
import type {SharedValue} from 'react-native-reanimated';
import type {ActionWithPayload, State} from '@hooks/useWorkletStateMachine';
import useWorkletStateMachine from '@hooks/useWorkletStateMachine';

type MeasuredElements = {
fy?: number;
popoverHeight?: number;
height?: number;
composerHeight?: number;
};

type Context = {
currentActionSheetState: SharedValue<State<MeasuredElements>>;
transitionActionSheetState: (action: ActionWithPayload) => void;
transitionActionSheetStateWorklet: (action: ActionWithPayload) => void;
resetStateMachine: () => void;
};

const currentActionSheetStateValue = {
previous: {
state: 'idle',
payload: null,
},
current: {
state: 'idle',
payload: null,
},
};
const defaultValue: Context = {
currentActionSheetState: {
value: currentActionSheetStateValue,
addListener: noop,
removeListener: noop,
modify: noop,
get: () => currentActionSheetStateValue,
set: noop,
},
transitionActionSheetState: noop,
transitionActionSheetStateWorklet: noop,
resetStateMachine: noop,
};

const ActionSheetAwareScrollViewContext = createContext<Context>(defaultValue);

const Actions = {
OPEN_KEYBOARD: 'KEYBOARD_OPEN',
CLOSE_KEYBOARD: 'CLOSE_KEYBOARD',
OPEN_POPOVER: 'OPEN_POPOVER',
CLOSE_POPOVER: 'CLOSE_POPOVER',
MEASURE_POPOVER: 'MEASURE_POPOVER',
MEASURE_COMPOSER: 'MEASURE_COMPOSER',
POPOVER_ANY_ACTION: 'POPOVER_ANY_ACTION',
HIDE_WITHOUT_ANIMATION: 'HIDE_WITHOUT_ANIMATION',
END_TRANSITION: 'END_TRANSITION',
};

const States = {
IDLE: 'idle',
KEYBOARD_OPEN: 'keyboardOpen',
POPOVER_OPEN: 'popoverOpen',
POPOVER_CLOSED: 'popoverClosed',
KEYBOARD_POPOVER_CLOSED: 'keyboardPopoverClosed',
KEYBOARD_POPOVER_OPEN: 'keyboardPopoverOpen',
KEYBOARD_CLOSED_POPOVER: 'keyboardClosingPopover',
POPOVER_MEASURED: 'popoverMeasured',
MODAL_WITH_KEYBOARD_OPEN_DELETED: 'modalWithKeyboardOpenDeleted',
};

const STATE_MACHINE = {
[States.IDLE]: {
[Actions.OPEN_POPOVER]: States.POPOVER_OPEN,
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
[Actions.MEASURE_POPOVER]: States.IDLE,
[Actions.MEASURE_COMPOSER]: States.IDLE,
},
[States.POPOVER_OPEN]: {
[Actions.CLOSE_POPOVER]: States.POPOVER_CLOSED,
[Actions.MEASURE_POPOVER]: States.POPOVER_OPEN,
[Actions.MEASURE_COMPOSER]: States.POPOVER_OPEN,
[Actions.POPOVER_ANY_ACTION]: States.POPOVER_CLOSED,
[Actions.HIDE_WITHOUT_ANIMATION]: States.IDLE,
},
[States.POPOVER_CLOSED]: {
[Actions.END_TRANSITION]: States.IDLE,
},
[States.KEYBOARD_OPEN]: {
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
[Actions.OPEN_POPOVER]: States.KEYBOARD_POPOVER_OPEN,
[Actions.CLOSE_KEYBOARD]: States.IDLE,
[Actions.MEASURE_COMPOSER]: States.KEYBOARD_OPEN,
},
[States.KEYBOARD_POPOVER_OPEN]: {
[Actions.MEASURE_POPOVER]: States.KEYBOARD_POPOVER_OPEN,
[Actions.CLOSE_POPOVER]: States.KEYBOARD_CLOSED_POPOVER,
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
},
[States.KEYBOARD_POPOVER_CLOSED]: {
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
},
[States.KEYBOARD_CLOSED_POPOVER]: {
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
[Actions.END_TRANSITION]: States.KEYBOARD_OPEN,
},
};

function ActionSheetAwareScrollViewProvider(props: PropsWithChildren<unknown>) {
const {currentState, transition, transitionWorklet, reset} = useWorkletStateMachine<MeasuredElements>(STATE_MACHINE, {
previous: {
state: 'idle',
payload: null,
},
current: {
state: 'idle',
payload: null,
},
});

const value = useMemo(
() => ({
currentActionSheetState: currentState,
transitionActionSheetState: transition,
transitionActionSheetStateWorklet: transitionWorklet,
resetStateMachine: reset,
}),
[currentState, reset, transition, transitionWorklet],
);

return <ActionSheetAwareScrollViewContext.Provider value={value}>{props.children}</ActionSheetAwareScrollViewContext.Provider>;
}

ActionSheetAwareScrollViewProvider.propTypes = {
children: PropTypes.node.isRequired,
};

export {ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider, Actions, States};
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React, {useContext, useEffect} from 'react';
import type {ViewProps} from 'react-native';
import {useKeyboardHandler} from 'react-native-keyboard-controller';
import Reanimated, {useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, withTiming} from 'react-native-reanimated';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {Actions, ActionSheetAwareScrollViewContext, States} from './ActionSheetAwareScrollViewContext';

const KeyboardState = {
UNKNOWN: 0,
OPENING: 1,
OPEN: 2,
CLOSING: 3,
CLOSED: 4,
};

const SPRING_CONFIG = {
mass: 3,
stiffness: 1000,
damping: 500,
};

const useAnimatedKeyboard = () => {
const state = useSharedValue(KeyboardState.UNKNOWN);
const height = useSharedValue(0);
const lastHeight = useSharedValue(0);
const heightWhenOpened = useSharedValue(0);

useKeyboardHandler(
{
onStart: (e) => {
'worklet';

// Save the last keyboard height
if (e.height !== 0) {
heightWhenOpened.set(e.height);
height.set(0);
}
height.set(heightWhenOpened.get());
lastHeight.set(e.height);
state.set(e.height > 0 ? KeyboardState.OPENING : KeyboardState.CLOSING);
},
onMove: (e) => {
'worklet';

height.set(e.height);
},
onEnd: (e) => {
'worklet';

state.set(e.height > 0 ? KeyboardState.OPEN : KeyboardState.CLOSED);
height.set(e.height);
},
},
[],
);

return {state, height, heightWhenOpened};
};

const useSafeAreaPaddings = () => {
const StyleUtils = useStyleUtils();
const insets = useSafeAreaInsets();
const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined);

return {top: paddingTop, bottom: paddingBottom};
};

function ActionSheetKeyboardSpace(props: ViewProps) {
const styles = useThemeStyles();
const safeArea = useSafeAreaPaddings();
const keyboard = useAnimatedKeyboard();

// Similar to using `global` in worklet but it's just a local object
const syncLocalWorkletState = useSharedValue(KeyboardState.UNKNOWN);
const {windowHeight} = useWindowDimensions();
const {currentActionSheetState, transitionActionSheetStateWorklet: transition, resetStateMachine} = useContext(ActionSheetAwareScrollViewContext);

// Reset state machine when component unmounts
// eslint-disable-next-line arrow-body-style
useEffect(() => {
return () => resetStateMachine();
}, [resetStateMachine]);

useAnimatedReaction(
() => keyboard.state.get(),
(lastState) => {
if (lastState === syncLocalWorkletState.get()) {
return;
}
// eslint-disable-next-line react-compiler/react-compiler
syncLocalWorkletState.set(lastState);

if (lastState === KeyboardState.OPEN) {
transition({type: Actions.OPEN_KEYBOARD});
} else if (lastState === KeyboardState.CLOSED) {
transition({type: Actions.CLOSE_KEYBOARD});
}
},
[],
);

const translateY = useDerivedValue(() => {
const {current, previous} = currentActionSheetState.get();

// We don't need to run any additional logic. it will always return 0 for idle state
if (current.state === States.IDLE) {
return withSpring(0, SPRING_CONFIG);
}

const keyboardHeight = keyboard.height.get() === 0 ? 0 : keyboard.height.get() - safeArea.bottom;

// Sometimes we need to know the last keyboard height
const lastKeyboardHeight = keyboard.heightWhenOpened.get() - safeArea.bottom;
const {popoverHeight = 0, fy, height} = current.payload ?? {};
const invertedKeyboardHeight = keyboard.state.get() === KeyboardState.CLOSED ? lastKeyboardHeight : 0;
const elementOffset = fy !== undefined && height !== undefined && popoverHeight !== undefined ? fy + safeArea.top + height - (windowHeight - popoverHeight) : 0;

// when the state is not idle we know for sure we have the previous state
const previousPayload = previous.payload ?? {};
const previousElementOffset =
previousPayload.fy !== undefined && previousPayload.height !== undefined && previousPayload.popoverHeight !== undefined
? previousPayload.fy + safeArea.top + previousPayload.height - (windowHeight - previousPayload.popoverHeight)
: 0;

const isOpeningKeyboard = syncLocalWorkletState.get() === 1;
const isClosingKeyboard = syncLocalWorkletState.get() === 3;
const isClosedKeyboard = syncLocalWorkletState.get() === 4;

// Depending on the current and sometimes previous state we can return
kirillzyusko marked this conversation as resolved.
Show resolved Hide resolved
// either animation or just a value
switch (current.state) {
case States.KEYBOARD_OPEN: {
if (isClosedKeyboard || isOpeningKeyboard) {
return lastKeyboardHeight - keyboardHeight;
}
if (previous.state === States.KEYBOARD_CLOSED_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) {
return Math.max(keyboard.heightWhenOpened.get() - keyboard.height.get() - safeArea.bottom, 0) + Math.max(elementOffset, 0);
}
return withSpring(0, SPRING_CONFIG);
}

case States.POPOVER_CLOSED: {
return withSpring(0, SPRING_CONFIG, () => {
transition({
type: Actions.END_TRANSITION,
});
});
}

case States.POPOVER_OPEN: {
if (popoverHeight) {
if (previousElementOffset !== 0 || elementOffset > previousElementOffset) {
return withSpring(elementOffset < 0 ? 0 : elementOffset, SPRING_CONFIG);
}

return withSpring(Math.max(previousElementOffset, 0), SPRING_CONFIG);
}

return 0;
}

case States.KEYBOARD_POPOVER_OPEN: {
if (keyboard.state.get() === KeyboardState.OPEN) {
return withSpring(0, SPRING_CONFIG);
}

const nextOffset = elementOffset + lastKeyboardHeight;

if (keyboard.state.get() === KeyboardState.CLOSED && nextOffset > invertedKeyboardHeight) {
return withSpring(nextOffset < 0 ? 0 : nextOffset, SPRING_CONFIG);
}

if (elementOffset < 0) {
return isClosingKeyboard ? 0 : lastKeyboardHeight - keyboardHeight;
}

return lastKeyboardHeight;
}

case States.KEYBOARD_CLOSED_POPOVER: {
if (elementOffset < 0) {
transition({type: Actions.END_TRANSITION});

return 0;
}

if (keyboard.state.get() === KeyboardState.CLOSED) {
return elementOffset + lastKeyboardHeight;
}

if (keyboard.height.get() > 0) {
return keyboard.heightWhenOpened.get() - keyboard.height.get() + elementOffset;
}

return withTiming(elementOffset + lastKeyboardHeight, {
duration: 0,
});
}

default:
return 0;
}
}, []);

const animatedStyle = useAnimatedStyle(() => ({
paddingTop: translateY.get(),
}));

return (
<Reanimated.View
style={[styles.flex1, animatedStyle]}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

ActionSheetKeyboardSpace.displayName = 'ActionSheetKeyboardSpace';

export default ActionSheetKeyboardSpace;
Loading
Loading