Added basic theming support (#131)
* Moved all view action types into a single file * Moved preferences into their own constants section * Added basic theming support
This commit is contained in:
parent
155a923cd0
commit
61c4d8b585
17 changed files with 341 additions and 36 deletions
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 '';
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={{flex: 1, backgroundColor: theme.centerChannelBg}}>
|
||||
<StatusBar barStyle='default'/>
|
||||
<Drawer
|
||||
open={this.state.leftSidebarOpen}
|
||||
type='displace'
|
||||
content={
|
||||
<ChannelSidebar
|
||||
currentTeam={this.props.currentTeam}
|
||||
channels={this.props.channels}
|
||||
currentTeam={currentTeam}
|
||||
channels={channels}
|
||||
/>
|
||||
}
|
||||
side='left'
|
||||
|
|
@ -87,22 +95,24 @@ export default class Channel extends React.Component {
|
|||
onCloseStart={this.closeRightSidebar}
|
||||
openDrawerOffset={0.2}
|
||||
>
|
||||
<View style={{backgroundColor: 'skyblue', flex: 1, flexDirection: 'row', justifyContent: 'space-between', marginTop: 20}}>
|
||||
<View style={{backgroundColor: theme.sidebarHeaderBg, flexDirection: 'row', justifyContent: 'space-between', marginTop: 20}}>
|
||||
<TouchableHighlight
|
||||
onPress={this.openLeftSidebar}
|
||||
style={{height: 50, width: 50}}
|
||||
>
|
||||
<Text>{'<'}</Text>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor}}>{'<'}</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
onPress={this.openRightSidebar}
|
||||
style={{height: 50, width: 50}}
|
||||
>
|
||||
<Text>{'>'}</Text>
|
||||
<Text style={{color: theme.sidebarHeaderTextColor}}>{'>'}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<Text>{this.props.currentTeam.id + ' - ' + this.props.currentTeam.name}</Text>
|
||||
<Text>{this.props.currentChannel.id + ' - ' + this.props.currentChannel.name}</Text>
|
||||
<View style={{flex: 1}}>
|
||||
<Text style={{color: theme.centerChannelColor}}>{currentTeam.id + ' - ' + currentTeam.name}</Text>
|
||||
<Text style={{color: theme.centerChannelColor}}>{currentChannel.id + ' - ' + currentChannel.name}</Text>
|
||||
</View>
|
||||
</Drawer>
|
||||
</Drawer>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"DefaultServerUrl": "http://localhost:8065",
|
||||
"MockFetchInTests": true,
|
||||
"DefaultLocale": "en"
|
||||
"DefaultLocale": "en",
|
||||
"DefaultTheme": "default"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
97
service/constants/themes.js
Normal file
97
service/constants/themes.js
Normal file
|
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
42
service/selectors/entities/preferences.js
Normal file
42
service/selectors/entities/preferences.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
139
test/service/selectors/preferences.test.js
Normal file
139
test/service/selectors/preferences.test.js
Normal file
|
|
@ -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'}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue