[RN] Implement Notifications on mobile

This commit is contained in:
Bettenbuk Zoltan
2018-06-28 12:47:50 +02:00
committed by Paweł Domas
parent 00f18e9369
commit ffd0827354
34 changed files with 1923 additions and 1329 deletions
+3
View File
@@ -27,6 +27,9 @@
.icon-arrow_back:before {
content: "\e5c4";
}
.icon-close:before {
content: "\e5cd";
}
.icon-event_note:before {
content: "\e616";
}
BIN
View File
Binary file not shown.
+1
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="&#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="&#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="&#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" />

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+508 -481
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,5 +24,16 @@ export const ColorPalette = {
darkGrey: '#555555',
green: '#40b183',
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
};
@@ -17,6 +17,7 @@ import { createDesiredLocalTracks } from '../../base/tracks';
import { ConferenceNotification } from '../../calendar-sync';
import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video';
import { NotificationsContainer } from '../../notifications';
import { setToolboxVisible, Toolbox } from '../../toolbox';
import ConferenceIndicators from './ConferenceIndicators';
@@ -267,6 +268,8 @@ class Conference extends Component<Props> {
this._renderConferenceNotification()
}
<NotificationsContainer />
{/*
* The dialogs are in the topmost stacking layers.
*/
@@ -10,6 +10,8 @@ import {
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { isFilmstripVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail';
import styles from './styles';
import Thumbnail from './Thumbnail';
@@ -188,7 +190,7 @@ class Filmstrip extends Component<Props> {
*/
function _mapStateToProps(state) {
const participants = state['features/base/participants'];
const { enabled, visible } = state['features/filmstrip'];
const { enabled } = state['features/filmstrip'];
return {
/**
@@ -215,7 +217,7 @@ function _mapStateToProps(state) {
* @private
* @type {boolean}
*/
_visible: visible && participants.length > 1
_visible: isFilmstripVisible(state)
};
}
@@ -1,4 +1,5 @@
import { ColorPalette } from '../../base/styles';
import { FILMSTRIP_SIZE } from '../constants';
/**
* Size for the Avatar.
@@ -44,12 +45,15 @@ export default {
...filmstrip,
flexDirection: 'row',
justifyContent: 'flex-end',
height: 90
height: FILMSTRIP_SIZE
},
/**
* The style of the wide {@link Filmstrip} version which displays thumbnails
* 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: {
...filmstrip,
+6
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;
@@ -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;
}
@@ -4,9 +4,25 @@ import {
getParticipantCount,
getPinnedParticipant
} from '../base/participants';
import { toState } from '../base/redux';
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
* the filmstrip.
+1
View File
@@ -1,6 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './constants';
export * from './functions';
import './middleware';
@@ -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
* not be displayed anymore.
+21 -5
View File
@@ -1,4 +1,7 @@
// @flow
import {
CLEAR_NOTIFICATIONS,
HIDE_NOTIFICATION,
SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION
@@ -6,6 +9,19 @@ import {
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.
*
@@ -16,7 +32,7 @@ import { NOTIFICATION_TYPE } from './constants';
* uid: number
* }}
*/
export function hideNotification(uid) {
export function hideNotification(uid: number) {
return {
type: HIDE_NOTIFICATION,
uid
@@ -32,7 +48,7 @@ export function hideNotification(uid) {
* enabled: boolean
* }}
*/
export function setNotificationsEnabled(enabled) {
export function setNotificationsEnabled(enabled: boolean) {
return {
type: SET_NOTIFICATIONS_ENABLED,
enabled
@@ -45,7 +61,7 @@ export function setNotificationsEnabled(enabled) {
* @param {Object} props - The props needed to show the notification component.
* @returns {Object}
*/
export function showErrorNotification(props) {
export function showErrorNotification(props: Object) {
return showNotification({
...props,
appearance: NOTIFICATION_TYPE.ERROR
@@ -65,7 +81,7 @@ export function showErrorNotification(props) {
* uid: number
* }}
*/
export function showNotification(props = {}, timeout) {
export function showNotification(props: Object = {}, timeout: ?number) {
return {
type: SHOW_NOTIFICATION,
props,
@@ -80,7 +96,7 @@ export function showNotification(props = {}, timeout) {
* @param {Object} props - The props needed to show the notification component.
* @returns {Object}
*/
export function showWarningNotification(props) {
export function showWarningNotification(props: Object) {
return showNotification({
...props,
appearance: NOTIFICATION_TYPE.WARNING
@@ -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);
}
}
@@ -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 : []
};
}
@@ -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);
@@ -5,13 +5,16 @@ import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
import ErrorIcon from '@atlaskit/icon/glyph/error';
import WarningIcon from '@atlaskit/icon/glyph/warning';
import { colors } from '@atlaskit/theme';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import { translate } from '../../base/i18n';
import { NOTIFICATION_TYPE } from '../constants';
import AbstractNotification, {
type Props
} from './AbstractNotification';
declare var interfaceConfig: Object;
/**
@@ -32,110 +35,7 @@ const ICON_COLOR = {
*
* @extends Component
*/
class Notification extends Component<*> {
/**
* 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);
}
class Notification extends AbstractNotification<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -168,6 +68,8 @@ class Notification extends Component<*> {
);
}
_getDescription: () => Array<string>
_onDismissed: () => void;
/**
@@ -178,32 +80,15 @@ class Notification extends Component<*> {
* @returns {ReactElement}
*/
_renderDescription() {
const {
description,
descriptionArguments,
descriptionKey,
t
} = this.props;
return (
<div>
{ descriptionKey
? t(descriptionKey, descriptionArguments) : null }
{ description || null }
{
this._getDescription()
}
</div>
);
}
/**
* Calls back into {@code FlagGroup} to dismiss the notification.
*
* @private
* @returns {void}
*/
_onDismissed() {
this.props.onDismissed(this.props.uid);
}
/**
* Opens the support page.
*
@@ -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));
@@ -1,11 +1,14 @@
// @flow
import { FlagGroup } from '@atlaskit/flag';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { hideNotification } from '../actions';
import { Notification } from './';
import AbstractNotificationsContainer, {
_abstractMapStateToProps as _mapStateToProps,
type Props
} from './AbstractNotificationsContainer';
import Notification from './Notification';
/**
* Implements a React {@link Component} which displays notifications and handles
@@ -14,91 +17,7 @@ import { Notification } from './';
*
* @extends {Component}
*/
class NotificationsContainer extends Component {
/**
* {@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);
}
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* Implements React's {@link Component#render()}.
@@ -114,20 +33,7 @@ class NotificationsContainer extends Component {
);
}
/**
* 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));
}
_onDismissed: number => void;
/**
* Renders notifications to display as ReactElements. An empty array will
@@ -137,11 +43,7 @@ class NotificationsContainer extends Component {
* @returns {ReactElement[]}
*/
_renderFlags() {
const { _notifications, _showNotifications } = this.props;
if (!_showNotifications) {
return [];
}
const { _notifications } = this.props;
return _notifications.map(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);
@@ -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'
}
});
+1
View File
@@ -2,4 +2,5 @@ export * from './actions';
export * from './actionTypes';
export * from './components';
import './middleware';
import './reducer';
@@ -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());
}
}
);
+6
View File
@@ -1,6 +1,7 @@
import { ReducerRegistry } from '../base/redux';
import {
CLEAR_NOTIFICATIONS,
HIDE_NOTIFICATION,
SET_NOTIFICATIONS_ENABLED,
SHOW_NOTIFICATION
@@ -28,6 +29,11 @@ const DEFAULT_STATE = {
ReducerRegistry.register('features/notifications',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case CLEAR_NOTIFICATIONS:
return {
...state,
notifications: []
};
case HIDE_NOTIFICATION:
return {
...state,
@@ -3,40 +3,10 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PageReloadFilmstripOnlyOverlay from './PageReloadFilmstripOnlyOverlay';
import PageReloadOverlay from './PageReloadOverlay';
import SuspendedFilmstripOnlyOverlay from './SuspendedFilmstripOnlyOverlay';
import SuspendedOverlay from './SuspendedOverlay';
import UserMediaPermissionsFilmstripOnlyOverlay
from './UserMediaPermissionsFilmstripOnlyOverlay';
import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay';
import { getOverlayToRender } from '../functions';
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}.
*/
@@ -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
* props.
@@ -117,30 +49,12 @@ function _getOverlays(filmstripOnly) {
* }}
*/
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 {
/**
* The React {@link Component} type of overlay to be rendered by the
* associated {@code OverlayContainer}.
*/
overlay
overlay: getOverlayToRender(state)
};
}
@@ -1 +1,15 @@
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';
+66
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;
}
+1
View File
@@ -1,5 +1,6 @@
export * from './actions';
export * from './components';
export * from './functions';
import './middleware';
import './reducer';
@@ -3,6 +3,8 @@ import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles';
import { HANGUP_BUTTON_SIZE } from '../../constants';
// Toolbox, toolbar:
/**
@@ -44,8 +46,8 @@ const styles = createStyleSheet({
...toolbarButton,
backgroundColor: ColorPalette.red,
borderRadius: 30,
height: 60,
width: 60
height: HANGUP_BUTTON_SIZE,
width: HANGUP_BUTTON_SIZE
},
/**
+7
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;
+1
View File
@@ -1,6 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './constants';
export * from './functions';
import './middleware';