From c7979a39449a5a861e4796bc9b9b8f55161c89a5 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 20 Mar 2019 15:09:23 -0500 Subject: [PATCH] feat(mobile): add 1 liner notifications Adds 1 liner notifications to mobile. Only the title is displayed. In case the title is missing there's a fallback to the description. --- lang/main.json | 6 +- .../components/AbstractConference.js | 42 +++++++++ .../components/native/Conference.js | 51 +++++++++- .../conference/components/native/Labels.js | 5 +- .../components/native/NavigationBar.js | 56 +++++------ .../conference/components/native/styles.js | 4 + .../conference/components/web/Conference.js | 12 ++- react/features/conference/functions.js | 27 +++++- .../AbstractNotificationsContainer.js | 11 +-- .../components/Notification.native.js | 92 +++++++++++++++++++ .../NotificationsContainer.native.js | 67 ++++++++++++++ .../notifications/components/styles.js | 61 ++++++++++++ react/features/notifications/functions.js | 15 +++ react/features/notifications/index.js | 1 + 14 files changed, 396 insertions(+), 54 deletions(-) create mode 100644 react/features/notifications/components/styles.js create mode 100644 react/features/notifications/functions.js diff --git a/lang/main.json b/lang/main.json index 484b65e2b..2228ee4e4 100644 --- a/lang/main.json +++ b/lang/main.json @@ -458,9 +458,9 @@ }, "me": "me", "notify": { - "connectedOneMember": "__name__ connected", - "connectedThreePlusMembers": "__name__ and __count__ others connected", - "connectedTwoMembers": "__first__ and __second__ connected", + "connectedOneMember": "__name__ joined the meeting", + "connectedThreePlusMembers": "__name__ and __count__ others joined the meeting", + "connectedTwoMembers": "__first__ and __second__ joined the meeting", "disconnected": "disconnected", "focus": "Conference focus", "focusFail": "__component__ not available - retry in __ms__ sec", diff --git a/react/features/conference/components/AbstractConference.js b/react/features/conference/components/AbstractConference.js index d19f07479..5ed3ea4cf 100644 --- a/react/features/conference/components/AbstractConference.js +++ b/react/features/conference/components/AbstractConference.js @@ -1,5 +1,10 @@ // @flow +import React, { Component } from 'react'; + +import { NotificationsContainer } from '../../notifications/components'; + +import { shouldDisplayNotifications } from '../functions'; import { shouldDisplayTileView } from '../../video-layout'; /** @@ -7,6 +12,14 @@ import { shouldDisplayTileView } from '../../video-layout'; */ export type AbstractProps = { + /** + * Set to {@code true} when the notifications are to be displayed. + * + * @protected + * @type {boolean} + */ + _notificationsVisible: boolean, + /** * Conference room name. * @@ -24,6 +37,34 @@ export type AbstractProps = { _shouldDisplayTileView: boolean }; +/** + * A container to hold video status labels, including recording status and + * current large video quality. + * + * @extends Component + */ +export class AbstractConference + extends Component { + + /** + * Renders the {@code LocalRecordingLabel}. + * + * @param {Object} props - The properties to be passed to + * the {@code NotificationsContainer}. + * @protected + * @returns {React$Element} + */ + renderNotificationsContainer(props: ?Object) { + if (this.props._notificationsVisible) { + return ( + React.createElement(NotificationsContainer, props) + ); + } + + return null; + } +} + /** * Maps (parts of) the redux state to the associated props of the {@link Labels} * {@code Component}. @@ -34,6 +75,7 @@ export type AbstractProps = { */ export function abstractMapStateToProps(state: Object) { return { + _notificationsVisible: shouldDisplayNotifications(state), _room: state['features/base/conference'].room, _shouldDisplayTileView: shouldDisplayTileView(state) }; diff --git a/react/features/conference/components/native/Conference.js b/react/features/conference/components/native/Conference.js index 8ee260b17..e28b072a7 100644 --- a/react/features/conference/components/native/Conference.js +++ b/react/features/conference/components/native/Conference.js @@ -1,8 +1,8 @@ // @flow -import React, { Component } from 'react'; +import React from 'react'; -import { BackHandler, StatusBar, View } from 'react-native'; +import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native'; import { connect as reactReduxConnect } from 'react-redux'; import { appNavigate } from '../../../app'; @@ -10,6 +10,7 @@ import { connect, disconnect } from '../../../base/connection'; import { getParticipantCount } from '../../../base/participants'; import { Container, LoadingIndicator, TintedView } from '../../../base/react'; import { + isNarrowAspectRatio, makeAspectRatioAware } from '../../../base/responsive-ui'; import { TestConnectionInfo } from '../../../base/testing'; @@ -17,6 +18,7 @@ import { createDesiredLocalTracks } from '../../../base/tracks'; import { ConferenceNotification } from '../../../calendar-sync'; import { Chat } from '../../../chat'; import { + FILMSTRIP_SIZE, Filmstrip, isFilmstripVisible, TileView @@ -26,7 +28,10 @@ import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite'; import { Captions } from '../../../subtitles'; import { setToolboxVisible, Toolbox } from '../../../toolbox'; -import { abstractMapStateToProps } from '../AbstractConference'; +import { + AbstractConference, + abstractMapStateToProps +} from '../AbstractConference'; import DisplayNameLabel from './DisplayNameLabel'; import Labels from './Labels'; import NavigationBar from './NavigationBar'; @@ -134,7 +139,7 @@ type Props = AbstractProps & { /** * The conference page of the mobile (i.e. React Native) application. */ -class Conference extends Component { +class Conference extends AbstractConference { /** * Initializes a new Conference instance. * @@ -296,7 +301,12 @@ class Conference extends Component { } - + + + { this.renderNotificationsContainer() } + @@ -341,6 +351,37 @@ class Conference extends Component { ? : undefined); } + + /** + * Renders a container for notifications to be displayed by the + * base/notifications feature. + * + * @private + * @returns {React$Element} + */ + renderNotificationsContainer() { + const notificationsStyle = {}; + + // In the landscape mode (wide) there's problem with notifications being + // shadowed by the filmstrip rendered on the right. This makes the "x" + // button not clickable. In order to avoid that a margin of the + // filmstrip's size is added to the right. + // + // Pawel: after many attempts I failed to make notifications adjust to + // their contents width because of column and rows being used in the + // flex layout. The only option that seemed to limit the notification's + // size was explicit 'width' value which is not better than the margin + // added here. + if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) { + notificationsStyle.marginRight = FILMSTRIP_SIZE; + } + + return super.renderNotificationsContainer( + { + style: notificationsStyle + } + ); + } } /** diff --git a/react/features/conference/components/native/Labels.js b/react/features/conference/components/native/Labels.js index 11f1b0df5..76fd653a6 100644 --- a/react/features/conference/components/native/Labels.js +++ b/react/features/conference/components/native/Labels.js @@ -21,6 +21,7 @@ import AbstractLabels, { _abstractMapStateToProps, type Props as AbstractLabelsProps } from '../AbstractLabels'; +import { shouldDisplayNotifications } from '../../functions'; import styles from './styles'; /** @@ -363,7 +364,9 @@ function _mapStateToProps(state) { return { ..._abstractMapStateToProps(state), _reducedUI: state['features/base/responsive-ui'].reducedUI, - _visible: !isToolboxVisible(state) && !shouldDisplayTileView(state) + _visible: !isToolboxVisible(state) + && !shouldDisplayTileView(state) + && !shouldDisplayNotifications(state) }; } diff --git a/react/features/conference/components/native/NavigationBar.js b/react/features/conference/components/native/NavigationBar.js index 08bf9ba14..f57932196 100644 --- a/react/features/conference/components/native/NavigationBar.js +++ b/react/features/conference/components/native/NavigationBar.js @@ -40,39 +40,33 @@ class NavigationBar extends Component { return null; } - return ( - - - - - - - - - - - - { this.props._meetingName } - - - + return [ + + + + , + + + + + { this.props._meetingName } + + - ); + ]; } } diff --git a/react/features/conference/components/native/styles.js b/react/features/conference/components/native/styles.js index c454e7b7c..9d71f6fc0 100644 --- a/react/features/conference/components/native/styles.js +++ b/react/features/conference/components/native/styles.js @@ -37,6 +37,10 @@ export default createStyleSheet({ }, gradient: { + position: 'absolute', + top: 0, + left: 0, + right: 0, flex: 1 }, diff --git a/react/features/conference/components/web/Conference.js b/react/features/conference/components/web/Conference.js index 4693acf0a..970893eeb 100644 --- a/react/features/conference/components/web/Conference.js +++ b/react/features/conference/components/web/Conference.js @@ -1,7 +1,7 @@ // @flow import _ from 'lodash'; -import React, { Component } from 'react'; +import React from 'react'; import { connect as reactReduxConnect } from 'react-redux'; import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout'; @@ -13,7 +13,6 @@ import { Chat } from '../../../chat'; import { Filmstrip } from '../../../filmstrip'; import { CalleeInfoContainer } from '../../../invite'; import { LargeVideo } from '../../../large-video'; -import { NotificationsContainer } from '../../../notifications'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { @@ -28,7 +27,10 @@ import { maybeShowSuboptimalExperienceNotification } from '../../functions'; import Labels from './Labels'; import { default as Notice } from './Notice'; import { default as Subject } from './Subject'; -import { abstractMapStateToProps } from '../AbstractConference'; +import { + AbstractConference, + abstractMapStateToProps +} from '../AbstractConference'; import type { AbstractProps } from '../AbstractConference'; @@ -87,7 +89,7 @@ type Props = AbstractProps & { /** * The conference page of the Web application. */ -class Conference extends Component { +class Conference extends AbstractConference { _onFullScreenChange: Function; _onShowToolbar: Function; _originalOnShowToolbar: Function; @@ -218,7 +220,7 @@ class Conference extends Component { { filmstripOnly || } { filmstripOnly || } - + { this.renderNotificationsContainer() } diff --git a/react/features/conference/functions.js b/react/features/conference/functions.js index deba440ed..482651766 100644 --- a/react/features/conference/functions.js +++ b/react/features/conference/functions.js @@ -1,7 +1,13 @@ -import { getName } from '../app'; import { translateToHTML } from '../base/i18n'; import { browser } from '../base/lib-jitsi-meet'; -import { showWarningNotification } from '../notifications'; +import { toState } from '../base/redux'; + +import { getName } from '../app'; +import { + areThereNotifications, + showWarningNotification +} from '../notifications'; +import { getOverlayToRender } from '../overlay'; /** * Shows the suboptimal experience notification if needed. @@ -36,3 +42,20 @@ export function maybeShowSuboptimalExperienceNotification(dispatch, t) { ); } } + +/** + * Tells whether or not the notifications should be displayed within + * the conference feature based on the current Redux state. + * + * @param {Object|Function} stateful - The redux store state. + * @returns {boolean} + */ +export function shouldDisplayNotifications(stateful) { + const state = toState(stateful); + const isAnyOverlayVisible = Boolean(getOverlayToRender(state)); + const { calleeInfoVisible } = state['features/invite']; + + return areThereNotifications(state) + && !isAnyOverlayVisible + && !calleeInfoVisible; +} diff --git a/react/features/notifications/components/AbstractNotificationsContainer.js b/react/features/notifications/components/AbstractNotificationsContainer.js index 998b86dde..7777f0c4d 100644 --- a/react/features/notifications/components/AbstractNotificationsContainer.js +++ b/react/features/notifications/components/AbstractNotificationsContainer.js @@ -2,9 +2,8 @@ import { Component } from 'react'; -import { getOverlayToRender } from '../../overlay'; - import { hideNotification } from '../actions'; +import { areThereNotifications } from '../functions'; export type Props = { @@ -165,12 +164,10 @@ export default class AbstractNotificationsContainer * }} */ export function _abstractMapStateToProps(state: Object) { - const isAnyOverlayVisible = Boolean(getOverlayToRender(state)); - const { enabled, notifications } = state['features/notifications']; - const { calleeInfoVisible } = state['features/invite']; + const { notifications } = state['features/notifications']; + const _visible = areThereNotifications(state); return { - _notifications: enabled && !isAnyOverlayVisible && !calleeInfoVisible - ? notifications : [] + _notifications: _visible ? notifications : [] }; } diff --git a/react/features/notifications/components/Notification.native.js b/react/features/notifications/components/Notification.native.js index e69de29bb..4e78d8c03 100644 --- a/react/features/notifications/components/Notification.native.js +++ b/react/features/notifications/components/Notification.native.js @@ -0,0 +1,92 @@ +// @flow + +import React from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; + +import { Icon } from '../../base/font-icons'; +import { translate } from '../../base/i18n'; + +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 { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { + isDismissAllowed + } = this.props; + + return ( + + + + { + this._renderContent() + } + + + { + isDismissAllowed + && + + + } + + ); + } + + /** + * Renders the notification's content. If the title or title key is present + * it will be just the title. Otherwise it will fallback to description. + * + * @returns {Array} + * @private + */ + _renderContent() { + const { t, title, titleArguments, titleKey } = this.props; + const titleText = title || (titleKey && t(titleKey, titleArguments)); + + if (titleText) { + return ( + + { titleText } + + ); + } + + return this._getDescription().map((line, index) => ( + + { line } + + )); + } + + _getDescription: () => Array; + + _onDismissed: () => void; +} + +export default translate(Notification); diff --git a/react/features/notifications/components/NotificationsContainer.native.js b/react/features/notifications/components/NotificationsContainer.native.js index e69de29bb..a8326d11e 100644 --- a/react/features/notifications/components/NotificationsContainer.native.js +++ b/react/features/notifications/components/NotificationsContainer.native.js @@ -0,0 +1,67 @@ +// @flow + +import React from 'react'; +import { View } from 'react-native'; +import { connect } from 'react-redux'; + +import AbstractNotificationsContainer, { + _abstractMapStateToProps, + type Props as AbstractProps +} from './AbstractNotificationsContainer'; +import Notification from './Notification'; +import styles from './styles'; + +type Props = AbstractProps & { + + /** + * Any custom styling applied to the notifications container. + */ + style: Object +}; + +/** + * 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 { + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + const { _notifications } = this.props; + + if (!_notifications || !_notifications.length) { + return null; + } + + return ( + + { + _notifications.map( + ({ props, uid }) => ( + )) + } + + ); + } + + _onDismissed: number => void; +} + +export default connect(_abstractMapStateToProps)(NotificationsContainer); diff --git a/react/features/notifications/components/styles.js b/react/features/notifications/components/styles.js new file mode 100644 index 000000000..6ecefc192 --- /dev/null +++ b/react/features/notifications/components/styles.js @@ -0,0 +1,61 @@ +// @flow + +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: { + justifyContent: 'center', + flex: 1, + flexDirection: 'column', + paddingLeft: 1.5 * BoxModel.padding + }, + + /** + * Test style of the notification. + */ + contentText: { + alignSelf: 'flex-start', + color: ColorPalette.white + }, + + /** + * Dismiss icon style. + */ + dismissIcon: { + color: ColorPalette.white, + fontSize: 20, + padding: 1.5 * BoxModel.padding + }, + + /** + * Outermost view of a single notification. + */ + notification: { + backgroundColor: '#768898', + flexDirection: 'row', + height: 48, + marginTop: 0.5 * BoxModel.margin + }, + + /** + * Outermost container of a list of notifications. + */ + notificationContainer: { + flexGrow: 0, + justifyContent: 'flex-end' + }, + + /** + * Wrapper for the message. + */ + notificationContent: { + flexDirection: 'column' + } +}); diff --git a/react/features/notifications/functions.js b/react/features/notifications/functions.js new file mode 100644 index 000000000..1f06858d6 --- /dev/null +++ b/react/features/notifications/functions.js @@ -0,0 +1,15 @@ +import { toState } from '../base/redux'; + +/** + * Tells whether or not the notifications are enabled and if there are any + * notifications to be displayed based on the current Redux state. + * + * @param {Object|Function} stateful - The redux store state. + * @returns {boolean} + */ +export function areThereNotifications(stateful) { + const state = toState(stateful); + const { enabled, notifications } = state['features/notifications']; + + return enabled && notifications.length > 0; +} diff --git a/react/features/notifications/index.js b/react/features/notifications/index.js index a29aa08e0..44000a1a7 100644 --- a/react/features/notifications/index.js +++ b/react/features/notifications/index.js @@ -1,6 +1,7 @@ export * from './actions'; export * from './actionTypes'; export * from './components'; +export * from './functions'; import './middleware'; import './reducer';