Fix Android keyboard gap using navigation events (#8565) (#8566)

(cherry picked from commit f2383f8bc5)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build 2025-02-06 12:24:20 +02:00 committed by GitHub
parent 84badb448a
commit 6f29392a2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 28 additions and 143 deletions

View file

@ -199,6 +199,8 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
CALL_HOST_CONTROLS,
]);
export const SCREENS_WITH_EXTRA_KEYBOARD = new Set<string>([CHANNEL, THREAD]);
export const NOT_READY = [
CREATE_TEAM,
];

View file

@ -1,98 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {renderHook} from '@testing-library/react-hooks';
import {Keyboard} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {useAndroidAdjustSoftKeyboard} from '.'; // Update with the correct path
jest.mock('react-native', () => ({
Keyboard: {
isVisible: jest.fn(),
dismiss: jest.fn(),
},
Platform: {
OS: 'android',
select: jest.fn((options) => options.android ?? options.default),
},
}));
jest.mock('react-native-navigation', () => {
const registerComponentListenerMock = jest.fn();
return {
Navigation: {
events: jest.fn(() => ({
registerComponentListener: registerComponentListenerMock,
})),
},
};
});
jest.mock('@mattermost/rnutils', () => ({
setSoftKeyboardToAdjustNothing: jest.fn(),
setSoftKeyboardToAdjustResize: jest.fn(),
}));
describe('useAndroidAdjustSoftKeyboard', () => {
let registerComponentListenerMock: jest.SpyInstance;
const unsubscribeMock = {remove: jest.fn()};
beforeEach(() => {
jest.useFakeTimers();
const events = Navigation.events();
registerComponentListenerMock = jest.spyOn(events, 'registerComponentListener');
registerComponentListenerMock.mockReturnValue(unsubscribeMock);
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it('should register component listener and handle lifecycle events', () => {
renderHook(() => useAndroidAdjustSoftKeyboard('Channel'));
expect(registerComponentListenerMock).toHaveBeenCalledWith(
expect.objectContaining({
componentDidAppear: expect.any(Function),
componentDidDisappear: expect.any(Function),
}),
'Channel',
);
const listener = registerComponentListenerMock.mock.calls[0][0];
jest.mocked(Keyboard.isVisible).mockReturnValue(true);
listener.componentDidAppear();
expect(Keyboard.dismiss).toHaveBeenCalled();
jest.runAllTimers();
expect(RNUtils.setSoftKeyboardToAdjustNothing).toHaveBeenCalled();
listener.componentDidDisappear();
expect(RNUtils.setSoftKeyboardToAdjustResize).toHaveBeenCalled();
});
it('should register component listener and handle lifecycle events when the screen is undefined', () => {
renderHook(() => useAndroidAdjustSoftKeyboard(undefined));
expect(registerComponentListenerMock).not.toHaveBeenCalledWith(
expect.objectContaining({
componentDidAppear: expect.any(Function),
componentDidDisappear: expect.any(Function),
}),
undefined,
);
});
it('should clean up on unmount', () => {
const {unmount} = renderHook(() => useAndroidAdjustSoftKeyboard('Channel'));
unmount();
expect(unsubscribeMock.remove).toHaveBeenCalled();
});
});

View file

@ -1,39 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {useEffect, useRef} from 'react';
import {Keyboard} from 'react-native';
import {Navigation, type EventSubscription} from 'react-native-navigation';
import type {AvailableScreens} from '@typings/screens/navigation';
export function useAndroidAdjustSoftKeyboard(screen?: AvailableScreens) {
const timeout = useRef<NodeJS.Timeout>();
useEffect(() => {
let unsubscribe: EventSubscription;
const listener = {
componentDidAppear: () => {
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
timeout.current = setTimeout(() => {
RNUtils.setSoftKeyboardToAdjustNothing();
}, 10);
},
componentDidDisappear: () => {
RNUtils.setSoftKeyboardToAdjustResize();
},
};
if (screen) {
unsubscribe = Navigation.events().registerComponentListener(listener, screen);
}
return () => {
clearTimeout(timeout.current);
unsubscribe?.remove();
};
}, []);
}

View file

@ -10,7 +10,6 @@ import FloatingCallContainer from '@calls/components/floating_call_container';
import FreezeScreen from '@components/freeze_screen';
import PostDraft from '@components/post_draft';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useAndroidAdjustSoftKeyboard} from '@hooks/android_adjust_soft_keyboard';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useChannelSwitch} from '@hooks/channel_switch';
import {useIsTablet} from '@hooks/device';
@ -79,7 +78,6 @@ const Channel = ({
}, [componentId]);
useAndroidHardwareBackHandler(componentId, handleBack);
useAndroidAdjustSoftKeyboard(componentId);
const marginTop = defaultHeight + (isTablet ? 0 : -insets.top);
useEffect(() => {

View file

@ -3,14 +3,15 @@
/* eslint-disable max-lines */
import RNUtils from '@mattermost/rnutils';
import merge from 'deepmerge';
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription} from 'react-native';
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription, type ComponentDidAppearEvent} from 'react-native-navigation';
import tinyColor from 'tinycolor2';
import CompassIcon from '@components/compass_icon';
import {Events, Screens, Launch} from '@constants';
import {NOT_READY} from '@constants/screens';
import {NOT_READY, SCREENS_WITH_EXTRA_KEYBOARD} from '@constants/screens';
import {getDefaultThemeByAppearance} from '@context/theme';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
@ -39,6 +40,8 @@ export function registerNavigationListeners() {
Navigation.events().registerScreenPoppedListener(onPoppedListener),
Navigation.events().registerCommandListener(onCommandListener),
Navigation.events().registerComponentWillAppearListener(onScreenWillAppear),
Navigation.events().registerComponentDidAppearListener(onScreenDidAppear),
Navigation.events().registerComponentDidDisappearListener(onScreenDidDisappear),
];
}
@ -76,12 +79,33 @@ function onPoppedListener({componentId}: ScreenPoppedEvent) {
NavigationStore.removeScreenFromStack(componentId as AvailableScreens);
}
function setAndroidSoftKeyboard(screen: AvailableScreens) {
if (Platform.OS !== 'android') {
return;
}
if (SCREENS_WITH_EXTRA_KEYBOARD.has(screen) || (isTablet() && screen === Screens.HOME)) {
RNUtils.setSoftKeyboardToAdjustNothing();
} else {
RNUtils.setSoftKeyboardToAdjustResize();
}
}
function onScreenWillAppear(event: ComponentWillAppearEvent) {
if (event.componentId === Screens.HOME) {
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
}
}
function onScreenDidAppear(event: ComponentDidAppearEvent) {
setAndroidSoftKeyboard(event.componentId as AvailableScreens);
}
function onScreenDidDisappear() {
const screen = NavigationStore.getVisibleScreen();
setAndroidSoftKeyboard(screen);
}
export const loginAnimationOptions = () => {
const theme = getThemeFromState();
return {

View file

@ -13,7 +13,6 @@ import PostDraft from '@components/post_draft';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useAndroidAdjustSoftKeyboard} from '@hooks/android_adjust_soft_keyboard';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useDidUpdate from '@hooks/did_update';
import {popTopScreen, setButtons} from '@screens/navigation';
@ -57,7 +56,6 @@ const Thread = ({
}, [componentId]);
useAndroidHardwareBackHandler(componentId, close);
useAndroidAdjustSoftKeyboard(componentId);
useEffect(() => {
if (isCRTEnabled && rootId) {