diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index d55b77126df..fb5a9c7a650 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -299,30 +299,33 @@ export default class PostList extends PureComponent { scrollToBottom = () => { setTimeout(() => { - if (this.flatListRef && this.flatListRef.current) { + if (this.flatListRef.current) { this.flatListRef.current.scrollToOffset({offset: 0, animated: true}); } }, 250); }; + flatListScrollToIndex = (index) => { + this.flatListRef.current.scrollToIndex({ + animated: false, + index, + viewOffset: 0, + viewPosition: 1, // 0 is at bottom + }); + } + scrollToIndex = (index) => { - if (this.flatListRef?.current) { - this.animationFrameInitialIndex = requestAnimationFrame(() => { - this.flatListRef.current.scrollToIndex({ - animated: false, - index, - viewOffset: 0, - viewPosition: 1, // 0 is at bottom - }); - }); - } + this.animationFrameInitialIndex = requestAnimationFrame(() => { + if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) { + this.flatListScrollToIndex(index); + } + }); }; scrollToInitialIndexIfNeeded = (index, count = 0) => { - if (!this.hasDoneInitialScroll && this.flatListRef?.current) { - this.hasDoneInitialScroll = true; - + if (!this.hasDoneInitialScroll) { if (index > 0 && index <= this.getItemCount()) { + this.hasDoneInitialScroll = true; this.scrollToIndex(index); } else if (count < 3) { setTimeout(() => { diff --git a/app/components/post_list/post_list.test.js b/app/components/post_list/post_list.test.js index a4219bf89d3..fe7c684b7d3 100644 --- a/app/components/post_list/post_list.test.js +++ b/app/components/post_list/post_list.test.js @@ -55,4 +55,32 @@ describe('PostList', () => { expect(baseProps.actions.handleSelectChannelByName).toHaveBeenCalled(); expect(wrapper.getElement()).toMatchSnapshot(); }); + + test('should call flatListScrollToIndex only when ref is set and index is in range', () => { + jest.spyOn(global, 'requestAnimationFrame').mockImplementation((cb) => cb()); + + const instance = wrapper.instance(); + const flatListScrollToIndex = jest.spyOn(instance, 'flatListScrollToIndex'); + const indexInRange = baseProps.postIds.length; + const indexOutOfRange = [-1, indexInRange + 1]; + + instance.flatListRef = { + current: null, + }; + instance.scrollToIndex(indexInRange); + expect(flatListScrollToIndex).not.toHaveBeenCalled(); + + instance.flatListRef = { + current: { + scrollToIndex: jest.fn(), + }, + }; + for (const index of indexOutOfRange) { + instance.scrollToIndex(index); + expect(flatListScrollToIndex).not.toHaveBeenCalled(); + } + + instance.scrollToIndex(indexInRange); + expect(flatListScrollToIndex).toHaveBeenCalled(); + }); }); diff --git a/app/i18n/index.js b/app/i18n/index.js index 1fe2158798e..bafaf7151e8 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.js @@ -108,12 +108,8 @@ function loadTranslation(locale) { } } -let momentLocale = DEFAULT_LOCALE; - -function setMomentLocale(locale) { - if (momentLocale !== locale) { - momentLocale = moment.locale(locale); - } +export function resetMomentLocale() { + moment.locale(DEFAULT_LOCALE); } export function getTranslations(locale) { @@ -121,8 +117,6 @@ export function getTranslations(locale) { loadTranslation(locale); } - setMomentLocale(locale.toLowerCase()); - return TRANSLATIONS[locale] || TRANSLATIONS[DEFAULT_LOCALE]; } diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index eed01a277a8..9f477ec6ef0 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -18,7 +18,7 @@ import {selectDefaultChannel} from 'app/actions/views/channel'; import {showOverlay} from 'app/actions/navigation'; import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; import {NavigationTypes, ViewTypes} from 'app/constants'; -import {getTranslations} from 'app/i18n'; +import {getTranslations, resetMomentLocale} from 'app/i18n'; import mattermostManaged from 'app/mattermost_managed'; import PushNotifications from 'app/push_notifications'; import {getCurrentLocale} from 'app/selectors/i18n'; @@ -40,7 +40,6 @@ class GlobalEventHandler { EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged); EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged); EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel); - this.turnOnInAppNotificationHandling(); Dimensions.addEventListener('change', this.onOrientationChange); AppState.addEventListener('change', this.onAppStateChange); Linking.addEventListener('url', this.onDeepLink); @@ -77,6 +76,10 @@ class GlobalEventHandler { this.store = opts.store; this.launchApp = opts.launchApp; + // onAppStateChange may be called by the AppState listener before we + // configure the global event handler so we manually call it here + this.onAppStateChange('active'); + const window = Dimensions.get('window'); this.onOrientationChange({window}); @@ -113,12 +116,12 @@ class GlobalEventHandler { if (this.store) { this.store.dispatch(setAppState(isActive)); - } - if (isActive && emmProvider.previousAppState === 'background') { - this.appActive(); - } else if (isBackground) { - this.appInactive(); + if (isActive && (!emmProvider.enabled || emmProvider.previousAppState === 'background')) { + this.appActive(); + } else if (isBackground) { + this.appInactive(); + } } emmProvider.previousAppState = appState; @@ -142,6 +145,7 @@ class GlobalEventHandler { this.store.dispatch(setServerVersion('')); deleteFileCache(); removeAppCredentials(); + resetMomentLocale(); PushNotifications.clearNotifications(); diff --git a/app/init/global_event_handler.test.js b/app/init/global_event_handler.test.js index ec0fe40f0f4..19fcee89aca 100644 --- a/app/init/global_event_handler.test.js +++ b/app/init/global_event_handler.test.js @@ -6,6 +6,7 @@ import thunk from 'redux-thunk'; import intitialState from 'app/initial_state'; import PushNotification from 'app/push_notifications'; +import * as I18n from 'app/i18n'; import GlobalEventHandler from './global_event_handler'; @@ -15,6 +16,12 @@ jest.mock('app/init/credentials', () => ({ removeAppCredentials: jest.fn(), })); +jest.mock('app/utils/error_handling', () => ({ + default: { + initializeErrorHandling: jest.fn(), + }, +})); + jest.mock('react-native-notifications', () => ({ addEventListener: jest.fn(), cancelAllLocalNotifications: jest.fn(), @@ -29,10 +36,54 @@ GlobalEventHandler.store = store; // TODO: Add Android test as part of https://mattermost.atlassian.net/browse/MM-17110 describe('GlobalEventHandler', () => { - it('should clear notifications on logout', async () => { + it('should clear notifications and reset moment locale on logout', async () => { const clearNotifications = jest.spyOn(PushNotification, 'clearNotifications'); + const resetMomentLocale = jest.spyOn(I18n, 'resetMomentLocale'); await GlobalEventHandler.onLogout(); expect(clearNotifications).toHaveBeenCalled(); + expect(resetMomentLocale).toHaveBeenCalledWith(); + }); + + it('should call onAppStateChange after configuration', () => { + const onAppStateChange = jest.spyOn(GlobalEventHandler, 'onAppStateChange'); + + GlobalEventHandler.configure({store}); + expect(GlobalEventHandler.store).not.toBeNull(); + expect(onAppStateChange).toHaveBeenCalledWith('active'); + }); + + it('should handle onAppStateChange to active if the store set', () => { + const appActive = jest.spyOn(GlobalEventHandler, 'appActive'); + const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive'); + expect(GlobalEventHandler.store).not.toBeNull(); + + GlobalEventHandler.onAppStateChange('active'); + expect(appActive).toHaveBeenCalled(); + expect(appInactive).not.toHaveBeenCalled(); + }); + + it('should handle onAppStateChange to background if the store set', () => { + const appActive = jest.spyOn(GlobalEventHandler, 'appActive'); + const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive'); + expect(GlobalEventHandler.store).not.toBeNull(); + + GlobalEventHandler.onAppStateChange('background'); + expect(appActive).not.toHaveBeenCalled(); + expect(appInactive).toHaveBeenCalled(); + }); + + it('should not handle onAppStateChange if the store is not set', () => { + const appActive = jest.spyOn(GlobalEventHandler, 'appActive'); + const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive'); + GlobalEventHandler.store = null; + + GlobalEventHandler.onAppStateChange('active'); + expect(appActive).not.toHaveBeenCalled(); + expect(appInactive).not.toHaveBeenCalled(); + + GlobalEventHandler.onAppStateChange('background'); + expect(appActive).not.toHaveBeenCalled(); + expect(appInactive).not.toHaveBeenCalled(); }); }); diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 383850f6b48..6b81b67e905 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -56,6 +56,8 @@ NSAppleMusicUsageDescription Let Mattermost access your Media files + NSBluetoothAlwaysUsageDescription + Share post data across devices with Mattermost NSBluetoothPeripheralUsageDescription Share post data accross devices with Mattermost NSCalendarsUsageDescription diff --git a/test/setup.js b/test/setup.js index fd80f20bd75..ad0bd71a68a 100644 --- a/test/setup.js +++ b/test/setup.js @@ -43,6 +43,9 @@ jest.mock('NativeModules', () => { addEventListener: jest.fn(), getCurrentState: jest.fn().mockResolvedValue({isConnected: true}), }, + StatusBarManager: { + getHeight: jest.fn(), + }, }; }); jest.mock('NativeEventEmitter');