[RN] Implement Notifications on mobile

This commit is contained in:
Bettenbuk Zoltan 2018-06-14 11:14:32 +02:00 committed by Paweł Domas
parent 00f18e9369
commit ffd0827354
34 changed files with 1923 additions and 1329 deletions

View File

@ -27,6 +27,9 @@
.icon-arrow_back:before { .icon-arrow_back:before {
content: "\e5c4"; content: "\e5c4";
} }
.icon-close:before {
content: "\e5cd";
}
.icon-event_note:before { .icon-event_note:before {
content: "\e616"; content: "\e616";
} }

Binary file not shown.

View File

@ -15,6 +15,7 @@
<glyph unicode="&#xe409;" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" /> <glyph unicode="&#xe409;" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
<glyph unicode="&#xe425;" glyph-name="timer" d="M512 170c166 0 298 134 298 300s-132 298-298 298-298-132-298-298 132-300 298-300zM812 708c52-66 84-148 84-238 0-212-172-384-384-384s-384 172-384 384 172 384 384 384c90 0 174-34 240-86l60 62c22-18 42-38 60-60zM470 426v256h84v-256h-84zM640 982v-86h-256v86h256z" /> <glyph unicode="&#xe425;" glyph-name="timer" d="M512 170c166 0 298 134 298 300s-132 298-298 298-298-132-298-298 132-300 298-300zM812 708c52-66 84-148 84-238 0-212-172-384-384-384s-384 172-384 384 172 384 384 384c90 0 174-34 240-86l60 62c22-18 42-38 60-60zM470 426v256h84v-256h-84zM640 982v-86h-256v86h256z" />
<glyph unicode="&#xe5c4;" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" /> <glyph unicode="&#xe5c4;" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
<glyph unicode="&#xe5cd;" glyph-name="close" d="M810 750l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
<glyph unicode="&#xe5d2;" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" /> <glyph unicode="&#xe5d2;" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
<glyph unicode="&#xe5d4;" glyph-name="thumb-menu" d="M512 342c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 598c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 682c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" /> <glyph unicode="&#xe5d4;" glyph-name="thumb-menu" d="M512 342c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 598c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 682c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe603;" glyph-name="presentation" horiz-adv-x="1088" d="M952.495 1019.065h-818.689c-72.81 0-132.183-60.63-132.183-135.162v-750.719c0-74.473 59.372-135.101 132.183-135.101h818.686c72.936 0 132.314 60.625 132.314 135.101v750.722c0.003 74.532-59.378 135.159-132.311 135.159zM946.346 139.651h-806.14v737.822h806.015l0.126-737.822zM685.753 738.544h216.911v-566.758h-216.911v566.758zM428.672 610.002h216.911v-438.216h-216.911v438.216zM172.339 481.46h216.161v-309.677h-216.161v309.677z" /> <glyph unicode="&#xe603;" glyph-name="presentation" horiz-adv-x="1088" d="M952.495 1019.065h-818.689c-72.81 0-132.183-60.63-132.183-135.162v-750.719c0-74.473 59.372-135.101 132.183-135.101h818.686c72.936 0 132.314 60.625 132.314 135.101v750.722c0.003 74.532-59.378 135.159-132.311 135.159zM946.346 139.651h-806.14v737.822h806.015l0.126-737.822zM685.753 738.544h216.911v-566.758h-216.911v566.758zM428.672 610.002h216.911v-438.216h-216.911v438.216zM172.339 481.46h216.161v-309.677h-216.161v309.677z" />

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -24,5 +24,16 @@ export const ColorPalette = {
darkGrey: '#555555', darkGrey: '#555555',
green: '#40b183', green: '#40b183',
red: '#D00000', red: '#D00000',
white: 'white' white: 'white',
/**
* These are colors from the atlaskit to be used on mobile, when needed.
*
* FIXME: Maybe a better solution would be good, or a native packaging of
* the respective atlaskit components.
*/
G400: '#00875A', // Slime
N500: '#42526E', // McFanning
R400: '#DE350B', // Red dirt
Y200: '#FFC400' // Pub mix
}; };

View File

@ -17,6 +17,7 @@ import { createDesiredLocalTracks } from '../../base/tracks';
import { ConferenceNotification } from '../../calendar-sync'; import { ConferenceNotification } from '../../calendar-sync';
import { Filmstrip } from '../../filmstrip'; import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video'; import { LargeVideo } from '../../large-video';
import { NotificationsContainer } from '../../notifications';
import { setToolboxVisible, Toolbox } from '../../toolbox'; import { setToolboxVisible, Toolbox } from '../../toolbox';
import ConferenceIndicators from './ConferenceIndicators'; import ConferenceIndicators from './ConferenceIndicators';
@ -267,6 +268,8 @@ class Conference extends Component<Props> {
this._renderConferenceNotification() this._renderConferenceNotification()
} }
<NotificationsContainer />
{/* {/*
* The dialogs are in the topmost stacking layers. * The dialogs are in the topmost stacking layers.
*/ */

View File

@ -10,6 +10,8 @@ import {
makeAspectRatioAware makeAspectRatioAware
} from '../../../base/responsive-ui'; } from '../../../base/responsive-ui';
import { isFilmstripVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail'; import LocalThumbnail from './LocalThumbnail';
import styles from './styles'; import styles from './styles';
import Thumbnail from './Thumbnail'; import Thumbnail from './Thumbnail';
@ -188,7 +190,7 @@ class Filmstrip extends Component<Props> {
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
const participants = state['features/base/participants']; const participants = state['features/base/participants'];
const { enabled, visible } = state['features/filmstrip']; const { enabled } = state['features/filmstrip'];
return { return {
/** /**
@ -215,7 +217,7 @@ function _mapStateToProps(state) {
* @private * @private
* @type {boolean} * @type {boolean}
*/ */
_visible: visible && participants.length > 1 _visible: isFilmstripVisible(state)
}; };
} }

