From 2941f5dde49d211b24f3d52d3a8f9e0362d0e1c2 Mon Sep 17 00:00:00 2001 From: Bettenbuk Zoltan Date: Tue, 22 Jan 2019 11:32:18 +0100 Subject: [PATCH] [RN] Add color scheme support - JS --- react/features/app/components/App.native.js | 14 ++ .../base/color-scheme/ColorSchemeRegistry.js | 160 ++++++++++++++++++ .../features/base/color-scheme/actionTypes.js | 11 ++ react/features/base/color-scheme/actions.js | 19 +++ .../base/color-scheme/defaultScheme.js | 6 + react/features/base/color-scheme/functions.js | 13 ++ react/features/base/color-scheme/index.js | 8 + .../features/base/color-scheme/middleware.js | 20 +++ react/features/base/color-scheme/reducer.js | 21 +++ .../styles/components/styles/ColorPalette.js | 2 +- react/features/base/styles/functions.js | 60 +++++++ 11 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 react/features/base/color-scheme/ColorSchemeRegistry.js create mode 100644 react/features/base/color-scheme/actionTypes.js create mode 100644 react/features/base/color-scheme/actions.js create mode 100644 react/features/base/color-scheme/defaultScheme.js create mode 100644 react/features/base/color-scheme/functions.js create mode 100644 react/features/base/color-scheme/index.js create mode 100644 react/features/base/color-scheme/middleware.js create mode 100644 react/features/base/color-scheme/reducer.js diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index 094adec7a..fd600d716 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -5,6 +5,7 @@ import { Linking } from 'react-native'; import '../../analytics'; import '../../authentication'; +import { setColorScheme } from '../../base/color-scheme'; import { DialogContainer } from '../../base/dialog'; import '../../base/jwt'; import { Platform } from '../../base/react'; @@ -35,6 +36,11 @@ const logger = require('jitsi-meet-logger').getLogger(__filename); */ type Props = AbstractAppProps & { + /** + * An object of colors that override the default colors of the app/sdk. + */ + colorScheme: Object, + /** * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar button * is rendered in the {@link Conference} view to afford entering @@ -56,6 +62,8 @@ type Props = AbstractAppProps & { * @extends AbstractApp */ export class App extends AbstractApp { + _init: Promise<*>; + /** * Initializes a new {@code App} instance. * @@ -86,6 +94,12 @@ export class App extends AbstractApp { componentDidMount() { super.componentDidMount(); + this._init.then(() => { + // We set the color scheme early enough so then we avoid any + // unnecessary re-renders. + this.state.store.dispatch(setColorScheme(this.props.colorScheme)); + }); + Linking.addEventListener('url', this._onLinkingURL); } diff --git a/react/features/base/color-scheme/ColorSchemeRegistry.js b/react/features/base/color-scheme/ColorSchemeRegistry.js new file mode 100644 index 000000000..5b7337e88 --- /dev/null +++ b/react/features/base/color-scheme/ColorSchemeRegistry.js @@ -0,0 +1,160 @@ +// @flow + +import { toState } from '../redux'; +import { StyleType } from '../styles'; + +import defaultScheme from './defaultScheme'; + +/** + * A registry class to register styles that need to be color-schemed. + * + * This class uses lazy initialization for scheme-ified style definitions on + * request. + */ +class ColorSchemeRegistry { + /** + * A map of already scheme-ified style definitions. + */ + _schemedStyles = new Map(); + + /** + * A map of registered style templates. + */ + _styleTemplates = new Map(); + + /** + * Clears the already scheme-ified style definitions. This is useful when + * the {@code SET_COLOR_SCHEME} action is dispatched (again). + * + * @returns {void} + */ + clear() { + this._schemedStyles.clear(); + } + + /** + * Retreives the color-scheme applied style definition of a component. + * + * @param {Object | Function} stateful - An object or function that can be + * resolved to Redux state using the {@code toState} function. + * @param {string} componentName - The name of the component whose style we + * want to retreive. + * @returns {StyleType} + */ + get(stateful: Object | Function, componentName: string): StyleType { + let schemedStyle = this._schemedStyles.get(componentName); + + if (!schemedStyle) { + schemedStyle + = this._applyColorScheme( + stateful, + componentName, + this._styleTemplates.get(componentName)); + this._schemedStyles.set(componentName, schemedStyle); + } + + return schemedStyle; + } + + /** + * Registers a style definition to the registry for color-scheming. + * + * NOTE: It's suggested to only use this registry on styles where color + * scheming is needed, otherwise just use a static style object as before. + * + * @param {string} componentName - The name of the component to register the + * style to (e.g. {@code 'Toolbox'}). + * @param {StyleType} style - The style definition to register. + * @returns {void} + */ + register(componentName: string, style: StyleType): void { + this._styleTemplates.set(componentName, style); + + // If this is a style overwrite, we need to delete the processed version + // of the style from the other map + this._schemedStyles.delete(componentName); + } + + /** + * Creates a color schemed style object applying the color scheme to every + * colors in the style object prepared in a special way. + * + * @param {Object | Function} stateful - An object or function that can be + * resolved to Redux state using the {@code toState} function. + * @param {string} componentName - The name of the component to apply the + * color scheme to. + * @param {StyleType} style - The style definition to apply the color scheme + * to. + * @returns {StyleType} + */ + _applyColorScheme( + stateful: Object | Function, + componentName: string, + style: StyleType): StyleType { + let schemedStyle; + + if (Array.isArray(style)) { + // The style is an array of styles, we apply the same transformation + // to each, recursively. + schemedStyle = []; + + for (const entry of style) { + schemedStyle.push(this._applyColorScheme( + stateful, componentName, entry)); + } + } else { + // The style is an object, we create a copy of it to avoid in-place + // modification. + schemedStyle = { + ...style + }; + + for (const [ + styleName, + styleValue + ] of Object.entries(schemedStyle)) { + if (typeof styleValue === 'object') { + // The value is another style object, we apply the same + // transformation recusively. + schemedStyle[styleName] + = this._applyColorScheme( + stateful, componentName, styleValue); + } else if (typeof styleValue === 'function') { + // The value is a function, which indicates that it's a + // dynamic, schemed color we need to resolve. + schemedStyle[styleName] + = this._getColor(stateful, componentName, styleValue()); + } + + } + } + + return schemedStyle; + } + + /** + * Function to get the color value for the provided identifier. + * + * @param {Object | Function} stateful - An object or function that can be + * resolved to Redux state using the {@code toState} function. + * @param {string} componentName - The name of the component to get the + * color value for. + * @param {string} colorDefinition - The string identifier of the color, + * e.g. {@code appBackground}. + * @returns {string} + */ + _getColor( + stateful: Object | Function, + componentName: string, + colorDefinition: string): string { + const colorScheme = toState(stateful)['features/base/color-scheme']; + + return { + ...defaultScheme[componentName], + ...colorScheme[componentName] + }[colorDefinition]; + } + +} + +export default new ColorSchemeRegistry(); diff --git a/react/features/base/color-scheme/actionTypes.js b/react/features/base/color-scheme/actionTypes.js new file mode 100644 index 000000000..b1bf03ebd --- /dev/null +++ b/react/features/base/color-scheme/actionTypes.js @@ -0,0 +1,11 @@ +// @flow + +/** + * Redux action to signal a color scheme change in the app/sdk. + * + * { + * type: SET_COLOR_SCHEME + * colorScheme: Object + * } + */ +export const SET_COLOR_SCHEME = Symbol('SET_COLOR_SCHEME'); diff --git a/react/features/base/color-scheme/actions.js b/react/features/base/color-scheme/actions.js new file mode 100644 index 000000000..7f96110ea --- /dev/null +++ b/react/features/base/color-scheme/actions.js @@ -0,0 +1,19 @@ +// @flow + +import { SET_COLOR_SCHEME } from './actionTypes'; + +/** + * Dispatches a Redux action to set the color scheme of the app/sdk. + * + * @param {Object} colorScheme - The color scheme to set. + * @returns {{ + * type: SET_COLOR_SCHEME, + * colorScheme: Object + * }} + */ +export function setColorScheme(colorScheme: Object): Object { + return { + type: SET_COLOR_SCHEME, + colorScheme + }; +} diff --git a/react/features/base/color-scheme/defaultScheme.js b/react/features/base/color-scheme/defaultScheme.js new file mode 100644 index 000000000..4e67811a8 --- /dev/null +++ b/react/features/base/color-scheme/defaultScheme.js @@ -0,0 +1,6 @@ +// @flow + +/** + * The default color scheme of the application. + */ +export default {}; diff --git a/react/features/base/color-scheme/functions.js b/react/features/base/color-scheme/functions.js new file mode 100644 index 000000000..fcf92b525 --- /dev/null +++ b/react/features/base/color-scheme/functions.js @@ -0,0 +1,13 @@ +// @flow + +/** + * A special function to be used in the {@code createColorSchemedStyle} call, + * that denotes that the color is a dynamic color. + * + * @param {string} colorDefinition - The definition of the color to mark to be + * resolved. + * @returns {Function} + */ +export function schemeColor(colorDefinition: string): Function { + return () => colorDefinition; +} diff --git a/react/features/base/color-scheme/index.js b/react/features/base/color-scheme/index.js new file mode 100644 index 000000000..fdc97c28a --- /dev/null +++ b/react/features/base/color-scheme/index.js @@ -0,0 +1,8 @@ +// @flow + +export * from './actions'; +export * from './actionTypes'; +export * from './functions'; +export { default as ColorSchemeRegistry } from './ColorSchemeRegistry'; + +import './reducer'; diff --git a/react/features/base/color-scheme/middleware.js b/react/features/base/color-scheme/middleware.js new file mode 100644 index 000000000..3ecad840c --- /dev/null +++ b/react/features/base/color-scheme/middleware.js @@ -0,0 +1,20 @@ +// @flow + +import { MiddlewareRegistry } from '../redux'; + +import { SET_COLOR_SCHEME } from './actionTypes'; +import ColorSchemeRegistry from './ColorSchemeRegistry'; + +/** + * The middleware of the feature {@code base/color-scheme}. + * + * @returns {Function} + */ +MiddlewareRegistry.register((/* store */) => next => action => { + switch (action.type) { + case SET_COLOR_SCHEME: + return ColorSchemeRegistry.clear(); + } + + return next(action); +}); diff --git a/react/features/base/color-scheme/reducer.js b/react/features/base/color-scheme/reducer.js new file mode 100644 index 000000000..7e0939bd0 --- /dev/null +++ b/react/features/base/color-scheme/reducer.js @@ -0,0 +1,21 @@ +// @flow + +import _ from 'lodash'; + +import { ReducerRegistry } from '../redux'; + +import { SET_COLOR_SCHEME } from './actionTypes'; + +/** + * The reducer of the feature {@code base/color-scheme}. + * + * @returns {Function} + */ +ReducerRegistry.register('features/base/color-scheme', (state = {}, action) => { + switch (action.type) { + case SET_COLOR_SCHEME: + return _.cloneDeep(action.colorScheme); + } + + return state; +}); diff --git a/react/features/base/styles/components/styles/ColorPalette.js b/react/features/base/styles/components/styles/ColorPalette.js index 558abe83d..1c450dde8 100644 --- a/react/features/base/styles/components/styles/ColorPalette.js +++ b/react/features/base/styles/components/styles/ColorPalette.js @@ -27,7 +27,7 @@ export const ColorPalette = { overflowMenuItemUnderlay: '#EEEEEE', red: '#D00000', transparent: 'rgba(0, 0, 0, 0)', - white: 'white', + white: '#FFFFFF', /** * These are colors from the atlaskit to be used on mobile, when needed. diff --git a/react/features/base/styles/functions.js b/react/features/base/styles/functions.js index c57d300a5..602c94ffd 100644 --- a/react/features/base/styles/functions.js +++ b/react/features/base/styles/functions.js @@ -6,6 +6,23 @@ import { ColorPalette } from './components'; declare type StyleSheet = Object; export type StyleType = StyleSheet | Array; +/** + * RegExp pattern for long HEX color format. + */ +const HEX_LONG_COLOR_FORMAT + = /^#([0-9A-F]{2,2})([0-9A-F]{2,2})([0-9A-F]{2,2})$/i; + +/** + * RegExp pattern for short HEX color format. + */ +const HEX_SHORT_COLOR_FORMAT + = /^#([0-9A-F]{1,1})([0-9A-F]{1,1})([0-9A-F]{1,1})$/i; + +/** + * RegExp pattern for RGB color format. + */ +const RGB_COLOR_FORMAT = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i; + /** * The list of the well-known style properties which may not be numbers on Web * but must be numbers on React Native. @@ -87,6 +104,49 @@ export function fixAndroidViewClipping(styles: T): T { return styles; } +/** + * Returns an rgba format of the provided color if it's in hex or rgb format. + * + * NOTE: The function will return the same color if it's not in one of those + * two formats (e.g. 'white'). + * + * @param {string} color - The string representation of the color in rgb or hex + * format. + * @param {number} alpha - The alpha value to apply. + * @returns {string} + */ +export function getRGBAFormat(color: string, alpha: number): string { + let match = color.match(HEX_LONG_COLOR_FORMAT); + + if (match) { + return `#${match[1]}${match[2]}${match[3]}${_getAlphaInHex(alpha)}`; + } + + match = color.match(HEX_SHORT_COLOR_FORMAT); + if (match) { + return `#${match[1]}${match[1]}${match[2]}${match[2]}${match[3]}${ + match[3]}${_getAlphaInHex(alpha)}`; + } + + match = color.match(RGB_COLOR_FORMAT); + if (match) { + return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${alpha})`; + } + + return color; +} + +/** + * Converts an [0..1] alpha value into HEX. + * + * @param {number} alpha - The alpha value to convert. + * @returns {string} + */ +function _getAlphaInHex(alpha: number): string { + return Number(Math.round(255 * alpha)).toString(16) + .padStart(2, '0'); +} + /** * Shims style properties to work correctly on native. Allows us to minimize the * number of style declarations that need to be set or overridden for specific