diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js
index 91a38d238..7270415e5 100644
--- a/app/components/safe_area_view/safe_area_view.ios.js
+++ b/app/components/safe_area_view/safe_area_view.ios.js
@@ -3,15 +3,17 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {Keyboard, NativeModules, View} from 'react-native';
+import {Dimensions, Keyboard, NativeModules, View} from 'react-native';
import SafeArea from 'react-native-safe-area';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
-import {DeviceTypes} from 'app/constants';
+import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
+import EphemeralStore from 'app/store/ephemeral_store';
const {StatusBarManager} = NativeModules;
+const {PORTRAIT, LANDSCAPE} = ViewTypes;
export default class SafeAreaIos extends PureComponent {
static propTypes = {
@@ -57,8 +59,12 @@ export default class SafeAreaIos extends PureComponent {
componentDidMount() {
this.mounted = true;
- SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
+ Dimensions.addEventListener('change', this.getSafeAreaInsets);
EventEmitter.on('update_safe_area_view', this.getSafeAreaInsets);
+ if (EphemeralStore.safeAreaInsets[PORTRAIT] === null || EphemeralStore.safeAreaInsets[LANDSCAPE] === null) {
+ SafeArea.addEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
+ }
+
this.keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
@@ -66,13 +72,39 @@ export default class SafeAreaIos extends PureComponent {
}
componentWillUnmount() {
- SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
+ Dimensions.removeEventListener('change', this.getSafeAreaInsets);
EventEmitter.off('update_safe_area_view', this.getSafeAreaInsets);
+ SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
this.mounted = false;
}
+ getSafeAreaInsets = async (dimensions) => {
+ this.getStatusBarHeight();
+
+ const safeAreaInsetsStored = EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null;
+ if ((DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) && !safeAreaInsetsStored) {
+ const window = dimensions?.window || Dimensions.get('window');
+ const landscape = window.width > window.length;
+ const {safeAreaInsets} = await SafeArea.getSafeAreaInsetsForRootView();
+ this.setSafeAreaInsets(safeAreaInsets, landscape);
+ }
+ }
+
+ setSafeAreaInsets = (safeAreaInsets, landscape) => {
+ const orientation = landscape ? LANDSCAPE : PORTRAIT;
+ if (EphemeralStore.safeAreaInsets[orientation] === null) {
+ EphemeralStore.safeAreaInsets[orientation] = safeAreaInsets;
+ }
+
+ if (this.mounted) {
+ this.setState({
+ safeAreaInsets: EphemeralStore.safeAreaInsets[orientation],
+ });
+ }
+ }
+
getStatusBarHeight = () => {
try {
StatusBarManager.getHeight(
@@ -87,26 +119,18 @@ export default class SafeAreaIos extends PureComponent {
}
};
- getSafeAreaInsets = () => {
- this.getStatusBarHeight();
+ onSafeAreaInsetsForRootViewChange = ({safeAreaInsets}) => {
+ if (EphemeralStore.safeAreaInsets[PORTRAIT] !== null && EphemeralStore.safeAreaInsets[LANDSCAPE] !== null) {
+ SafeArea.removeEventListener('safeAreaInsetsForRootViewDidChange', this.onSafeAreaInsetsForRootViewChange);
+ return;
+ }
if (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets) {
- SafeArea.getSafeAreaInsetsForRootView().then((result) => {
- const {safeAreaInsets} = result;
-
- if (this.mounted) {
- this.setState({safeAreaInsets});
- }
- });
- }
- };
-
- onSafeAreaInsetsForRootViewChange = (result) => {
- const {safeAreaInsets} = result;
-
- if (this.mounted && (DeviceTypes.IS_IPHONE_WITH_INSETS || mattermostManaged.hasSafeAreaInsets)) {
this.getStatusBarHeight();
- this.setState({safeAreaInsets});
+
+ const {width, height} = Dimensions.get('window');
+ const landscape = width > height;
+ this.setSafeAreaInsets(safeAreaInsets, landscape);
}
}
diff --git a/app/components/safe_area_view/safe_area_view.ios.test.js b/app/components/safe_area_view/safe_area_view.ios.test.js
index 85c64b572..45861f552 100644
--- a/app/components/safe_area_view/safe_area_view.ios.test.js
+++ b/app/components/safe_area_view/safe_area_view.ios.test.js
@@ -6,11 +6,14 @@ import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
-import {DeviceTypes} from 'app/constants';
+import {DeviceTypes, ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
+import EphemeralStore from 'app/store/ephemeral_store';
import SafeAreaIos from './safe_area_view.ios';
+const {PORTRAIT, LANDSCAPE} = ViewTypes;
+
describe('SafeAreaIos', () => {
const baseProps = {
children: [],
@@ -37,10 +40,44 @@ describe('SafeAreaIos', () => {
},
};
+ const PORTRAIT_INSETS = {
+ safeAreaInsets: {
+ top: 111,
+ left: 111,
+ bottom: 111,
+ right: 111,
+ },
+ };
+
+ const LANDSCAPE_INSETS = {
+ safeAreaInsets: {
+ top: 222,
+ left: 222,
+ bottom: 222,
+ right: 222,
+ },
+ };
+
+ const IGNORED_INSETS = {
+ safeAreaInsets: {
+ top: 333,
+ left: 333,
+ bottom: 333,
+ right: 333,
+ },
+ };
+
SafeArea.getSafeAreaInsetsForRootView = jest.fn().mockImplementation(() => {
return Promise.resolve(TEST_INSETS_1);
});
+ beforeEach(() => {
+ EphemeralStore.safeAreaInsets = {
+ [PORTRAIT]: null,
+ [LANDSCAPE]: null,
+ };
+ });
+
test('should match snapshot', () => {
const wrapper = shallow(
@@ -148,4 +185,144 @@ describe('SafeAreaIos', () => {
instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_2);
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
});
+
+ test('should set portrait safe area insets', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+
+ const landscape = false;
+ const instance = wrapper.instance();
+ instance.setSafeAreaInsets(PORTRAIT_INSETS.safeAreaInsets, landscape);
+ expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+ });
+
+ test('should set portrait safe area insets from EphemeralStore', () => {
+ const wrapper = shallow(
+
+ );
+
+ EphemeralStore.safeAreaInsets[PORTRAIT] = PORTRAIT_INSETS.safeAreaInsets;
+ expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
+
+ const landscape = false;
+ const instance = wrapper.instance();
+ instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, landscape);
+ expect(wrapper.state().safeAreaInsets).toEqual(PORTRAIT_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(PORTRAIT_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+ });
+
+ test('should set landscape safe area insets', () => {
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+
+ const landscape = true;
+ const instance = wrapper.instance();
+ instance.setSafeAreaInsets(LANDSCAPE_INSETS.safeAreaInsets, landscape);
+ expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ });
+
+ test('should set landscape safe area insets from EphemeralStore', () => {
+ const wrapper = shallow(
+
+ );
+
+ EphemeralStore.safeAreaInsets[LANDSCAPE] = LANDSCAPE_INSETS.safeAreaInsets;
+ expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+
+ const landscape = true;
+ const instance = wrapper.instance();
+ instance.setSafeAreaInsets(IGNORED_INSETS.safeAreaInsets, landscape);
+ expect(wrapper.state().safeAreaInsets).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(LANDSCAPE_INSETS.safeAreaInsets);
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ });
+
+ test('should add safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are not set', () => {
+ const addEventListener = jest.spyOn(SafeArea, 'addEventListener');
+
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+ let wrapper = shallow(
+
+ );
+ let instance = wrapper.instance();
+ expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
+ addEventListener.mockClear();
+
+ EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
+ wrapper = shallow(
+
+ );
+ instance = wrapper.instance();
+ expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
+ addEventListener.mockClear();
+
+ EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
+ EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
+ wrapper = shallow(
+
+ );
+ instance = wrapper.instance();
+ expect(addEventListener).not.toHaveBeenCalled();
+ });
+
+ test('should remove safeAreaInsetsForRootViewDidChange listener when EphemeralStore values are set', () => {
+ const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
+
+ const wrapper = shallow(
+
+ );
+ const instance = wrapper.instance();
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+
+ instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
+ expect(removeEventListener).not.toHaveBeenCalled();
+
+ EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
+ instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
+ expect(removeEventListener).not.toHaveBeenCalled();
+
+ EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
+ instance.onSafeAreaInsetsForRootViewChange(TEST_INSETS_1);
+ expect(removeEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
+ });
+
+ test('getSafeAreaInsets should set safe area insets when not already in ephemeral store', async () => {
+ const wrapper = shallow(
+
+ );
+ const instance = wrapper.instance();
+ const setSafeAreaInsets = jest.spyOn(instance, 'setSafeAreaInsets');
+
+ expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
+ expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
+ await instance.getSafeAreaInsets();
+ expect(setSafeAreaInsets).toHaveBeenCalled();
+ setSafeAreaInsets.mockClear();
+
+ EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
+ await instance.getSafeAreaInsets();
+ expect(setSafeAreaInsets).toHaveBeenCalled();
+ setSafeAreaInsets.mockClear();
+
+ EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
+ await instance.getSafeAreaInsets();
+ expect(setSafeAreaInsets).not.toHaveBeenCalled();
+ });
});
diff --git a/app/constants/view.js b/app/constants/view.js
index e661dfe01..06e92db5e 100644
--- a/app/constants/view.js
+++ b/app/constants/view.js
@@ -95,6 +95,9 @@ const ViewTypes = keyMirror({
SELECTED_ACTION_MENU: null,
SUBMIT_ATTACHMENT_MENU_ACTION: null,
SELECT_CHANNEL_WITH_MEMBER: null,
+
+ PORTRAIT: null,
+ LANDSCAPE: null,
});
export default {
diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js
index 574fb029f..3639ecdda 100644
--- a/app/store/ephemeral_store.js
+++ b/app/store/ephemeral_store.js
@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import {ViewTypes} from 'app/constants';
+
class EphemeralStore {
constructor() {
this.appStarted = false;
@@ -9,6 +11,10 @@ class EphemeralStore {
this.navigationComponentIdStack = [];
this.allNavigationComponentIds = [];
this.currentServerUrl = null;
+ this.safeAreaInsets = {
+ [ViewTypes.PORTRAIT]: null,
+ [ViewTypes.LANDSCAPE]: null,
+ };
}
getNavigationTopComponentId = () => this.navigationComponentIdStack[0];