View File

@ -1,4 +1,5 @@
import { ColorPalette } from '../../base/styles'; import { ColorPalette } from '../../base/styles';
import { FILMSTRIP_SIZE } from '../constants';
/** /**
* Size for the Avatar. * Size for the Avatar.
@ -44,12 +45,15 @@ export default {
...filmstrip, ...filmstrip,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'flex-end',
height: 90 height: FILMSTRIP_SIZE
}, },
/** /**
* The style of the wide {@link Filmstrip} version which displays thumbnails * The style of the wide {@link Filmstrip} version which displays thumbnails
* in a column on the short size of the screen. * in a column on the short size of the screen.
*
* NOTE: width is calculated based on the children, but it should also align
* to {@code FILMSTRIP_SIZE}.
*/ */
filmstripWide: { filmstripWide: {
...filmstrip, ...filmstrip,

View File

@ -0,0 +1,6 @@
// @flow
/**
* The height of the filmstrip in narrow aspect ratio, or width in wide.
*/
export const FILMSTRIP_SIZE = 90;

View File

@ -0,0 +1,20 @@
// @flow
import { toState } from '../base/redux';
/**
* Returns true if the filmstrip on mobile is visible, false otherwise.
*
* NOTE: Filmstrip on mobile behaves differently to web, and is only visible
* when there are at least 2 participants.
*
* @param {Object | Function} stateful - The Object or Function that can be
* resolved to a Redux state object with the toState function.
* @returns {boolean}
*/
export function isFilmstripVisible(stateful: Object | Function) {
const state = toState(stateful);
const { length: participantCount } = state['features/base/participants'];
return state['features/filmstrip'].visible && participantCount > 1;
}

View File

@ -4,9 +4,25 @@ import {
getParticipantCount, getParticipantCount,
getPinnedParticipant getPinnedParticipant
} from '../base/participants'; } from '../base/participants';
import { toState } from '../base/redux';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/**
* Returns true if the filmstrip on mobile is visible, false otherwise.
*
* NOTE: Filmstrip on web behaves differently to mobile, much simpler, but so
* function lies here only for the sake of consistency and to avoid flow errors
* on import.
*
* @param {Object | Function} stateful - The Object or Function that can be
* resolved to a Redux state object with the toState function.
* @returns {boolean}
*/
export function isFilmstripVisible(stateful: Object | Function) {
return toState(stateful)['features/filmstrip'].visible;
}
/** /**
* Determines whether the remote video thumbnails should be displayed/visible in * Determines whether the remote video thumbnails should be displayed/visible in
* the filmstrip. * the filmstrip.

View File

@ -1,6 +1,7 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components'; export * from './components';
export * from './constants';
export * from './functions'; export * from './functions';
import './middleware'; import './middleware';

View File

@ -1,3 +1,13 @@
/**
* The type of (redux) action which signals that all the stored notifications
* need to be cleared.
*
* {
* type: CLEAR_NOTIFICATIONS
* }
*/
export const CLEAR_NOTIFICATIONS = Symbol('CLEAR_NOTIFICATIONS');
/** /**
* The type of (redux) action which signals that a specific notification should * The type of (redux) action which signals that a specific notification should
* not be displayed anymore. * not be displayed anymore.

View File

@ -1,4 +1,7 @@
// @flow
import { import {
CLEAR_NOTIFICATIONS,
HIDE_NOTIFICATION, HIDE_NOTIFICATION,
SET_NOTIFICATIONS_ENABLED, SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION SHOW_NOTIFICATION
@ -6,6 +9,19 @@ import {
import { NOTIFICATION_TYPE } from './constants'; import { NOTIFICATION_TYPE } from './constants';
/**
* Clears (removes) all the notifications.
*
* @returns {{
* type: CLEAR_NOTIFICATIONS
* }}
*/
export function clearNotifications() {
return {
type: CLEAR_NOTIFICATIONS
};
}
/** /**
* Removes the notification with the passed in id. * Removes the notification with the passed in id.
* *
@ -16,7 +32,7 @@ import { NOTIFICATION_TYPE } from './constants';
* uid: number * uid: number
* }} * }}
*/ */
export function hideNotification(uid) { export function hideNotification(uid: number) {
return { return {
type: HIDE_NOTIFICATION, type: HIDE_NOTIFICATION,
uid uid
@ -32,7 +48,7 @@ export function hideNotification(uid) {
* enabled: boolean * enabled: boolean
* }} * }}
*/ */
export function setNotificationsEnabled(enabled) { export function setNotificationsEnabled(enabled: boolean) {
return { return {
type: SET_NOTIFICATIONS_ENABLED, type: SET_NOTIFICATIONS_ENABLED,
enabled enabled
@ -45,7 +61,7 @@ export function setNotificationsEnabled(enabled) {
* @param {Object} props - The props needed to show the notification component. * @param {Object} props - The props needed to show the notification component.
* @returns {Object} * @returns {Object}
*/ */
export function showErrorNotification(props) { export function showErrorNotification(props: Object) {
return showNotification({ return showNotification({
...props, ...props,
appearance: NOTIFICATION_TYPE.ERROR appearance: NOTIFICATION_TYPE.ERROR
@ -65,7 +81,7 @@ export function showErrorNotification(props) {
* uid: number * uid: number
* }} * }}
*/ */
export function showNotification(props = {}, timeout) { export function showNotification(props: Object = {}, timeout: ?number) {
return { return {
type: SHOW_NOTIFICATION, type: SHOW_NOTIFICATION,
props, props,
@ -80,7 +96,7 @@ export function showNotification(props = {}, timeout) {
* @param {Object} props - The props needed to show the notification component. * @param {Object} props - The props needed to show the notification component.
* @returns {Object} * @returns {Object}
*/ */
export function showWarningNotification(props) { export function showWarningNotification(props: Object) {
return showNotification({ return showNotification({
...props, ...props,
appearance: NOTIFICATION_TYPE.WARNING appearance: NOTIFICATION_TYPE.WARNING

View File

@ -0,0 +1,147 @@
// @flow
import { Component } from 'react';
import { NOTIFICATION_TYPE } from '../constants';
export type Props = {
/**
* Display appearance for the component, passed directly to the
* notification.
*/
appearance: string,
/**
* The text to display in the body of the notification. If not passed
* in, the passed in descriptionKey will be used.
*/
defaultTitleKey: string,
/**
* A description string that can be used in addition to the prop
* descriptionKey.
*/
description: string,
/**
* The translation arguments that may be necessary for the description.
*/
descriptionArguments: Object,
/**
* The translation key to use as the body of the notification.
*/
descriptionKey: string,
/**
* Whether the support link should be hidden in the case of an error
* message.
*/
hideErrorSupportLink: boolean,
/**
* Whether or not the dismiss button should be displayed.
*/
isDismissAllowed: boolean,
/**
* Callback invoked when the user clicks to dismiss the notification.
*/
onDismissed: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* The text to display at the top of the notification. If not passed in,
* the passed in titleKey will be used.
*/
title: string,
/**
* The translation arguments that may be necessary for the title.
*/
titleArguments: Object,
/**
* The translation key to display as the title of the notification if
* no title is provided.
*/
titleKey: string,
/**
* The unique identifier for the notification.
*/
uid: number
};
/**
* Abstract class for {@code Notification} component.
*
* @extends Component
*/
export default class AbstractNotification<P: Props> extends Component<P> {
/**
* Default values for {@code Notification} component's properties.
*
* @static
*/
static defaultProps = {
appearance: NOTIFICATION_TYPE.NORMAL,
isDismissAllowed: true
};
/**
* Initializes a new {@code Notification} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
_getDescription: () => Array<string>
/**
* Returns the description array to be displayed.
*
* @protected
* @returns {Array<string>}
*/
_getDescription() {
const {
description,
descriptionArguments,
descriptionKey,
t
} = this.props;
const descriptionArray = [];
descriptionKey
&& descriptionArray.push(t(descriptionKey, descriptionArguments));
description && descriptionArray.push(description);
return descriptionArray;
}
_onDismissed: () => void;
/**
* Callback to dismiss the notification.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.onDismissed(this.props.uid);
}
}

View File

@ -0,0 +1,146 @@
// @flow
import { Component } from 'react';
import { getOverlayToRender } from '../../overlay';
import { hideNotification } from '../actions';
export type Props = {
/**
* The notifications to be displayed, with the first index being the
* notification at the top and the rest shown below it in order.
*/
_notifications: Array<Object>,
/**
* Invoked to update the redux store in order to remove notifications.
*/
dispatch: Function
};
/**
* Abstract class for {@code NotificationsContainer} component.
*/
export default class AbstractNotificationsContainer<P: Props>
extends Component<P> {
/**
* A timeout id returned by setTimeout.
*/
_notificationDismissTimeout: ?TimeoutID;
/**
* Initializes a new {@code AbstractNotificationsContainer} instance.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
/**
* The timeout set for automatically dismissing a displayed
* notification. This value is set on the instance and not state to
* avoid additional re-renders.
*
* @type {number|null}
*/
this._notificationDismissTimeout = null;
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Sets a timeout if the currently displayed notification has changed.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: P) {
const { _notifications } = this.props;
if (_notifications.length) {
const notification = _notifications[0];
const previousNotification
= prevProps._notifications.length
? prevProps._notifications[0]
: undefined;
if (notification !== previousNotification) {
this._clearNotificationDismissTimeout();
if (notification) {
const { timeout, uid } = notification;
this._notificationDismissTimeout = setTimeout(() => {
// Perform a no-op if a timeout is not specified.
if (Number.isInteger(timeout)) {
this._onDismissed(uid);
}
}, timeout);
}
}
} else if (this._notificationDismissTimeout) {
// Clear timeout when all notifications are cleared (e.g external
// call to clear them)
this._clearNotificationDismissTimeout();
}
}
/**
* Clear any dismissal timeout that is still active.
*
* @inheritdoc
* returns {void}
*/
componentWillUnmount() {
this._clearNotificationDismissTimeout();
}
_onDismissed: number => void;
/**
* Clears the running notification dismiss timeout, if any.
*
* @returns {void}
*/
_clearNotificationDismissTimeout() {
this._notificationDismissTimeout
&& clearTimeout(this._notificationDismissTimeout);
this._notificationDismissTimeout = null;
}
/**
* Emits an action to remove the notification from the redux store so it
* stops displaying.
*
* @param {number} uid - The id of the notification to be removed.
* @private
* @returns {void}
*/
_onDismissed(uid) {
this._clearNotificationDismissTimeout();
this.props.dispatch(hideNotification(uid));
}
}
/**
* Maps (parts of) the Redux state to the associated NotificationsContainer's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _notifications: Array
* }}
*/
export function _abstractMapStateToProps(state: Object) {
const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
const { enabled, notifications } = state['features/notifications'];
return {
_notifications: enabled && !isAnyOverlayVisible ? notifications : []
};
}

View File

@ -0,0 +1,109 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { Icon } from '../../base/font-icons';
import { translate } from '../../base/i18n';
import { NOTIFICATION_TYPE } from '../constants';
import AbstractNotification, {
type Props
} from './AbstractNotification';
import styles from './styles';
/**
* Implements a React {@link Component} to display a notification.
*
* @extends Component
*/
class Notification extends AbstractNotification<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
appearance,
isDismissAllowed,
t,
title,
titleArguments,
titleKey
} = this.props;
let notificationStyle;
switch (appearance) {
case NOTIFICATION_TYPE.ERROR:
notificationStyle = styles.notificationTypeError;
break;
case NOTIFICATION_TYPE.NORMAL:
notificationStyle = styles.notificationTypeNormal;
break;
case NOTIFICATION_TYPE.SUCCESS:
notificationStyle = styles.notificationTypeSuccess;
break;
case NOTIFICATION_TYPE.WARNING:
notificationStyle = styles.notificationTypeWarning;
break;
case NOTIFICATION_TYPE.INFO:
default:
notificationStyle = styles.notificationTypeInfo;
}
return (
<View
pointerEvents = 'box-none'
style = { [
styles.notification,
notificationStyle
] }>
<View style = { styles.contentColumn }>
<View
pointerEvents = 'box-none'
style = { styles.notificationTitle }>
<Text style = { styles.titleText }>
{
title || t(titleKey, titleArguments)
}
</Text>
</View>
<View
pointerEvents = 'box-none'
style = { styles.notificationContent }>
{
// eslint-disable-next-line no-extra-parens
this._getDescription().map((line, index) => (
<Text
key = { index }
style = { styles.contentText }>
{ line }
</Text>
))
}
</View>
</View>
{
isDismissAllowed
&& <View style = { styles.actionColumn }>
<TouchableOpacity onPress = { this._onDismissed }>
<Icon
name = { 'close' }
style = { styles.dismissIcon } />
</TouchableOpacity>
</View>
}
</View>
);
}
_getDescription: () => Array<string>;
_onDismissed: () => void;
}
export default translate(Notification);

View File

@ -5,13 +5,16 @@ import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
import ErrorIcon from '@atlaskit/icon/glyph/error'; import ErrorIcon from '@atlaskit/icon/glyph/error';
import WarningIcon from '@atlaskit/icon/glyph/warning'; import WarningIcon from '@atlaskit/icon/glyph/warning';
import { colors } from '@atlaskit/theme'; import { colors } from '@atlaskit/theme';
import PropTypes from 'prop-types'; import React from 'react';
import React, { Component } from 'react';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { NOTIFICATION_TYPE } from '../constants'; import { NOTIFICATION_TYPE } from '../constants';
import AbstractNotification, {
type Props
} from './AbstractNotification';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/** /**
@ -32,110 +35,7 @@ const ICON_COLOR = {
* *
* @extends Component * @extends Component
*/ */
class Notification extends Component<*> { class Notification extends AbstractNotification<Props> {
/**
* Default values for {@code Notification} component's properties.
*
* @static
*/
static defaultProps = {
appearance: NOTIFICATION_TYPE.NORMAL
};
/**
* {@code Notification} component's property types.
*
* @static
*/
static propTypes = {
/**
* Display appearance for the component, passed directly to
* {@code Flag}.
*/
appearance: PropTypes.string,
/**
* The text to display in the body of the notification. If not passed
* in, the passed in descriptionKey will be used.
*/
defaultTitleKey: PropTypes.string,
/**
* A description string that can be used in addition to the prop
* descriptionKey.
*/
description: PropTypes.string,
/**
* The translation arguments that may be necessary for the description.
*/
descriptionArguments: PropTypes.object,
/**
* The translation key to use as the body of the notification.
*/
descriptionKey: PropTypes.string,
/**
* Whether the support link should be hidden in the case of an error
* message.
*/
hideErrorSupportLink: PropTypes.bool,
/**
* Whether or not the dismiss button should be displayed. This is passed
* in by {@code FlagGroup}.
*/
isDismissAllowed: PropTypes.bool,
/**
* Callback invoked when the user clicks to dismiss the notification.
* this is passed in by {@code FlagGroup}.
*/
onDismissed: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* The text to display at the top of the notification. If not passed in,
* the passed in titleKey will be used.
*/
title: PropTypes.string,
/**
* The translation arguments that may be necessary for the title.
*/
titleArguments: PropTypes.object,
/**
* The translation key to display as the title of the notification if
* no title is provided.
*/
titleKey: PropTypes.string,
/**
* The unique identifier for the notification. Passed back by the
* {@code Flag} component in the onDismissed callback.
*/
uid: PropTypes.number
};
/**
* Initializes a new {@code Notification} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -168,6 +68,8 @@ class Notification extends Component<*> {
); );
} }
_getDescription: () => Array<string>
_onDismissed: () => void; _onDismissed: () => void;
/** /**
@ -178,32 +80,15 @@ class Notification extends Component<*> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
_renderDescription() { _renderDescription() {
const {
description,
descriptionArguments,
descriptionKey,
t
} = this.props;
return ( return (
<div> <div>
{ descriptionKey {
? t(descriptionKey, descriptionArguments) : null } this._getDescription()
{ description || null } }
</div> </div>
); );
} }
/**
* Calls back into {@code FlagGroup} to dismiss the notification.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.onDismissed(this.props.uid);
}
/** /**
* Opens the support page. * Opens the support page.
* *

View File

@ -0,0 +1,145 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../base/responsive-ui';
import { FILMSTRIP_SIZE, isFilmstripVisible } from '../../filmstrip';
import { HANGUP_BUTTON_SIZE } from '../../toolbox';
import AbstractNotificationsContainer, {
_abstractMapStateToProps,
type Props as AbstractProps
} from './AbstractNotificationsContainer';
import Notification from './Notification';
import styles from './styles';
type Props = AbstractProps & {
/**
* True if the {@code Filmstrip} is visible, false otherwise.
*/
_filmstripVisible: boolean,
/**
* True if the {@ćode Toolbox} is visible, false otherwise.
*/
_toolboxVisible: boolean
};
/**
* The margin of the container to be kept from other components.
*/
const CONTAINER_MARGIN = 10;
/**
* Implements a React {@link Component} which displays notifications and handles
* automatic dismissmal after a notification is shown for a defined timeout
* period.
*
* @extends {Component}
*/
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _notifications } = this.props;
if (!_notifications || !_notifications.length) {
return null;
}
return (
<View
pointerEvents = 'box-none'
style = { styles.notificationOverlay }>
<View
pointerEvents = 'box-none'
style = { [
styles.notificationContainer,
this._getContainerStyle()
] }>
{
_notifications.map(notification => {
const { props, uid } = notification;
return (
<Notification
{ ...props }
key = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />
);
})
}
</View>
</View>
);
}
/**
* Generates a style object that is to be used for the notification
* container.
*
* @private
* @returns {?Object}
*/
_getContainerStyle() {
const { _filmstripVisible, _toolboxVisible } = this.props;
// The filmstrip only affects the position if we're on a narrow view.
const _narrow = isNarrowAspectRatio(this);
let bottom = 0;
let right = 0;
// The container needs additional distance from bottom when the
// filmstrip or the toolbox is visible.
_filmstripVisible && !_narrow && (right += FILMSTRIP_SIZE);
_filmstripVisible && _narrow && (bottom += FILMSTRIP_SIZE);
_toolboxVisible && (bottom += HANGUP_BUTTON_SIZE);
bottom += CONTAINER_MARGIN;
return {
bottom,
right
};
}
_onDismissed: number => void;
}
/**
* Maps (parts of) the Redux state to the associated NotificationsContainer's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _filmstripVisible: boolean,
* _notifications: Array,
* _showNotifications: boolean,
* _toolboxVisible: boolean
* }}
*/
export function _mapStateToProps(state: Object) {
return {
..._abstractMapStateToProps(state),
_filmstripVisible: isFilmstripVisible(state),
_toolboxVisible: state['features/toolbox'].visible
};
}
export default connect(_mapStateToProps)(
makeAspectRatioAware(NotificationsContainer));

View File

@ -1,11 +1,14 @@
// @flow
import { FlagGroup } from '@atlaskit/flag'; import { FlagGroup } from '@atlaskit/flag';
import PropTypes from 'prop-types'; import React from 'react';
import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hideNotification } from '../actions'; import AbstractNotificationsContainer, {
_abstractMapStateToProps as _mapStateToProps,
import { Notification } from './'; type Props
} from './AbstractNotificationsContainer';
import Notification from './Notification';
/** /**
* Implements a React {@link Component} which displays notifications and handles * Implements a React {@link Component} which displays notifications and handles
@ -14,91 +17,7 @@ import { Notification } from './';
* *
* @extends {Component} * @extends {Component}
*/ */
class NotificationsContainer extends Component { class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* {@code NotificationsContainer} component's property types.
*
* @static
*/
static propTypes = {
/**
* The notifications to be displayed, with the first index being the
* notification at the top and the rest shown below it in order.
*/
_notifications: PropTypes.array,
/**
* Whether or not notifications should be displayed at all. If not,
* notifications will be dismissed immediately.
*/
_showNotifications: PropTypes.bool,
/**
* Invoked to update the redux store in order to remove notifications.
*/
dispatch: PropTypes.func
};
/**
* Initializes a new {@code NotificationsContainer} instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
/**
* The timeout set for automatically dismissing a displayed
* notification. This value is set on the instance and not state to
* avoid additional re-renders.
*
* @type {number|null}
*/
this._notificationDismissTimeout = null;
// Bind event handlers so they are only bound once for every instance.
this._onDismissed = this._onDismissed.bind(this);
}
/**
* Sets a timeout if the currently displayed notification has changed.
*
* @inheritdoc
* returns {void}
*/
componentDidUpdate() {
const { _notifications, _showNotifications } = this.props;
if (_notifications.length) {
const notification = _notifications[0];
if (!_showNotifications || this._notificationDismissTimeout) {
// No-op because there should already be a notification that
// is waiting for dismissal.
} else {
const { timeout, uid } = notification;
this._notificationDismissTimeout = setTimeout(() => {
// Perform a no-op if a timeout is not specified.
if (Number.isInteger(timeout)) {
this._onDismissed(uid);
}
}, timeout);
}
}
}
/**
* Clear any dismissal timeout that is still active.
*
* @inheritdoc
* returns {void}
*/
componentWillUnmount() {
clearTimeout(this._notificationDismissTimeout);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
@ -114,20 +33,7 @@ class NotificationsContainer extends Component {
); );
} }
/** _onDismissed: number => void;
* Emits an action to remove the notification from the redux store so it
* stops displaying.
*
* @param {number} flagUid - The id of the notification to be removed.
* @private
* @returns {void}
*/
_onDismissed(flagUid) {
clearTimeout(this._notificationDismissTimeout);
this._notificationDismissTimeout = null;
this.props.dispatch(hideNotification(flagUid));
}
/** /**
* Renders notifications to display as ReactElements. An empty array will * Renders notifications to display as ReactElements. An empty array will
@ -137,11 +43,7 @@ class NotificationsContainer extends Component {
* @returns {ReactElement[]} * @returns {ReactElement[]}
*/ */
_renderFlags() { _renderFlags() {
const { _notifications, _showNotifications } = this.props; const { _notifications } = this.props;
if (!_showNotifications) {
return [];
}
return _notifications.map(notification => { return _notifications.map(notification => {
const { props, uid } = notification; const { props, uid } = notification;
@ -161,37 +63,4 @@ class NotificationsContainer extends Component {
} }
} }
/**
* Maps (parts of) the Redux state to the associated NotificationsContainer's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _notifications: Array
* }}
*/
function _mapStateToProps(state) {
// TODO: Per existing behavior, notifications should not display when an
// overlay is visible. This logic for checking overlay display can likely be
// simplified.
const {
connectionEstablished,
haveToReload,
isMediaPermissionPromptVisible,
suspendDetected
} = state['features/overlay'];
const isAnyOverlayVisible = (connectionEstablished && haveToReload)
|| isMediaPermissionPromptVisible
|| suspendDetected
|| state['features/base/jwt'].calleeInfoVisible;
const { enabled, notifications } = state['features/notifications'];
return {
_notifications: notifications,
_showNotifications: enabled && !isAnyOverlayVisible
};
}
export default connect(_mapStateToProps)(NotificationsContainer); export default connect(_mapStateToProps)(NotificationsContainer);

View File

@ -0,0 +1,115 @@
// @flow
import { StyleSheet } from 'react-native';
import { BoxModel, createStyleSheet, ColorPalette } from '../../base/styles';
/**
* The styles of the React {@code Components} of the feature notifications.
*/
export default createStyleSheet({
/**
* The content (left) column of the notification.
*/
contentColumn: {
flex: 1,
flexDirection: 'column',
padding: BoxModel.padding
},
/**
* Test style of the notification.
*/
contentText: {
color: ColorPalette.white
},
/**
* Dismiss icon style.
*/
dismissIcon: {
alignSelf: 'center',
color: ColorPalette.white,
fontSize: 16,
padding: 1.5 * BoxModel.padding
},
/**
* Outermost view of a single notification.
*/
notification: {
borderRadius: 5,
flexDirection: 'row',
marginTop: 0.5 * BoxModel.margin
},
/**
* Outermost container of a list of notifications.
*/
notificationContainer: {
alignItems: 'flex-start',
bottom: 0,
left: 0,
padding: 2 * BoxModel.padding,
position: 'absolute',
right: 0
},
/**
* Wrapper for the message (without title).
*/
notificationContent: {
flexDirection: 'column',
paddingVertical: 0.5 * BoxModel.padding
},
/**
* A full screen overlay to help to position the container.
*/
notificationOverlay: {
...StyleSheet.absoluteFillObject
},
/**
* The View containing the title.
*/
notificationTitle: {
paddingVertical: 0.5 * BoxModel.padding
},
/**
* Background settings for different notification types.
*/
notificationTypeError: {
backgroundColor: ColorPalette.R400
},
notificationTypeInfo: {
backgroundColor: ColorPalette.N500
},
notificationTypeNormal: {
// NOTE: Mobile has black background when the large video doesn't render
// a stream, so we avoid using black as the background of the normal
// type notifications.
backgroundColor: ColorPalette.N500
},
notificationTypeSuccess: {
backgroundColor: ColorPalette.G400
},
notificationTypeWarning: {
backgroundColor: ColorPalette.Y200
},
/**
* Title text style.
*/
titleText: {
color: ColorPalette.white,
fontWeight: 'bold'
}
});

View File

@ -2,4 +2,5 @@ export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components'; export * from './components';
import './middleware';
import './reducer'; import './reducer';

View File

@ -0,0 +1,19 @@
/* @flow */
import { getCurrentConference } from '../base/conference';
import { StateListenerRegistry } from '../base/redux';
import { clearNotifications } from './actions';
/**
* StateListenerRegistry provides a reliable way to detect the leaving of a
* conference, where we need to clean up the notifications.
*/
StateListenerRegistry.register(
/* selector */ state => getCurrentConference(state),
/* listener */ (conference, { dispatch }) => {
if (!conference) {
dispatch(clearNotifications());
}
}
);

View File

@ -1,6 +1,7 @@
import { ReducerRegistry } from '../base/redux'; import { ReducerRegistry } from '../base/redux';
import { import {
CLEAR_NOTIFICATIONS,
HIDE_NOTIFICATION, HIDE_NOTIFICATION,
SET_NOTIFICATIONS_ENABLED, SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION SHOW_NOTIFICATION
@ -28,6 +29,11 @@ const DEFAULT_STATE = {
ReducerRegistry.register('features/notifications', ReducerRegistry.register('features/notifications',
(state = DEFAULT_STATE, action) => { (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case CLEAR_NOTIFICATIONS:
return {
...state,
notifications: []
};
case HIDE_NOTIFICATION: case HIDE_NOTIFICATION:
return { return {
...state, ...state,

View File

@ -3,40 +3,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PageReloadFilmstripOnlyOverlay from './PageReloadFilmstripOnlyOverlay'; import { getOverlayToRender } from '../functions';
import PageReloadOverlay from './PageReloadOverlay';
import SuspendedFilmstripOnlyOverlay from './SuspendedFilmstripOnlyOverlay';
import SuspendedOverlay from './SuspendedOverlay';
import UserMediaPermissionsFilmstripOnlyOverlay
from './UserMediaPermissionsFilmstripOnlyOverlay';
import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/**
* The lazily-initialized list of overlay React {@link Component} types used The
* user interface is filmstrip-only.
*
* XXX The value is meant to be compile-time defined so it does not contradict
* our coding style to not have global values that are runtime defined and
* merely works around side effects of circular imports.
*
* @type Array
*/
let _filmstripOnlyOverlays;
/**
* The lazily-initialized list of overlay React {@link Component} types used The
* user interface is not filmstrip-only.
*
* XXX The value is meant to be compile-time defined so it does not contradict
* our coding style to not have global values that are runtime defined and
* merely works around side effects of circular imports.
*
* @type Array
*/
let _nonFilmstripOnlyOverlays;
/** /**
* The type of the React {@link Component} props of {@code OverlayContainer}. * The type of the React {@link Component} props of {@code OverlayContainer}.
*/ */
@ -68,44 +38,6 @@ class OverlayContainer extends Component<Props> {
} }
} }
/**
* Returns the list of overlay React {@link Component} types to be rendered by
* {@code OverlayContainer}. The list is lazily initialized the first time it is
* required in order to works around side effects of circular imports.
*
* @param {boolean} filmstripOnly - The indicator which determines whether the
* user interface is filmstrip-only.
* @returns {Array} The list of overlay React {@code Component} types to be
* rendered by {@code OverlayContainer}.
*/
function _getOverlays(filmstripOnly) {
let overlays;
if (filmstripOnly) {
if (!(overlays = _filmstripOnlyOverlays)) {
overlays = _filmstripOnlyOverlays = [
PageReloadFilmstripOnlyOverlay,
SuspendedFilmstripOnlyOverlay,
UserMediaPermissionsFilmstripOnlyOverlay
];
}
} else if (!(overlays = _nonFilmstripOnlyOverlays)) {
overlays = _nonFilmstripOnlyOverlays = [
PageReloadOverlay
];
// Mobile only has a PageReloadOverlay.
if (navigator.product !== 'ReactNative') {
overlays.push(...[
SuspendedOverlay,
UserMediaPermissionsOverlay
]);
}
}
return overlays;
}
/** /**
* Maps (parts of) the redux state to the associated {@code OverlayContainer}'s * Maps (parts of) the redux state to the associated {@code OverlayContainer}'s
* props. * props.
@ -117,30 +49,12 @@ function _getOverlays(filmstripOnly) {
* }} * }}
*/ */
function _mapStateToProps(state) { function _mapStateToProps(state) {
// XXX In the future interfaceConfig is expected to not be a global variable
// but a redux state like config. Hence, the variable filmStripOnly
// naturally belongs here in preparation for the future.
const filmstripOnly
= typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly;
let overlay;
for (const o of _getOverlays(filmstripOnly)) {
// react-i18n / react-redux wrap components and thus we cannot access
// the wrapped component's static methods directly.
const component = o.WrappedComponent || o;
if (component.needsRender(state)) {
overlay = o;
break;
}
}
return { return {
/** /**
* The React {@link Component} type of overlay to be rendered by the * The React {@link Component} type of overlay to be rendered by the
* associated {@code OverlayContainer}. * associated {@code OverlayContainer}.
*/ */
overlay overlay: getOverlayToRender(state)
}; };
} }

View File

@ -1 +1,15 @@
export { default as OverlayContainer } from './OverlayContainer'; export { default as OverlayContainer } from './OverlayContainer';
export {
default as PageReloadFilmstripOnlyOverlay
} from './PageReloadFilmstripOnlyOverlay';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export {
default as SuspendedFilmstripOnlyOverlay
} from './SuspendedFilmstripOnlyOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export {
default as UserMediaPermissionsFilmstripOnlyOverlay
} from './UserMediaPermissionsFilmstripOnlyOverlay';
export {
default as UserMediaPermissionsOverlay
} from './UserMediaPermissionsOverlay';

View File

@ -0,0 +1,66 @@
// @flow
import {
PageReloadFilmstripOnlyOverlay,
PageReloadOverlay,
SuspendedFilmstripOnlyOverlay,
SuspendedOverlay,
UserMediaPermissionsFilmstripOnlyOverlay,
UserMediaPermissionsOverlay
} from './components';
declare var interfaceConfig: Object;
/**
* Returns the list of available overlays that might be rendered.
*
* @private
* @returns {Array<?React$ComponentType<*>>}
*/
function _getOverlays() {
const filmstripOnly
= typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly;
let overlays;
if (filmstripOnly) {
overlays = [
PageReloadFilmstripOnlyOverlay,
SuspendedFilmstripOnlyOverlay,
UserMediaPermissionsFilmstripOnlyOverlay
];
} else {
overlays = [
PageReloadOverlay
];
}
// Mobile only has a PageReloadOverlay.
if (navigator.product !== 'ReactNative') {
overlays.push(...[
SuspendedOverlay,
UserMediaPermissionsOverlay
]);
}
return overlays;
}
/**
* Returns the overlay to be currently rendered.
*
* @param {Object} state - The Redux state.
* @returns {?React$ComponentType<*>}
*/
export function getOverlayToRender(state: Object) {
for (const overlay of _getOverlays()) {
// react-i18n / react-redux wrap components and thus we cannot access
// the wrapped component's static methods directly.
const component = overlay.WrappedComponent || overlay;
if (component.needsRender(state)) {
return overlay;
}
}
return undefined;
}

View File

@ -1,5 +1,6 @@
export * from './actions'; export * from './actions';
export * from './components'; export * from './components';
export * from './functions';
import './middleware'; import './middleware';
import './reducer'; import './reducer';

View File

@ -3,6 +3,8 @@ import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles'; import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles';
import { HANGUP_BUTTON_SIZE } from '../../constants';
// Toolbox, toolbar: // Toolbox, toolbar:
/** /**
@ -44,8 +46,8 @@ const styles = createStyleSheet({
...toolbarButton, ...toolbarButton,
backgroundColor: ColorPalette.red, backgroundColor: ColorPalette.red,
borderRadius: 30, borderRadius: 30,
height: 60, height: HANGUP_BUTTON_SIZE,
width: 60 width: HANGUP_BUTTON_SIZE
}, },
/** /**

View File

@ -0,0 +1,7 @@
// @flow
/**
* The size of the hangup button. As that is the largest button, it defines
* the size of the {@code ToolBox}, so other components may relate to that.
*/
export const HANGUP_BUTTON_SIZE = 60;

View File

@ -1,6 +1,7 @@
export * from './actions'; export * from './actions';
export * from './actionTypes'; export * from './actionTypes';
export * from './components'; export * from './components';
export * from './constants';
export * from './functions'; export * from './functions';
import './middleware'; import './middleware';