[RN] Implement Notifications on mobile
This commit is contained in:
parent
00f18e9369
commit
ffd0827354
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
|
@ -15,6 +15,7 @@
|
||||||
<glyph unicode="" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
|
<glyph unicode="" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
|
||||||
<glyph unicode="" 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="" 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="" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
|
<glyph unicode="" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
|
||||||
|
<glyph unicode="" 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="" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
|
<glyph unicode="" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
|
||||||
<glyph unicode="" 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="" 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="" 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="" 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
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
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',
|
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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
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.
|
|
@ -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';
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 { 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);
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
||||||
|
});
|
|
@ -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';
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,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';
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,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';
|
||||||
|
|
Loading…
Reference in New Issue