diff --git a/app/actions/views/login.js b/app/actions/views/login.js
index 2e82b30bb..dcedf9e23 100644
--- a/app/actions/views/login.js
+++ b/app/actions/views/login.js
@@ -1,12 +1,12 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import {UsersViewTypes} from 'app/constants';
+import {ViewTypes} from 'app/constants';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
dispatch({
- type: UsersViewTypes.LOGIN_ID_CHANGED,
+ type: ViewTypes.LOGIN_ID_CHANGED,
loginId
}, getState);
};
@@ -15,7 +15,7 @@ export function handleLoginIdChanged(loginId) {
export function handlePasswordChanged(password) {
return async (dispatch, getState) => {
dispatch({
- type: UsersViewTypes.PASSWORD_CHANGED,
+ type: ViewTypes.PASSWORD_CHANGED,
password
}, getState);
};
diff --git a/app/actions/views/select_server.js b/app/actions/views/select_server.js
index 10e077a6f..667c18fe4 100644
--- a/app/actions/views/select_server.js
+++ b/app/actions/views/select_server.js
@@ -1,12 +1,12 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import {GeneralTypes} from 'service/constants';
+import {ViewTypes} from 'app/constants';
export function handleServerUrlChanged(serverUrl) {
return async (dispatch, getState) => {
dispatch({
- type: GeneralTypes.SERVER_URL_CHANGED,
+ type: ViewTypes.SERVER_URL_CHANGED,
serverUrl
}, getState);
};
diff --git a/app/constants/index.js b/app/constants/index.js
index 68624e6be..e42c42e3e 100644
--- a/app/constants/index.js
+++ b/app/constants/index.js
@@ -2,9 +2,9 @@
// See License.txt for license information.
import NavigationTypes from './navigation';
-import UsersViewTypes from './users';
+import ViewTypes from './view';
export {
NavigationTypes,
- UsersViewTypes
+ ViewTypes
};
diff --git a/app/constants/users.js b/app/constants/view.js
similarity index 69%
rename from app/constants/users.js
rename to app/constants/view.js
index ea86cf78a..b450fae19 100644
--- a/app/constants/users.js
+++ b/app/constants/view.js
@@ -3,9 +3,11 @@
import keymirror from 'keymirror';
-const UsersViewTypes = keymirror({
+const ViewTypes = keymirror({
+ SERVER_URL_CHANGED: null,
+
LOGIN_ID_CHANGED: null,
PASSWORD_CHANGED: null
});
-export default UsersViewTypes;
+export default ViewTypes;
diff --git a/app/reducers/views/login.js b/app/reducers/views/login.js
index f46a9aa0e..2287fb0d0 100644
--- a/app/reducers/views/login.js
+++ b/app/reducers/views/login.js
@@ -3,11 +3,11 @@
import {combineReducers} from 'redux';
import {UsersTypes} from 'service/constants';
-import {UsersViewTypes} from 'app/constants';
+import {ViewTypes} from 'app/constants';
function loginId(state = '', action) {
switch (action.type) {
- case UsersViewTypes.LOGIN_ID_CHANGED:
+ case ViewTypes.LOGIN_ID_CHANGED:
return action.loginId;
case UsersTypes.LOGOUT_SUCCESS:
return '';
@@ -18,7 +18,7 @@ function loginId(state = '', action) {
function password(state = '', action) {
switch (action.type) {
- case UsersViewTypes.PASSWORD_CHANGED:
+ case ViewTypes.PASSWORD_CHANGED:
return action.password;
case UsersTypes.LOGOUT_SUCCESS:
return '';
diff --git a/app/reducers/views/select_server.js b/app/reducers/views/select_server.js
index fc8fb2a8a..6c6b6af85 100644
--- a/app/reducers/views/select_server.js
+++ b/app/reducers/views/select_server.js
@@ -5,11 +5,11 @@ import {combineReducers} from 'redux';
import Config from 'config';
-import {GeneralTypes} from 'service/constants';
+import {ViewTypes} from 'app/constants';
function serverUrl(state = Config.DefaultServerUrl, action) {
switch (action.type) {
- case GeneralTypes.SERVER_URL_CHANGED:
+ case ViewTypes.SERVER_URL_CHANGED:
return action.serverUrl;
default:
diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js
index fbc0ebf00..d909f33a7 100644
--- a/app/scenes/channel/channel.js
+++ b/app/scenes/channel/channel.js
@@ -16,7 +16,8 @@ export default class Channel extends React.Component {
actions: React.PropTypes.object.isRequired,
currentTeam: React.PropTypes.object.isRequired,
currentChannel: React.PropTypes.object,
- channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+ channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ theme: React.PropTypes.object.isRequired
};
constructor(props) {
@@ -57,20 +58,27 @@ export default class Channel extends React.Component {
}
render() {
- if (!this.props.currentChannel) {
+ const {
+ currentChannel,
+ currentTeam,
+ channels,
+ theme
+ } = this.props;
+
+ if (!currentChannel) {
return ;
}
return (
-
+
}
side='left'
@@ -87,22 +95,24 @@ export default class Channel extends React.Component {
onCloseStart={this.closeRightSidebar}
openDrawerOffset={0.2}
>
-
+
- {'<'}
+ {'<'}
- {'>'}
+ {'>'}
- {this.props.currentTeam.id + ' - ' + this.props.currentTeam.name}
- {this.props.currentChannel.id + ' - ' + this.props.currentChannel.name}
+
+ {currentTeam.id + ' - ' + currentTeam.name}
+ {currentChannel.id + ' - ' + currentChannel.name}
+
diff --git a/app/scenes/channel/channel_container.js b/app/scenes/channel/channel_container.js
index e93d17634..20ca5f459 100644
--- a/app/scenes/channel/channel_container.js
+++ b/app/scenes/channel/channel_container.js
@@ -3,7 +3,10 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
+
import {fetchMyChannelsAndMembers} from 'service/actions/channels';
+import {getTheme} from 'service/selectors/entities/preferences';
+
import Channel from './channel.js';
function mapStateToProps(state, ownProps) {
@@ -32,7 +35,8 @@ function mapStateToProps(state, ownProps) {
...ownProps,
currentTeam,
currentChannel,
- channels
+ channels,
+ theme: getTheme(state)
};
}
diff --git a/config/config.json b/config/config.json
index 7c7a3e066..d65c581c8 100644
--- a/config/config.json
+++ b/config/config.json
@@ -1,5 +1,6 @@
{
"DefaultServerUrl": "http://localhost:8065",
"MockFetchInTests": true,
- "DefaultLocale": "en"
+ "DefaultLocale": "en",
+ "DefaultTheme": "default"
}
diff --git a/service/actions/channels.js b/service/actions/channels.js
index 433c6abe4..8af0cd089 100644
--- a/service/actions/channels.js
+++ b/service/actions/channels.js
@@ -1,7 +1,13 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import {Constants, ChannelTypes, PreferencesTypes, UsersTypes} from 'service/constants';
+import {
+ Constants,
+ ChannelTypes,
+ Preferences,
+ PreferencesTypes,
+ UsersTypes
+} from 'service/constants';
import {forceLogoutIfNecessary} from './helpers';
import {batchActions} from 'redux-batched-actions';
import Client from 'service/client';
@@ -90,7 +96,7 @@ export function createDirectChannel(userId, otherUserId) {
},
{
type: PreferencesTypes.RECEIVED_PREFERENCES,
- data: [{category: Constants.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'}]
+ data: [{category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'}]
},
{
type: ChannelTypes.CREATE_CHANNEL_SUCCESS
diff --git a/service/constants/constants.js b/service/constants/constants.js
index a117a6fa8..f0cd7ebdf 100644
--- a/service/constants/constants.js
+++ b/service/constants/constants.js
@@ -10,9 +10,7 @@ const Constants = {
TEAM_ADMIN_ROLE: 'team_admin',
CHANNEL_USER_ROLE: 'channel_user',
- CHANNEL_ADMIN_ROLE: 'channel_admin',
-
- CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
+ CHANNEL_ADMIN_ROLE: 'channel_admin'
};
export default Constants;
diff --git a/service/constants/general.js b/service/constants/general.js
index ab80d3a36..f25af4346 100644
--- a/service/constants/general.js
+++ b/service/constants/general.js
@@ -20,9 +20,7 @@ const GeneralTypes = keymirror({
LOG_CLIENT_ERROR_REQUEST: null,
LOG_CLIENT_ERROR_SUCCESS: null,
- LOG_CLIENT_ERROR_FAILURE: null,
-
- SERVER_URL_CHANGED: null
+ LOG_CLIENT_ERROR_FAILURE: null
});
export default GeneralTypes;
diff --git a/service/constants/index.js b/service/constants/index.js
index ae257a294..5fe222e42 100644
--- a/service/constants/index.js
+++ b/service/constants/index.js
@@ -9,6 +9,12 @@ import TeamsTypes from './teams';
import PostsTypes from './posts';
import PreferencesTypes from './preferences';
import RequestStatus from './request_status';
+import Themes from './themes';
+
+const Preferences = {
+ CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
+ CATEGORY_THEME: 'theme'
+};
export {
Constants,
@@ -18,5 +24,7 @@ export {
ChannelTypes,
PostsTypes,
PreferencesTypes,
- RequestStatus
+ Preferences,
+ RequestStatus,
+ Themes
};
diff --git a/service/constants/themes.js b/service/constants/themes.js
new file mode 100644
index 000000000..e459ccdd5
--- /dev/null
+++ b/service/constants/themes.js
@@ -0,0 +1,97 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default {
+ default: {
+ sidebarBg: '#2071a7',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#136197',
+ sidebarTextActiveBorder: '#7AB0D6',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#2f81b7',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
+ mentionBj: '#FBFBFB',
+ mentionColor: '#2071A7',
+ centerChannelBg: '#f2f4f8',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2f81b7',
+ buttonBg: '#1dacfc',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#fff2bb',
+ mentionHighlightLink: '#2f81b7',
+ codeTheme: 'github'
+ },
+ mattermost: {
+ sidebarBg: '#fafafa',
+ sidebarText: '#333333',
+ sidebarUnreadText: '#333333',
+ sidebarTextHoverBg: '#e6f2fa',
+ sidebarTextActiveBorder: '#378FD2',
+ sidebarTextActiveColor: '#111111',
+ sidebarHeaderBg: '#3481B9',
+ sidebarHeaderTextColor: '#ffffff',
+ onlineIndicator: '#7DBE00',
+ awayIndicator: '#DCBD4E',
+ mentionBj: '#2389d7',
+ mentionColor: '#ffffff',
+ centerChannelBg: '#ffffff',
+ centerChannelColor: '#333333',
+ newMessageSeparator: '#FF8800',
+ linkColor: '#2389d7',
+ buttonBg: '#23A2FF',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#fff2bb',
+ mentionHighlightLink: '#2f81b7',
+ codeTheme: 'github'
+ },
+ mattermostDark: {
+ sidebarBg: '#1B2C3E',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#4A5664',
+ sidebarTextActiveBorder: '#66B9A7',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1B2C3E',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#55C5B2',
+ awayIndicator: '#A9A14C',
+ mentionBj: '#B74A4A',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#2F3E4E',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#5de5da',
+ linkColor: '#A4FFEB',
+ buttonBg: '#4CBBA4',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#984063',
+ mentionHighlightLink: '#A4FFEB',
+ codeTheme: 'solarized-dark'
+ },
+ windows10: {
+ sidebarBg: '#171717',
+ sidebarText: '#fff',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#302e30',
+ sidebarTextActiveBorder: '#196CAF',
+ sidebarTextActiveColor: '#FFFFFF',
+ sidebarHeaderBg: '#1f1f1f',
+ sidebarHeaderTextColor: '#FFFFFF',
+ onlineIndicator: '#0177e7',
+ awayIndicator: '#A9A14C',
+ mentionBj: '#0177e7',
+ mentionColor: '#FFFFFF',
+ centerChannelBg: '#1F1F1F',
+ centerChannelColor: '#DDDDDD',
+ newMessageSeparator: '#CC992D',
+ linkColor: '#0D93FF',
+ buttonBg: '#0177e7',
+ buttonColor: '#FFFFFF',
+ mentionHighlightBg: '#784098',
+ mentionHighlightLink: '#A4FFEB',
+ codeTheme: 'monokai'
+ }
+};
diff --git a/service/reducers/entities/preferences.js b/service/reducers/entities/preferences.js
index 55940d820..22cf9b686 100644
--- a/service/reducers/entities/preferences.js
+++ b/service/reducers/entities/preferences.js
@@ -5,7 +5,7 @@ import {combineReducers} from 'redux';
import {PreferencesTypes, UsersTypes} from 'service/constants';
function getKey(preference) {
- return `${preference.category}-${preference.name}`;
+ return `${preference.category}--${preference.name}`;
}
function myPreferences(state = {}, action) {
diff --git a/service/selectors/entities/preferences.js b/service/selectors/entities/preferences.js
new file mode 100644
index 000000000..d03ea77ca
--- /dev/null
+++ b/service/selectors/entities/preferences.js
@@ -0,0 +1,42 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Config from 'config';
+
+import {Preferences, Themes} from 'service/constants';
+
+export function getTheme(state) {
+ const myPreferences = state.entities.preferences.myPreferences;
+ const currentTeamId = state.entities.teams.currentId;
+
+ // Prefer the user's current team-specific theme over the user's current global theme over the default theme
+ let themePreference;
+ if (currentTeamId) {
+ themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--${currentTeamId}`];
+ }
+
+ if (!themePreference) {
+ themePreference = myPreferences[`${Preferences.CATEGORY_THEME}--`];
+ }
+
+ let theme;
+ if (themePreference) {
+ theme = themePreference.value;
+ } else {
+ theme = Config.DefaultTheme;
+ }
+
+ if (typeof theme === 'string') {
+ try {
+ // A custom theme will be a JSON-serialized object stored in a preference
+ theme = JSON.parse(theme);
+ } catch (e) {
+ // But if it's not a JSON object, it must be the name of a default theme
+ theme = Themes[theme];
+ }
+ }
+
+ // At this point, the theme should be a plain object
+
+ return theme;
+}
diff --git a/test/service/selectors/preferences.test.js b/test/service/selectors/preferences.test.js
new file mode 100644
index 000000000..6c7e2ef69
--- /dev/null
+++ b/test/service/selectors/preferences.test.js
@@ -0,0 +1,139 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import assert from 'assert';
+
+import Config from 'config';
+
+import {Preferences, Themes} from 'service/constants';
+import {getTheme} from 'service/selectors/entities/preferences';
+
+describe('Selectors.Preferences', () => {
+ it.only('getTheme', () => {
+ it('should return default theme', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: ''
+ },
+ preferences: {
+ myPreferences: {}
+ }
+ }
+ }),
+ typeof Config.DefaultTheme === 'string' ? Themes[Config.DefaultTheme] : Config.DefaultTheme
+ );
+ });
+
+ it('should return global theme by name', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: ''
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: 'mattermost'}
+ }
+ }
+ }
+ }),
+ Themes.mattermost
+ );
+ });
+
+ it('should return global custom theme', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: ''
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: '{"sidebarBg": "#ff0000"}'}
+ }
+ }
+ }
+ }),
+ {sidebarBg: '#ff0000'}
+ );
+ });
+
+ it('should return global theme by name when on team', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: '1234'
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: 'mattermost'}
+ }
+ }
+ }
+ }),
+ Themes.mattermost
+ );
+ });
+
+ it('should return global custom theme when on team', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: '1234'
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: '{"sidebarBg": "#ff0000"}'}
+ }
+ }
+ }
+ }),
+ {sidebarBg: '#ff0000'}
+ );
+ });
+
+ it('should return team-specific theme by name when on team', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: '1234'
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: 'mattermost'},
+ [`${Preferences.CATEGORY_THEME}--1234`]: {value: 'mattermostDark'}
+ }
+ }
+ }
+ }),
+ Themes.mattermostDark
+ );
+ });
+
+ it('should return team-specific custom theme when on team', () => {
+ assert.deepEqual(
+ getTheme({
+ entities: {
+ teams: {
+ currentId: '1234'
+ },
+ preferences: {
+ myPreferences: {
+ [`${Preferences.CATEGORY_THEME}--`]: {value: '{"sidebarBg": "#ff0000"}'},
+ [`${Preferences.CATEGORY_THEME}--1234`]: {value: '{"sidebarBg": "#00ff00"}'}
+ }
+ }
+ }
+ }),
+ {sidebarBg: '#00ff00'}
+ );
+ });
+ });
+});