diff --git a/app/actions/navigation.js b/app/actions/navigation.js index f1e6852bb..94ab126c7 100644 --- a/app/actions/navigation.js +++ b/app/actions/navigation.js @@ -133,7 +133,7 @@ export function resetToTeams(name, title, passProps = {}, options = {}) { export function goToScreen(name, title, passProps = {}, options = {}) { return (dispatch, getState) => { const state = getState(); - const componentId = EphemeralStore.getTopComponentId(); + const componentId = EphemeralStore.getNavigationTopComponentId(); const theme = getTheme(state); const defaultOptions = { layout: { @@ -168,7 +168,7 @@ export function goToScreen(name, title, passProps = {}, options = {}) { export function popTopScreen() { return () => { - const componentId = EphemeralStore.getTopComponentId(); + const componentId = EphemeralStore.getNavigationTopComponentId(); Navigation.pop(componentId); }; @@ -176,7 +176,7 @@ export function popTopScreen() { export function popToRoot() { return () => { - const componentId = EphemeralStore.getTopComponentId(); + const componentId = EphemeralStore.getNavigationTopComponentId(); Navigation.popToRoot(componentId).catch(() => { // RNN returns a promise rejection if there are no screens @@ -286,7 +286,7 @@ export function showSearchModal(initialValue = '') { export function dismissModal(options = {}) { return () => { - const componentId = EphemeralStore.getTopComponentId(); + const componentId = EphemeralStore.getNavigationTopComponentId(); Navigation.dismissModal(componentId, options); }; @@ -304,7 +304,7 @@ export function dismissAllModals(options = {}) { export function peek(name, passProps = {}, options = {}) { return () => { - const componentId = EphemeralStore.getTopComponentId(); + const componentId = EphemeralStore.getNavigationTopComponentId(); const defaultOptions = { preview: { commit: false, @@ -359,3 +359,25 @@ export function dismissOverlay(componentId) { }); }; } + +export function applyTheme() { + return (dispatch, getState) => { + const theme = getTheme(getState()); + + EphemeralStore.getNavigationComponentIds().forEach((componentId) => { + Navigation.mergeOptions(componentId, { + topBar: { + backButton: { + color: theme.sidebarHeaderTextColor, + }, + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + }, + }, + }); + }); + }; +} diff --git a/app/mattermost.js b/app/mattermost.js index f9d160281..67636f3a7 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -87,11 +87,8 @@ const launchAppAndAuthenticateIfNeeded = async () => { Navigation.events().registerAppLaunchedListener(() => { init(); - // Keep track of the latest componentId to appear and disappear + // Keep track of the latest componentId to appear Navigation.events().registerComponentDidAppearListener(({componentId}) => { - EphemeralStore.addComponentIdToStack(componentId); - }); - Navigation.events().registerComponentDidDisappearListener(({componentId}) => { - EphemeralStore.removeComponentIdFromStack(componentId); + EphemeralStore.addNavigationComponentId(componentId); }); }); diff --git a/app/screens/theme/index.js b/app/screens/theme/index.js index 3ed397309..13b93c3bf 100644 --- a/app/screens/theme/index.js +++ b/app/screens/theme/index.js @@ -3,12 +3,15 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; + import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {savePreferences} from 'mattermost-redux/actions/preferences'; + import {getAllowedThemes, getCustomTheme} from 'app/selectors/theme'; import {isLandscape, isTablet} from 'app/selectors/device'; +import {applyTheme} from 'app/actions/navigation'; import Theme from './theme'; @@ -25,6 +28,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators({ savePreferences, + applyTheme, }, dispatch), }); diff --git a/app/screens/theme/theme.js b/app/screens/theme/theme.js index 0fed665d1..364cac066 100644 --- a/app/screens/theme/theme.js +++ b/app/screens/theme/theme.js @@ -26,6 +26,7 @@ export default class Theme extends React.PureComponent { static propTypes = { actions: PropTypes.shape({ savePreferences: PropTypes.func.isRequired, + applyTheme: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, allowedThemes: PropTypes.arrayOf(PropTypes.object), @@ -61,7 +62,12 @@ export default class Theme extends React.PureComponent { } setTheme = (key) => { - const {userId, teamId, actions: {savePreferences}, allowedThemes} = this.props; + const { + userId, + teamId, + allowedThemes, + actions: {savePreferences, applyTheme}, + } = this.props; const {customTheme} = this.state; const selectedTheme = allowedThemes.concat(customTheme).find((theme) => theme.key === key); @@ -71,6 +77,7 @@ export default class Theme extends React.PureComponent { name: teamId, value: JSON.stringify(selectedTheme), }]); + applyTheme(); } renderAllowedThemeTiles = () => { diff --git a/app/screens/theme/theme.test.js b/app/screens/theme/theme.test.js index 486b751fe..0c33caa99 100644 --- a/app/screens/theme/theme.test.js +++ b/app/screens/theme/theme.test.js @@ -3,15 +3,29 @@ import React from 'react'; import {shallow} from 'enzyme'; +import {Navigation} from 'react-native-navigation'; -import ThemeTile from './theme_tile'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import Preferences from 'mattermost-redux/constants/preferences'; +import {applyTheme} from 'app/actions/navigation'; +import EphemeralStore from 'app/store/ephemeral_store'; + import Theme from './theme'; +import ThemeTile from './theme_tile'; jest.mock('react-intl'); +jest.mock('react-native-navigation', () => ({ + Navigation: { + mergeOptions: jest.fn(), + }, +})); + +const mockStore = configureMockStore([thunk]); + const allowedThemes = [ { type: 'Mattermost', @@ -127,6 +141,7 @@ describe('Theme', () => { const baseProps = { actions: { savePreferences: jest.fn(), + applyTheme: jest.fn(), }, allowedThemes, isLandscape: false, @@ -144,4 +159,57 @@ describe('Theme', () => { expect(wrapper.getElement()).toMatchSnapshot(); expect(wrapper.find(ThemeTile)).toHaveLength(4); }); + + test('should apply new theme to all navigation components that have appeared', () => { + const componentIds = ['component-1', 'component-2', 'component-3']; + componentIds.forEach((componentId) => { + EphemeralStore.addNavigationComponentId(componentId); + }); + + const store = mockStore({ + entities: { + preferences: { + myPreferences: { + theme: {}, + }, + }, + teams: { + currentTeamId: 'current-team-id', + }, + general: { + config: {}, + }, + }, + }); + baseProps.actions.applyTheme.mockImplementation(() => { + store.dispatch(applyTheme()); + }); + + const wrapper = shallow( + , + ); + + const theme = allowedThemes[0]; + wrapper.instance().setTheme(theme.key); + expect(baseProps.actions.applyTheme).toHaveBeenCalledTimes(1); + + const options = { + topBar: { + backButton: { + color: theme.sidebarHeaderTextColor, + }, + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + }, + }, + }; + expect(Navigation.mergeOptions.mock.calls).toEqual([ + [componentIds[2], options], + [componentIds[1], options], + [componentIds[0], options], + ]); + }); }); diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js index f95803088..a0df7d125 100644 --- a/app/store/ephemeral_store.js +++ b/app/store/ephemeral_store.js @@ -6,19 +6,19 @@ class EphemeralStore { this.appStarted = false; this.appStartedFromPushNotification = false; this.deviceToken = null; - this.componentIdStack = []; + this.navigationComponentIdStack = []; } - getTopComponentId = () => this.componentIdStack[0]; + getNavigationTopComponentId = () => this.navigationComponentIdStack[0]; + getNavigationComponentIds = () => this.navigationComponentIdStack; - addComponentIdToStack = (componentId) => { - this.componentIdStack.unshift(componentId); - } + addNavigationComponentId = (componentId) => { + const index = this.navigationComponentIdStack.indexOf(componentId); + if (index > 0) { + this.navigationComponentIdStack.slice(index, 1); + } - removeComponentIdFromStack = (componentId) => { - this.componentIdStack = this.componentIdStack.filter((id) => { - return id !== componentId; - }); + this.navigationComponentIdStack.unshift(componentId); } }