From 6f29392a2eee49b3ee6ecc7d787b4ebf91f4bd4d Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Thu, 6 Feb 2025 12:24:20 +0200 Subject: [PATCH] Fix Android keyboard gap using navigation events (#8565) (#8566) (cherry picked from commit f2383f8bc5058b294d674e38b1560c5f4b332293) Co-authored-by: Elias Nahum --- app/constants/screens.ts | 2 + .../index.test.ts | 98 ------------------- .../android_adjust_soft_keyboard/index.ts | 39 -------- app/screens/channel/channel.tsx | 2 - app/screens/navigation.ts | 28 +++++- app/screens/thread/thread.tsx | 2 - 6 files changed, 28 insertions(+), 143 deletions(-) delete mode 100644 app/hooks/android_adjust_soft_keyboard/index.test.ts delete mode 100644 app/hooks/android_adjust_soft_keyboard/index.ts diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 23bc9ea89..beab61e38 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -199,6 +199,8 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set([ CALL_HOST_CONTROLS, ]); +export const SCREENS_WITH_EXTRA_KEYBOARD = new Set([CHANNEL, THREAD]); + export const NOT_READY = [ CREATE_TEAM, ]; diff --git a/app/hooks/android_adjust_soft_keyboard/index.test.ts b/app/hooks/android_adjust_soft_keyboard/index.test.ts deleted file mode 100644 index 2e2508872..000000000 --- a/app/hooks/android_adjust_soft_keyboard/index.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/app/hooks/android_adjust_soft_keyboard/index.ts b/app/hooks/android_adjust_soft_keyboard/index.ts deleted file mode 100644 index 930b0afa5..000000000 --- a/app/hooks/android_adjust_soft_keyboard/index.ts +++ /dev/null @@ -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(); - - 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(); - }; - }, []); -} diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index 762eb6a0a..f525511d3 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -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(() => { diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index f3a955232..95995ae08 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -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 { diff --git a/app/screens/thread/thread.tsx b/app/screens/thread/thread.tsx index daae8161d..f66a3d919 100644 --- a/app/screens/thread/thread.tsx +++ b/app/screens/thread/thread.tsx @@ -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) {