From 762bbabfa08791753428c2b507bb57be41026631 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 5 Feb 2025 08:12:57 +0800 Subject: [PATCH] Fix android keyboard area when switching apps (#8554) --- .../index.test.ts | 98 +++++++++++++++++++ .../android_adjust_soft_keyboard/index.ts | 39 ++++++++ app/screens/channel/channel.tsx | 18 +--- app/screens/thread/thread.tsx | 18 +--- 4 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 app/hooks/android_adjust_soft_keyboard/index.test.ts create mode 100644 app/hooks/android_adjust_soft_keyboard/index.ts diff --git a/app/hooks/android_adjust_soft_keyboard/index.test.ts b/app/hooks/android_adjust_soft_keyboard/index.test.ts new file mode 100644 index 000000000..2e2508872 --- /dev/null +++ b/app/hooks/android_adjust_soft_keyboard/index.test.ts @@ -0,0 +1,98 @@ +// 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 new file mode 100644 index 000000000..930b0afa5 --- /dev/null +++ b/app/hooks/android_adjust_soft_keyboard/index.ts @@ -0,0 +1,39 @@ +// 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 4c4768c7c..762eb6a0a 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -1,10 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import RNUtils from '@mattermost/rnutils'; import React, {useCallback, useEffect, useState} from 'react'; import {type LayoutChangeEvent, StyleSheet, View} from 'react-native'; -import {Navigation} from 'react-native-navigation'; import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import {storeLastViewedChannelIdAndServer, removeLastViewedChannelIdAndServer} from '@actions/app/global'; @@ -12,6 +10,7 @@ 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'; @@ -80,20 +79,7 @@ const Channel = ({ }, [componentId]); useAndroidHardwareBackHandler(componentId, handleBack); - - useEffect(() => { - const listener = { - componentDidAppear: () => { - RNUtils.setSoftKeyboardToAdjustNothing(); - }, - componentDidDisappear: () => { - RNUtils.setSoftKeyboardToAdjustResize(); - }, - }; - const unsubscribe = Navigation.events().registerComponentListener(listener, componentId!); - - return () => unsubscribe.remove(); - }, []); + useAndroidAdjustSoftKeyboard(componentId); const marginTop = defaultHeight + (isTablet ? 0 : -insets.top); useEffect(() => { diff --git a/app/screens/thread/thread.tsx b/app/screens/thread/thread.tsx index 435940f38..daae8161d 100644 --- a/app/screens/thread/thread.tsx +++ b/app/screens/thread/thread.tsx @@ -1,11 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import RNUtils from '@mattermost/rnutils'; import {uniqueId} from 'lodash'; import React, {useCallback, useEffect, useState} from 'react'; import {type LayoutChangeEvent, StyleSheet, View} from 'react-native'; -import {Navigation} from 'react-native-navigation'; import {type Edge, SafeAreaView} from 'react-native-safe-area-context'; import {storeLastViewedThreadIdAndServer, removeLastViewedThreadIdAndServer} from '@actions/app/global'; @@ -15,6 +13,7 @@ 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'; @@ -58,20 +57,7 @@ const Thread = ({ }, [componentId]); useAndroidHardwareBackHandler(componentId, close); - - useEffect(() => { - const listener = { - componentDidAppear: () => { - RNUtils.setSoftKeyboardToAdjustNothing(); - }, - componentDidDisappear: () => { - RNUtils.setSoftKeyboardToAdjustResize(); - }, - }; - const unsubscribe = Navigation.events().registerComponentListener(listener, componentId!); - - return () => unsubscribe.remove(); - }, []); + useAndroidAdjustSoftKeyboard(componentId); useEffect(() => { if (isCRTEnabled && rootId) {