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.
This commit is contained in:
paweldomas 2019-03-20 15:09:23 -05:00 committed by Zoltan Bettenbuk
parent 15fd27543a
commit c7979a3944
14 changed files with 396 additions and 54 deletions

View File

@ -458,9 +458,9 @@
}, },
"me": "me", "me": "me",
"notify": { "notify": {
"connectedOneMember": "__name__ connected", "connectedOneMember": "__name__ joined the meeting",
"connectedThreePlusMembers": "__name__ and __count__ others connected", "connectedThreePlusMembers": "__name__ and __count__ others joined the meeting",
"connectedTwoMembers": "__first__ and __second__ connected", "connectedTwoMembers": "__first__ and __second__ joined the meeting",
"disconnected": "disconnected", "disconnected": "disconnected",
"focus": "Conference focus", "focus": "Conference focus",
"focusFail": "__component__ not available - retry in __ms__ sec", "focusFail": "__component__ not available - retry in __ms__ sec",

View File

@ -1,5 +1,10 @@
// @flow // @flow
import React, { Component } from 'react';
import { NotificationsContainer } from '../../notifications/components';
import { shouldDisplayNotifications } from '../functions';
import { shouldDisplayTileView } from '../../video-layout'; import { shouldDisplayTileView } from '../../video-layout';
/** /**
@ -7,6 +12,14 @@ import { shouldDisplayTileView } from '../../video-layout';
*/ */
export type AbstractProps = { export type AbstractProps = {
/**
* Set to {@code true} when the notifications are to be displayed.
*
* @protected
* @type {boolean}
*/
_notificationsVisible: boolean,
/** /**
* Conference room name. * Conference room name.
* *
@ -24,6 +37,34 @@ export type AbstractProps = {
_shouldDisplayTileView: boolean _shouldDisplayTileView: boolean
}; };
/**
* A container to hold video status labels, including recording status and
* current large video quality.
*
* @extends Component
*/
export class AbstractConference<P: AbstractProps, S>
extends Component<P, S> {
/**
* 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} * Maps (parts of) the redux state to the associated props of the {@link Labels}
* {@code Component}. * {@code Component}.
@ -34,6 +75,7 @@ export type AbstractProps = {
*/ */
export function abstractMapStateToProps(state: Object) { export function abstractMapStateToProps(state: Object) {
return { return {
_notificationsVisible: shouldDisplayNotifications(state),
_room: state['features/base/conference'].room, _room: state['features/base/conference'].room,
_shouldDisplayTileView: shouldDisplayTileView(state) _shouldDisplayTileView: shouldDisplayTileView(state)
}; };

View File

@ -1,8 +1,8 @@
// @flow // @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 { connect as reactReduxConnect } from 'react-redux';
import { appNavigate } from '../../../app'; import { appNavigate } from '../../../app';
@ -10,6 +10,7 @@ import { connect, disconnect } from '../../../base/connection';
import { getParticipantCount } from '../../../base/participants'; import { getParticipantCount } from '../../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../../base/react'; import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { import {
isNarrowAspectRatio,
makeAspectRatioAware makeAspectRatioAware
} from '../../../base/responsive-ui'; } from '../../../base/responsive-ui';
import { TestConnectionInfo } from '../../../base/testing'; import { TestConnectionInfo } from '../../../base/testing';
@ -17,6 +18,7 @@ import { createDesiredLocalTracks } from '../../../base/tracks';
import { ConferenceNotification } from '../../../calendar-sync'; import { ConferenceNotification } from '../../../calendar-sync';
import { Chat } from '../../../chat'; import { Chat } from '../../../chat';
import { import {
FILMSTRIP_SIZE,
Filmstrip, Filmstrip,
isFilmstripVisible, isFilmstripVisible,
TileView TileView
@ -26,7 +28,10 @@ import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { Captions } from '../../../subtitles'; import { Captions } from '../../../subtitles';
import { setToolboxVisible, Toolbox } from '../../../toolbox'; import { setToolboxVisible, Toolbox } from '../../../toolbox';
import { abstractMapStateToProps } from '../AbstractConference'; import {
AbstractConference,
abstractMapStateToProps
} from '../AbstractConference';
import DisplayNameLabel from './DisplayNameLabel'; import DisplayNameLabel from './DisplayNameLabel';
import Labels from './Labels'; import Labels from './Labels';
import NavigationBar from './NavigationBar'; import NavigationBar from './NavigationBar';
@ -134,7 +139,7 @@ type Props = AbstractProps & {
/** /**
* The conference page of the mobile (i.e. React Native) application. * The conference page of the mobile (i.e. React Native) application.
*/ */
class Conference extends Component<Props> { class Conference extends AbstractConference<Props, *> {
/** /**
* Initializes a new Conference instance. * Initializes a new Conference instance.
* *
@ -296,7 +301,12 @@ class Conference extends Component<Props> {
} }
</View> </View>
<NavigationBar /> <SafeAreaView
pointerEvents = 'box-none'
style = { styles.navBarSafeView }>
<NavigationBar />
{ this.renderNotificationsContainer() }
</SafeAreaView>
<TestConnectionInfo /> <TestConnectionInfo />
@ -341,6 +351,37 @@ class Conference extends Component<Props> {
? <ConferenceNotification /> ? <ConferenceNotification />
: undefined); : 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
}
);
}
} }
/** /**

View File

@ -21,6 +21,7 @@ import AbstractLabels, {
_abstractMapStateToProps, _abstractMapStateToProps,
type Props as AbstractLabelsProps type Props as AbstractLabelsProps
} from '../AbstractLabels'; } from '../AbstractLabels';
import { shouldDisplayNotifications } from '../../functions';
import styles from './styles'; import styles from './styles';
/** /**
@ -363,7 +364,9 @@ function _mapStateToProps(state) {
return { return {
..._abstractMapStateToProps(state), ..._abstractMapStateToProps(state),
_reducedUI: state['features/base/responsive-ui'].reducedUI, _reducedUI: state['features/base/responsive-ui'].reducedUI,
_visible: !isToolboxVisible(state) && !shouldDisplayTileView(state) _visible: !isToolboxVisible(state)
&& !shouldDisplayTileView(state)
&& !shouldDisplayNotifications(state)
}; };
} }

View File

@ -40,39 +40,33 @@ class NavigationBar extends Component<Props> {
return null; return null;
} }
return ( return [
<View <LinearGradient
pointerEvents = 'box-none' colors = { NAVBAR_GRADIENT_COLORS }
style = { styles.navBarContainer }> key = { 1 }
<LinearGradient pointerEvents = 'none'
colors = { NAVBAR_GRADIENT_COLORS } style = { styles.gradient }>
pointerEvents = 'none' <SafeAreaView>
style = { styles.gradient }> <View style = { styles.gradientStretch } />
<SafeAreaView>
<View style = { styles.gradientStretch } />
</SafeAreaView>
</LinearGradient>
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.navBarSafeView }>
<View
pointerEvents = 'box-none'
style = { styles.navBarWrapper }>
<PictureInPictureButton
styles = { styles.navBarButton } />
<View
pointerEvents = 'box-none'
style = { styles.roomNameWrapper }>
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{ this.props._meetingName }
</Text>
</View>
</View>
</SafeAreaView> </SafeAreaView>
</LinearGradient>,
<View
key = { 2 }
pointerEvents = 'box-none'
style = { styles.navBarWrapper }>
<PictureInPictureButton
styles = { styles.navBarButton } />
<View
pointerEvents = 'box-none'
style = { styles.roomNameWrapper }>
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{ this.props._meetingName }
</Text>
</View>
</View> </View>
); ];
} }
} }

View File

@ -37,6 +37,10 @@ export default createStyleSheet({
}, },
gradient: { gradient: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
flex: 1 flex: 1
}, },

View File

@ -1,7 +1,7 @@
// @flow // @flow
import _ from 'lodash'; import _ from 'lodash';
import React, { Component } from 'react'; import React from 'react';
import { connect as reactReduxConnect } from 'react-redux'; import { connect as reactReduxConnect } from 'react-redux';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout'; import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
@ -13,7 +13,6 @@ import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip'; import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite'; import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video'; import { LargeVideo } from '../../../large-video';
import { NotificationsContainer } from '../../../notifications';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { import {
@ -28,7 +27,10 @@ import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import Labels from './Labels'; import Labels from './Labels';
import { default as Notice } from './Notice'; import { default as Notice } from './Notice';
import { default as Subject } from './Subject'; import { default as Subject } from './Subject';
import { abstractMapStateToProps } from '../AbstractConference'; import {
AbstractConference,
abstractMapStateToProps
} from '../AbstractConference';
import type { AbstractProps } from '../AbstractConference'; import type { AbstractProps } from '../AbstractConference';
@ -87,7 +89,7 @@ type Props = AbstractProps & {
/** /**
* The conference page of the Web application. * The conference page of the Web application.
*/ */
class Conference extends Component<Props> { class Conference extends AbstractConference<Props, *> {
_onFullScreenChange: Function; _onFullScreenChange: Function;
_onShowToolbar: Function; _onShowToolbar: Function;
_originalOnShowToolbar: Function; _originalOnShowToolbar: Function;
@ -218,7 +220,7 @@ class Conference extends Component<Props> {
{ filmstripOnly || <Toolbox /> } { filmstripOnly || <Toolbox /> }
{ filmstripOnly || <Chat /> } { filmstripOnly || <Chat /> }
<NotificationsContainer /> { this.renderNotificationsContainer() }
<CalleeInfoContainer /> <CalleeInfoContainer />
</div> </div>

View File

@ -1,7 +1,13 @@
import { getName } from '../app';
import { translateToHTML } from '../base/i18n'; import { translateToHTML } from '../base/i18n';
import { browser } from '../base/lib-jitsi-meet'; 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. * 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;
}

View File

@ -2,9 +2,8 @@
import { Component } from 'react'; import { Component } from 'react';
import { getOverlayToRender } from '../../overlay';
import { hideNotification } from '../actions'; import { hideNotification } from '../actions';
import { areThereNotifications } from '../functions';
export type Props = { export type Props = {
@ -165,12 +164,10 @@ export default class AbstractNotificationsContainer<P: Props>
* }} * }}
*/ */
export function _abstractMapStateToProps(state: Object) { export function _abstractMapStateToProps(state: Object) {
const isAnyOverlayVisible = Boolean(getOverlayToRender(state)); const { notifications } = state['features/notifications'];
const { enabled, notifications } = state['features/notifications']; const _visible = areThereNotifications(state);
const { calleeInfoVisible } = state['features/invite'];
return { return {
_notifications: enabled && !isAnyOverlayVisible && !calleeInfoVisible _notifications: _visible ? notifications : []
? notifications : []
}; };
} }

View File

@ -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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
isDismissAllowed
} = this.props;
return (
<View
pointerEvents = 'box-none'
style = { styles.notification }>
<View style = { styles.contentColumn }>
<View
pointerEvents = 'box-none'
style = { styles.notificationContent }>
{
this._renderContent()
}
</View>
</View>
{
isDismissAllowed
&& <TouchableOpacity onPress = { this._onDismissed }>
<Icon
name = { 'close' }
style = { styles.dismissIcon } />
</TouchableOpacity>
}
</View>
);
}
/**
* 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<ReactElement>}
* @private
*/
_renderContent() {
const { t, title, titleArguments, titleKey } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
if (titleText) {
return (
<Text
numberOfLines = { 1 }
style = { styles.contentText } >
{ titleText }
</Text>
);
}
return this._getDescription().map((line, index) => (
<Text
key = { index }
numberOfLines = { 1 }
style = { styles.contentText }>
{ line }
</Text>
));
}
_getDescription: () => Array<string>;
_onDismissed: () => void;
}
export default translate(Notification);

View File

@ -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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { _notifications } = this.props;
if (!_notifications || !_notifications.length) {
return null;
}
return (
<View
pointerEvents = 'box-none'
style = { [
styles.notificationContainer,
this.props.style
] } >
{
_notifications.map(
({ props, uid }) => (
<Notification
{ ...props }
key = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />))
}
</View>
);
}
_onDismissed: number => void;
}
export default connect(_abstractMapStateToProps)(NotificationsContainer);

View File

@ -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'
}
});

View File

@ -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;
}

View File

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