From 9d51a3b970c3d830c981ed96734b56986e2d5e44 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 5 Jan 2017 14:32:24 -0500 Subject: [PATCH] Added whitelabeling support using assets/override (#156) --- .babelrc | 6 +- .gitignore | 5 +- Makefile | 25 +++-- .../root_layout/root_layout_container.js | 4 +- app/reducers/views/i18n.js | 3 +- app/reducers/views/select_server.js | 2 +- app/scenes/login/login.js | 4 +- app/scenes/select_server/select_server.js | 6 +- app/scenes/select_team/select_team.js | 9 +- {config => assets/base}/config.json | 1 - {service => assets/base}/i18n/en.json | 0 {service => assets/base}/i18n/es.json | 0 {app => assets/base}/images/logo.png | Bin {app => assets/base}/images/menu.png | Bin {app => assets/base}/images/send.png | Bin assets/base/themes.json | 94 +++++++++++++++++ config/index.js | 12 --- scripts/make-dist-assets.js | 89 ++++++++++++++++ service/constants/index.js | 2 - service/constants/themes.js | 97 ------------------ service/i18n/index.js | 4 +- service/selectors/entities/preferences.js | 5 +- test/service/actions/general.test.js | 9 +- test/service/selectors/preferences.test.js | 5 +- test/test_helper.js | 4 +- 25 files changed, 240 insertions(+), 146 deletions(-) rename {config => assets/base}/config.json (78%) rename {service => assets/base}/i18n/en.json (100%) rename {service => assets/base}/i18n/es.json (100%) rename {app => assets/base}/images/logo.png (100%) rename {app => assets/base}/images/menu.png (100%) rename {app => assets/base}/images/send.png (100%) create mode 100644 assets/base/themes.json delete mode 100644 config/index.js create mode 100644 scripts/make-dist-assets.js delete mode 100644 service/constants/themes.js diff --git a/.babelrc b/.babelrc index 5cacc07d8..5fdee0245 100644 --- a/.babelrc +++ b/.babelrc @@ -2,8 +2,10 @@ "presets": [ "react-native" ], "plugins": [ ["module-resolver", { - "root": [".", "config", "test"], - "alias": {} + "root": ["."], + "alias": { + "assets": "./dist/assets" + } }] ] } diff --git a/.gitignore b/.gitignore index d3b3491b7..1e9932e64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +assets/override +dist + # OSX # .DS_Store @@ -49,5 +52,3 @@ Session.vim .netrwhist *~ tags - -config/config.secret.json diff --git a/Makefile b/Makefile index 8e71eed51..2ea7e74fb 100644 --- a/Makefile +++ b/Makefile @@ -12,15 +12,24 @@ touch $@ -config/config.secret.json: - @if ! [ $(shell test -f config/config.secret.json) ]; then \ - echo "Generating default config/config.secret.json"; \ - echo '{}' > "config/config.secret.json"; \ +BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*') +OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null) +dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS) + + mkdir -p dist + + @if [ -e dist/assets ] ; then \ + rm -rf dist/assets; \ fi + node scripts/make-dist-assets.js + +.PHONY: prepare +pre-run: .npminstall dist/assets + run: run-ios -run-ios: .npminstall config/config.secret.json +run-ios: pre-run @if ! [ $(shell command -v xcodebuild) ]; then \ echo "xcode is not installed"; \ exit 1; \ @@ -35,8 +44,7 @@ run-ios: .npminstall config/config.secret.json npm run run-ios open -a Simulator - -run-android: .npminstall config/config.secret.json +run-android: pre-run @if ! [ $(ANDROID_HOME) ]; then \ echo "ANDROID_HOME is not set"; \ exit 1; \ @@ -58,7 +66,7 @@ run-android: .npminstall config/config.secret.json npm run run-android -test: .npminstall config/config.secret.json +test: pre-run npm test check-style: .npminstall @@ -72,6 +80,7 @@ clean: npm cache clean rm -rf node_modules rm -f .npminstall + rm -rf dist post-install: ./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver diff --git a/app/layouts/root_layout/root_layout_container.js b/app/layouts/root_layout/root_layout_container.js index 7d1f98567..1bed3cbf6 100644 --- a/app/layouts/root_layout/root_layout_container.js +++ b/app/layouts/root_layout/root_layout_container.js @@ -3,9 +3,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import Config from 'config/index'; + +import Config from 'assets/config.json'; import {setAppState} from 'service/actions/general'; + import RootLayout from './root_layout'; function mapStateToProps(state, ownProps) { diff --git a/app/reducers/views/i18n.js b/app/reducers/views/i18n.js index da7628b29..96fb525fd 100644 --- a/app/reducers/views/i18n.js +++ b/app/reducers/views/i18n.js @@ -3,7 +3,8 @@ import {combineReducers} from 'redux'; -import Config from 'config/index'; +import Config from 'assets/config.json'; + import {UsersTypes} from 'service/constants'; function locale(state = Config.DefaultLocale, action) { diff --git a/app/reducers/views/select_server.js b/app/reducers/views/select_server.js index 5f5bd0ff5..04ad395f3 100644 --- a/app/reducers/views/select_server.js +++ b/app/reducers/views/select_server.js @@ -3,7 +3,7 @@ import {combineReducers} from 'redux'; -import Config from 'config/index'; +import Config from 'assets/config.json'; import {ViewTypes} from 'app/constants'; diff --git a/app/scenes/login/login.js b/app/scenes/login/login.js index 68ddac15a..a31ff0bc7 100644 --- a/app/scenes/login/login.js +++ b/app/scenes/login/login.js @@ -2,6 +2,7 @@ // See License.txt for license information. import React, {Component, PropTypes} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import {Text, TextInput, Image, KeyboardAvoidingView} from 'react-native'; import Button from 'app/components/button'; @@ -9,9 +10,8 @@ import FormattedText from 'app/components/formatted_text'; import ErrorText from 'app/components/error_text'; import Loading from 'app/components/loading'; import {GlobalStyles} from 'app/styles'; -import logo from 'app/images/logo.png'; -import {injectIntl, intlShape} from 'react-intl'; +import logo from 'assets/images/logo.png'; import {RequestStatus} from 'service/constants'; diff --git a/app/scenes/select_server/select_server.js b/app/scenes/select_server/select_server.js index 7761fc5c6..e3292695e 100644 --- a/app/scenes/select_server/select_server.js +++ b/app/scenes/select_server/select_server.js @@ -2,17 +2,17 @@ // See License.txt for license information. import React, {Component} from 'react'; +import {injectIntl, intlShape} from 'react-intl'; import {TextInput, Image, KeyboardAvoidingView} from 'react-native'; -import Client from 'service/client'; import Button from 'app/components/button'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; import {GlobalStyles} from 'app/styles'; -import logo from 'app/images/logo.png'; -import {injectIntl, intlShape} from 'react-intl'; +import logo from 'assets/images/logo.png'; +import Client from 'service/client'; import RequestStatus from 'service/constants/request_status'; class SelectServer extends Component { diff --git a/app/scenes/select_team/select_team.js b/app/scenes/select_team/select_team.js index 6c6d1a835..99372df3d 100644 --- a/app/scenes/select_team/select_team.js +++ b/app/scenes/select_team/select_team.js @@ -2,16 +2,17 @@ // See License.txt for license information. import React, {Component, PropTypes} from 'react'; - import {View, Image, Text} from 'react-native'; import Button from 'react-native-button'; -import Loading from 'app/components/loading'; import Icon from 'react-native-vector-icons/MaterialIcons'; import ErrorText from 'app/components/error_text'; -import {GlobalStyles} from 'app/styles'; -import logo from 'app/images/logo.png'; import FormattedText from 'app/components/formatted_text'; +import Loading from 'app/components/loading'; +import {GlobalStyles} from 'app/styles'; + +import logo from 'assets/images/logo.png'; + import {RequestStatus} from 'service/constants'; export default class SelectTeam extends Component { diff --git a/config/config.json b/assets/base/config.json similarity index 78% rename from config/config.json rename to assets/base/config.json index d65c581c8..8060edfae 100644 --- a/config/config.json +++ b/assets/base/config.json @@ -1,6 +1,5 @@ { "DefaultServerUrl": "http://localhost:8065", - "MockFetchInTests": true, "DefaultLocale": "en", "DefaultTheme": "default" } diff --git a/service/i18n/en.json b/assets/base/i18n/en.json similarity index 100% rename from service/i18n/en.json rename to assets/base/i18n/en.json diff --git a/service/i18n/es.json b/assets/base/i18n/es.json similarity index 100% rename from service/i18n/es.json rename to assets/base/i18n/es.json diff --git a/app/images/logo.png b/assets/base/images/logo.png similarity index 100% rename from app/images/logo.png rename to assets/base/images/logo.png diff --git a/app/images/menu.png b/assets/base/images/menu.png similarity index 100% rename from app/images/menu.png rename to assets/base/images/menu.png diff --git a/app/images/send.png b/assets/base/images/send.png similarity index 100% rename from app/images/send.png rename to assets/base/images/send.png diff --git a/assets/base/themes.json b/assets/base/themes.json new file mode 100644 index 000000000..02ce1d93a --- /dev/null +++ b/assets/base/themes.json @@ -0,0 +1,94 @@ +{ + "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/config/index.js b/config/index.js deleted file mode 100644 index c061f751b..000000000 --- a/config/index.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import BaseConfig from './config.json'; -import SecretConfig from './config.secret.json'; - -const Config = { - ...BaseConfig, - ...SecretConfig -}; - -export default Config; diff --git a/scripts/make-dist-assets.js b/scripts/make-dist-assets.js new file mode 100644 index 000000000..7dda77b5e --- /dev/null +++ b/scripts/make-dist-assets.js @@ -0,0 +1,89 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* eslint-disable no-console */ + +var fs = require('fs'); + +// Takes the files in rootA/path, overwrites or merges them with the corresponding fiel in rootB/path, and places the +// resulting file in dest/path. JSON files that exist in both places are shallowly (TODO maybe deeply) merged and all +// other types of files are overwritten. +function leftMergeDirs(rootA, rootB, dest, path) { + var pathA = rootA + path; + var pathB = rootB + path; + + try { + fs.mkdirSync(dest + path); + } catch (e) { + if (e.code !== 'EEXIST') { + console.error('Failed to create destination dir ' + dest + path); + console.log(e); + throw e; + } + } + + for (var file of fs.readdirSync(pathA)) { + var filePathA = pathA + file; + var filePathB = pathB + file; + + var stat; + try { + stat = fs.statSync(filePathA); + } catch (e) { + console.error('File ' + file + ' doesn\'t exist in ' + pathA); + throw e; + } + + if (stat.isDirectory()) { + leftMergeDirs(rootA, rootB, dest, path + file + '/'); + } else { + var fileA; + try { + fileA = fs.readFileSync(filePathA); + } catch (e) { + console.error('Failed to read ' + filePathA); + throw e; + } + + var outPath = dest + path + file; + var out; + try { + out = fs.createWriteStream(outPath); + } catch (e) { + console.error('Failed to open output file ' + dest + path + file); + throw e; + } + + var fileB = null; + try { + fileB = fs.readFileSync(filePathB); + } catch (e) { + // do nothing + } + + if (fileB) { + if (file.endsWith('.json')) { + // We need to merge these files + var objA = JSON.parse(fileA); + var objB = JSON.parse(fileB); + + console.log('Merging ' + filePathA + ' with ' + filePathB + ' into ' + outPath); + out.write(JSON.stringify(Object.assign({}, objA, objB))); + } else { + // Copy fileB instead of fileA + console.log('Copying ' + filePathB + ' from override to ' + outPath); + out.write(fileB); + } + } else { + // Copy fileA to the destination + console.log('Copying ' + filePathA + ' from base to ' + outPath); + out.write(fileA); + } + } + } +} + +// Assumes dist/assets exists and is empty +leftMergeDirs('assets/base/', 'assets/override/', 'dist/assets/', ''); + +/* eslint-enable no-console */ diff --git a/service/constants/index.js b/service/constants/index.js index 7385ca577..d83974f3d 100644 --- a/service/constants/index.js +++ b/service/constants/index.js @@ -9,7 +9,6 @@ import TeamsTypes from './teams'; import PostsTypes from './posts'; import PreferencesTypes from './preferences'; import RequestStatus from './request_status'; -import Themes from './themes'; import WebsocketEvents from './websocket'; const Preferences = { @@ -27,6 +26,5 @@ export { PreferencesTypes, Preferences, RequestStatus, - Themes, WebsocketEvents }; diff --git a/service/constants/themes.js b/service/constants/themes.js deleted file mode 100644 index e459ccdd5..000000000 --- a/service/constants/themes.js +++ /dev/null @@ -1,97 +0,0 @@ -// 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/i18n/index.js b/service/i18n/index.js index 0085db75f..87ff6a9f7 100644 --- a/service/i18n/index.js +++ b/service/i18n/index.js @@ -3,8 +3,8 @@ import 'intl'; -import en from './en.json'; -import es from './es.json'; +import en from 'assets/i18n/en.json'; +import es from 'assets/i18n/es.json'; const DEFAULT_LOCALE = 'en'; diff --git a/service/selectors/entities/preferences.js b/service/selectors/entities/preferences.js index d03ea77ca..e972ba61e 100644 --- a/service/selectors/entities/preferences.js +++ b/service/selectors/entities/preferences.js @@ -1,9 +1,10 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import Config from 'config'; +import Config from 'assets/config.json'; +import {Themes} from 'assets/themes.json'; -import {Preferences, Themes} from 'service/constants'; +import {Preferences} from 'service/constants'; export function getTheme(state) { const myPreferences = state.entities.preferences.myPreferences; diff --git a/test/service/actions/general.test.js b/test/service/actions/general.test.js index 7d584b5ef..7652ce78b 100644 --- a/test/service/actions/general.test.js +++ b/test/service/actions/general.test.js @@ -3,11 +3,14 @@ import assert from 'assert'; -import * as Actions from 'service/actions/general'; -import Config from 'config'; -import Client from 'service/client'; import configureStore from 'app/store'; + +import Config from 'assets/config.json'; + +import * as Actions from 'service/actions/general'; +import Client from 'service/client'; import {RequestStatus} from 'service/constants'; + import TestHelper from 'test/test_helper'; describe('Actions.General', () => { diff --git a/test/service/selectors/preferences.test.js b/test/service/selectors/preferences.test.js index bb926453b..5ed66c2cb 100644 --- a/test/service/selectors/preferences.test.js +++ b/test/service/selectors/preferences.test.js @@ -3,9 +3,10 @@ import assert from 'assert'; -import Config from 'config'; +import Config from 'assets/config.json'; +import Themes from 'assets/themes.json'; -import {Preferences, Themes} from 'service/constants'; +import {Preferences} from 'service/constants'; import {getTheme} from 'service/selectors/entities/preferences'; describe('Selectors.Preferences', () => { diff --git a/test/test_helper.js b/test/test_helper.js index 4b6f4369a..58afbb82d 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -2,8 +2,10 @@ // See License.txt for license information. import assert from 'assert'; + +import Config from 'assets/config.json'; + import Client from 'service/client/client'; -import Config from 'config'; const PASSWORD = 'password1';