diff --git a/react/features/base/color-scheme/defaultScheme.js b/react/features/base/color-scheme/defaultScheme.js index 8eab4c5e7..8b8d3d4c5 100644 --- a/react/features/base/color-scheme/defaultScheme.js +++ b/react/features/base/color-scheme/defaultScheme.js @@ -17,6 +17,13 @@ export default { icon: ColorPalette.white, text: ColorPalette.white }, + 'Header': { + background: ColorPalette.blue, + icon: ColorPalette.white, + statusBar: ColorPalette.blueHighlight, + statusBarContent: ColorPalette.white, + text: ColorPalette.white + }, 'LargeVideo': { background: ColorPalette.black }, diff --git a/react/features/base/react/components/native/BackButton.js b/react/features/base/react/components/native/BackButton.js index 289f5119a..87c827153 100644 --- a/react/features/base/react/components/native/BackButton.js +++ b/react/features/base/react/components/native/BackButton.js @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import { TouchableOpacity } from 'react-native'; +import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../../color-scheme'; import { Icon } from '../../../font-icons'; -import styles from './styles'; - /** * The type of the React {@code Component} props of {@link BackButton} */ @@ -20,13 +20,18 @@ type Props = { /** * An external style object passed to the component. */ - style?: Object + style?: Object, + + /** + * The color schemed style of the Header component. + */ + _headerStyles: Object }; /** * A component rendering a back button. */ -export default class BackButton extends Component { +class BackButton extends Component { /** * Implements React's {@link Component#render()}, renders the button. * @@ -41,10 +46,26 @@ export default class BackButton extends Component { ); } } + +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _headerStyles: Object + * }} + */ +function _mapStateToProps(state) { + return { + _headerStyles: ColorSchemeRegistry.get(state, 'Header') + }; +} + +export default connect(_mapStateToProps)(BackButton); diff --git a/react/features/base/react/components/native/ForwardButton.js b/react/features/base/react/components/native/ForwardButton.js index 96c338f92..ab87f4e65 100644 --- a/react/features/base/react/components/native/ForwardButton.js +++ b/react/features/base/react/components/native/ForwardButton.js @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import { Text, TouchableOpacity } from 'react-native'; +import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../../color-scheme'; import { translate } from '../../../i18n'; -import styles from './styles'; - /** * The type of the React {@code Component} props of {@link ForwardButton} */ @@ -35,7 +35,12 @@ type Props = { /** * The function to be used to translate i18n labels. */ - t: Function + t: Function, + + /** + * The color schemed style of the Header component. + */ + _headerStyles: Object }; /** @@ -49,6 +54,8 @@ class ForwardButton extends Component { * @returns {ReactElement} */ render() { + const { _headerStyles } = this.props; + return ( { onPress = { this.props.onPress } > { this.props.t(this.props.labelKey) } @@ -67,4 +74,18 @@ class ForwardButton extends Component { } } -export default translate(ForwardButton); +/** + * Maps part of the Redux state to the props of the component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _headerStyles: Object + * }} + */ +function _mapStateToProps(state) { + return { + _headerStyles: ColorSchemeRegistry.get(state, 'Header') + }; +} + +export default translate(connect(_mapStateToProps)(ForwardButton)); diff --git a/react/features/base/react/components/native/Header.js b/react/features/base/react/components/native/Header.js index 5346db201..08c0dd3ad 100644 --- a/react/features/base/react/components/native/Header.js +++ b/react/features/base/react/components/native/Header.js @@ -2,14 +2,24 @@ import React, { Component, type Node } from 'react'; import { Platform, SafeAreaView, StatusBar, View } from 'react-native'; +import { connect } from 'react-redux'; -import styles, { HEADER_PADDING, STATUSBAR_COLOR } from './styles'; +import { ColorSchemeRegistry } from '../../../color-scheme'; +import { isDarkColor } from '../../../styles'; + +import { HEADER_PADDING } from './headerstyles'; /** * Compatibility header padding size for iOS 10 (and older) devices. */ const IOS10_PADDING = 20; +/** + * Constanst for the (currently) supported statusbar colors. + */ +const STATUSBAR_DARK = 'dark-content'; +const STATUSBAR_LIGHT = 'light-content'; + /** * The type of the React {@code Component} props of {@link Header} */ @@ -23,43 +33,18 @@ type Props = { /** * The component's external style */ - style: Object + style: Object, + + /** + * The color schemed style of the component. + */ + _styles: Object } /** * A generic screen header component. */ -export default class Header extends Component { - - /** - * The style of button-like React {@code Component}s rendered in - * {@code Header}. - * - * @returns {Object} - */ - static get buttonStyle(): Object { - return styles.headerButtonIcon; - } - - /** - * The style of a React {@code Component} rendering a {@code Header} as its - * child. - * - * @returns {Object} - */ - static get pageStyle(): Object { - return styles.page; - } - - /** - * The style of text rendered in {@code Header}. - * - * @returns {Object} - */ - static get textStyle(): Object { - return styles.headerText; - } - +class Header extends Component { /** * Initializes a new {@code Header} instance. * @@ -78,20 +63,22 @@ export default class Header extends Component { * @inheritdoc */ render() { + const { _styles } = this.props; + return ( { @@ -128,4 +115,54 @@ export default class Header extends Component { return null; } + + /** + * Calculates the color of the statusbar content (light or dark) based on + * certain criterias. + * + * @returns {string} + */ + _getStatusBarContentColor() { + const { _styles } = this.props; + const { statusBarContent } = _styles; + + if (statusBarContent) { + // We have the possibility to define the statusbar color in the + // color scheme feature, but since mobile devices (at the moment) + // only support two colors (light and dark) we need to normalize + // the value. + + if (isDarkColor(statusBarContent)) { + return STATUSBAR_DARK; + } + + return STATUSBAR_LIGHT; + } + + // The statusbar color is not defined, so we need to base our choice + // on the header colors + const { statusBar, screenHeader } = _styles; + + if (isDarkColor(statusBar || screenHeader.backgroundColor)) { + return STATUSBAR_LIGHT; + } + + return STATUSBAR_DARK; + } } + +/** + * Maps part of the Redux state to the props of the component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _styles: Object + * }} + */ +function _mapStateToProps(state) { + return { + _styles: ColorSchemeRegistry.get(state, 'Header') + }; +} + +export default connect(_mapStateToProps)(Header); diff --git a/react/features/base/react/components/native/HeaderLabel.js b/react/features/base/react/components/native/HeaderLabel.js index c7e28b316..cbd19b7ca 100644 --- a/react/features/base/react/components/native/HeaderLabel.js +++ b/react/features/base/react/components/native/HeaderLabel.js @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import { Text, View } from 'react-native'; +import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../../color-scheme'; import { translate } from '../../../i18n'; -import styles from './styles'; - /** * The type of the React {@code Component} props of {@link HeaderLabel} */ @@ -20,7 +20,12 @@ type Props = { /** * The i18n translate function. */ - t: Function + t: Function, + + /** + * The color schemed style of the Header component. + */ + _headerStyles: Object }; /** @@ -34,13 +39,15 @@ class HeaderLabel extends Component { * @returns {ReactElement} */ render() { + const { _headerStyles } = this.props; + return ( + style = { _headerStyles.headerTextWrapper }> { this.props.t(this.props.labelKey) } @@ -49,4 +56,18 @@ class HeaderLabel extends Component { } } -export default translate(HeaderLabel); +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _headerStyles: Object + * }} + */ +function _mapStateToProps(state) { + return { + _headerStyles: ColorSchemeRegistry.get(state, 'Header') + }; +} + +export default translate(connect(_mapStateToProps)(HeaderLabel)); diff --git a/react/features/base/react/components/native/headerstyles.js b/react/features/base/react/components/native/headerstyles.js new file mode 100644 index 000000000..268150256 --- /dev/null +++ b/react/features/base/react/components/native/headerstyles.js @@ -0,0 +1,87 @@ +// @flex + +import { StyleSheet } from 'react-native'; + +import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme'; +import { BoxModel } from '../../../styles'; + +const HEADER_HEIGHT = 48; + +export const HEADER_PADDING = BoxModel.padding / 2; + +ColorSchemeRegistry.register('Header', { + + /** + * Style of a disabled button in the header (e.g. Next). + */ + disabledButtonText: { + opacity: 0.6 + }, + + /** + * Platform specific header button (e.g. back, menu, etc). + */ + headerButtonIcon: { + alignSelf: 'center', + color: schemeColor('icon'), + fontSize: 22, + marginRight: 12, + padding: 8 + }, + + headerButtonText: { + color: schemeColor('text'), + fontSize: 20 + }, + + /** + * Style of the header overlay to cover the unsafe areas. + */ + headerOverlay: { + backgroundColor: schemeColor('background') + }, + + /** + * Generic style for a label placed in the header. + */ + headerText: { + color: schemeColor('text'), + fontSize: 18 + }, + + headerTextWrapper: { + alignItems: 'center', + justifyContent: 'center', + left: 0, + position: 'absolute', + right: 0 + }, + + /** + * The top-level element of a page. + */ + page: { + ...StyleSheet.absoluteFillObject, + alignItems: 'stretch', + flex: 1, + flexDirection: 'column', + overflow: 'hidden' + }, + + /** + * Base style of Header. + */ + screenHeader: { + alignItems: 'center', + backgroundColor: schemeColor('background'), + flexDirection: 'row', + height: HEADER_HEIGHT, + justifyContent: 'space-between', + paddingHorizontal: BoxModel.padding, + paddingVertical: HEADER_PADDING + }, + + statusBar: schemeColor('statusBar'), + + statusBarContent: schemeColor('statusBarContent') +}); diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js index a09846469..b67e527ca 100644 --- a/react/features/base/react/components/native/styles.js +++ b/react/features/base/react/components/native/styles.js @@ -5,88 +5,13 @@ import { StyleSheet } from 'react-native'; import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles'; const AVATAR_OPACITY = 0.4; -const HEADER_COLOR = ColorPalette.blue; - -const HEADER_HEIGHT = 48; const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; const SECONDARY_ACTION_BUTTON_SIZE = 30; export const AVATAR_SIZE = 65; -export const HEADER_PADDING = BoxModel.padding / 2; -export const STATUSBAR_COLOR = ColorPalette.blueHighlight; export const SIDEBAR_WIDTH = 250; export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; -const HEADER_STYLES = { - - disabledButtonText: { - opacity: 0.6 - }, - - /** - * Platform specific header button (e.g. back, menu, etc). - */ - headerButtonIcon: { - alignSelf: 'center', - color: ColorPalette.white, - fontSize: 22, - marginRight: 12, - padding: 8 - }, - - headerButtonText: { - color: ColorPalette.white, - fontSize: 20 - }, - - /** - * Style of the header overlay to cover the unsafe areas. - */ - headerOverlay: { - backgroundColor: HEADER_COLOR - }, - - /** - * Generic style for a label placed in the header. - */ - headerText: { - color: ColorPalette.white, - fontSize: 18 - }, - - headerTextWrapper: { - alignItems: 'center', - justifyContent: 'center', - left: 0, - position: 'absolute', - right: 0 - }, - - /** - * The top-level element of a page. - */ - page: { - ...StyleSheet.absoluteFillObject, - alignItems: 'stretch', - flex: 1, - flexDirection: 'column', - overflow: 'hidden' - }, - - /** - * Base style of Header. - */ - screenHeader: { - alignItems: 'center', - backgroundColor: HEADER_COLOR, - flexDirection: 'row', - height: HEADER_HEIGHT, - justifyContent: 'space-between', - paddingHorizontal: BoxModel.padding, - paddingVertical: HEADER_PADDING - } -}; - /** * Style classes of the PagedList-based components. */ @@ -355,7 +280,6 @@ export const TINTED_VIEW_DEFAULT = { * base/react. */ export default createStyleSheet({ - ...HEADER_STYLES, ...PAGED_LIST_STYLES, ...SECTION_LIST_STYLES, ...SIDEBAR_STYLES diff --git a/react/features/base/styles/functions.js b/react/features/base/styles/functions.js index 602c94ffd..90ba79f9e 100644 --- a/react/features/base/styles/functions.js +++ b/react/features/base/styles/functions.js @@ -23,6 +23,12 @@ const HEX_SHORT_COLOR_FORMAT */ const RGB_COLOR_FORMAT = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i; +/** + * RegExp pattern for RGBA color format. + */ +const RGBA_COLOR_FORMAT + = /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*([0-9.]+)\)$/i; + /** * The list of the well-known style properties which may not be numbers on Web * but must be numbers on React Native. @@ -136,6 +142,23 @@ export function getRGBAFormat(color: string, alpha: number): string { return color; } +/** + * Decides if a color is light or dark based on the ITU-R BT.709 and W3C + * recommendations. + * + * NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef. + * + * @param {string} color - The color in rgb, rgba or hex format. + * @returns {boolean} + */ +export function isDarkColor(color: string): boolean { + const rgb = _getRGBObjectFormat(color); + + return ((_getColorLuminance(rgb.r) * 0.2126) + + (_getColorLuminance(rgb.g) * 0.7152) + + (_getColorLuminance(rgb.b) * 0.0722)) <= 0.179; +} + /** * Converts an [0..1] alpha value into HEX. * @@ -147,6 +170,67 @@ function _getAlphaInHex(alpha: number): string { .padStart(2, '0'); } +/** + * Calculated the color luminance component for an individual color channel. + * + * NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef. + * + * @param {number} c - The color which we need the individual luminance + * for. + * @returns {number} + */ +function _getColorLuminance(c: number): number { + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); +} + +/** + * Parses a color string into an object containing the RGB values as numbers. + * + * NOTE: Object properties are not alpha-sorted for sanity. + * + * @param {string} color - The color to convert. + * @returns {{ + * r: number, + * g: number, + * b: number + * }} + */ +function _getRGBObjectFormat(color: string): {r: number, g: number, b: number} { + let match = color.match(HEX_LONG_COLOR_FORMAT); + + if (match) { + return { + r: parseInt(match[1], 16) / 255.0, + g: parseInt(match[2], 16) / 255.0, + b: parseInt(match[3], 16) / 255.0 + }; + } + + match = color.match(HEX_SHORT_COLOR_FORMAT); + if (match) { + return { + r: parseInt(`${match[1]}${match[1]}`, 16) / 255.0, + g: parseInt(`${match[2]}${match[2]}`, 16) / 255.0, + b: parseInt(`${match[3]}${match[3]}`, 16) / 255.0 + }; + } + + match = color.match(RGB_COLOR_FORMAT) || color.match(RGBA_COLOR_FORMAT); + if (match) { + return { + r: parseInt(match[1], 10) / 255.0, + g: parseInt(match[2], 10) / 255.0, + b: parseInt(match[3], 10) / 255.0 + }; + } + + return { + r: 0, + g: 0, + b: 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 diff --git a/react/features/settings/components/AbstractSettingsView.js b/react/features/settings/components/AbstractSettingsView.js index e7e060210..3bd0ef4bb 100644 --- a/react/features/settings/components/AbstractSettingsView.js +++ b/react/features/settings/components/AbstractSettingsView.js @@ -9,7 +9,7 @@ import { updateSettings } from '../../base/settings'; * The type of the React {@code Component} props of * {@link AbstractSettingsView}. */ -type Props = { +export type Props = { /** * The default URL for when there is no custom URL set in the settings. @@ -47,15 +47,15 @@ type Props = { * * @abstract */ -export class AbstractSettingsView extends Component { +export class AbstractSettingsView extends Component

{ /** * Initializes a new {@code AbstractSettingsView} instance. * - * @param {Props} props - The React {@code Component} props to initialize + * @param {P} props - The React {@code Component} props to initialize * the component. */ - constructor(props: Props) { + constructor(props: P) { super(props); // Bind event handlers so they are only bound once per instance. diff --git a/react/features/settings/components/native/SettingsView.js b/react/features/settings/components/native/SettingsView.js index 2cf45dfb4..6ec885242 100644 --- a/react/features/settings/components/native/SettingsView.js +++ b/react/features/settings/components/native/SettingsView.js @@ -11,12 +11,14 @@ import { } from 'react-native'; import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../../base/color-scheme'; import { translate } from '../../../base/i18n'; import { BackButton, Header, Modal } from '../../../base/react'; import { AbstractSettingsView, - _mapStateToProps + _mapStateToProps as _abstractMapStateToProps, + type Props as AbstractProps } from '../AbstractSettingsView'; import { setSettingsViewVisible } from '../../actions'; import FormRow from './FormRow'; @@ -25,12 +27,20 @@ import { normalizeUserInputURL } from '../../functions'; import styles from './styles'; import { HeaderLabel } from '../../../base/react/components/native'; +type Props = AbstractProps & { + + /** + * Color schemed style of the header component. + */ + _headerStyles: Object +} + /** * The native container rendering the app settings page. * * @extends AbstractSettingsView */ -class SettingsView extends AbstractSettingsView { +class SettingsView extends AbstractSettingsView { _urlField: Object; /** @@ -60,7 +70,7 @@ class SettingsView extends AbstractSettingsView { onRequestClose = { this._onRequestClose } presentationStyle = 'overFullScreen' visible = { this.props._visible }> - + { this._renderHeader() } { this._renderBody() } @@ -239,4 +249,19 @@ class SettingsView extends AbstractSettingsView { } } +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _headerStyles: Object + * }} + */ +function _mapStateToProps(state) { + return { + ..._abstractMapStateToProps(state), + _headerStyles: ColorSchemeRegistry.get(state, 'Header') + }; +} + export default translate(connect(_mapStateToProps)(SettingsView)); diff --git a/react/features/welcome/components/VideoSwitch.js b/react/features/welcome/components/VideoSwitch.js index 93e778151..8f68623a6 100644 --- a/react/features/welcome/components/VideoSwitch.js +++ b/react/features/welcome/components/VideoSwitch.js @@ -4,8 +4,9 @@ import React, { Component } from 'react'; import { Switch, TouchableWithoutFeedback, View } from 'react-native'; import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../base/color-scheme'; import { translate } from '../../base/i18n'; -import { Header, Text } from '../../base/react'; +import { Text } from '../../base/react'; import { updateSettings } from '../../base/settings'; import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles'; @@ -25,6 +26,11 @@ type Props = { */ t: Function, + /** + * Color schemed style of the header component. + */ + _headerStyles: Object, + /** * The current settings from redux. */ @@ -55,15 +61,14 @@ class VideoSwitch extends Component { * @inheritdoc */ render() { - const { t, _settings } = this.props; - const { textStyle } = Header; + const { t, _headerStyles, _settings } = this.props; return ( - + { t('welcomepage.audioVideoSwitch.video') } @@ -77,7 +82,7 @@ class VideoSwitch extends Component { - + { t('welcomepage.audioVideoSwitch.audio') } @@ -132,6 +137,7 @@ class VideoSwitch extends Component { */ export function _mapStateToProps(state: Object) { return { + _headerStyles: ColorSchemeRegistry.get(state, 'Header'), _settings: state['features/base/settings'] }; } diff --git a/react/features/welcome/components/WelcomePage.native.js b/react/features/welcome/components/WelcomePage.native.js index ae9b3f1cf..2cd9a3ca7 100644 --- a/react/features/welcome/components/WelcomePage.native.js +++ b/react/features/welcome/components/WelcomePage.native.js @@ -10,6 +10,7 @@ import { } from 'react-native'; import { connect } from 'react-redux'; +import { ColorSchemeRegistry } from '../../base/color-scheme'; import { translate } from '../../base/i18n'; import { Icon } from '../../base/font-icons'; import { MEDIA_TYPE } from '../../base/media'; @@ -21,7 +22,10 @@ import { } from '../../base/tracks'; import { SettingsView } from '../../settings'; -import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; +import { + AbstractWelcomePage, + _mapStateToProps as _abstractMapStateToProps +} from './AbstractWelcomePage'; import { setSideBarVisible } from '../actions'; import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay'; import styles, { PLACEHOLDER_TEXT_COLOR } from './styles'; @@ -90,18 +94,17 @@ class WelcomePage extends AbstractWelcomePage { * @returns {ReactElement} */ render() { - const { buttonStyle, pageStyle } = Header; const roomnameAccLabel = 'welcomepage.accessibilityLabel.roomname'; - const { t } = this.props; + const { _headerStyles, t } = this.props; return ( - +

+ style = { _headerStyles.headerButtonIcon } />
@@ -269,4 +272,19 @@ class WelcomePage extends AbstractWelcomePage { } } +/** + * Maps part of the Redux state to the props of this component. + * + * @param {Object} state - The Redux state. + * @returns {{ + * _headerStyles: Object + * }} + */ +function _mapStateToProps(state) { + return { + ..._abstractMapStateToProps(state), + _headerStyles: ColorSchemeRegistry.get(state, 'Header') + }; +} + export default translate(connect(_mapStateToProps)(WelcomePage));