diff --git a/images/watermark.png b/images/watermark.png index 3c4ae443d6ba..e356b2aee15f 100644 Binary files a/images/watermark.png and b/images/watermark.png differ diff --git a/react/features/base/react/components/native/WaitingMessage.js b/react/features/base/react/components/native/WaitingMessage.js new file mode 100644 index 000000000000..0f478f22f465 --- /dev/null +++ b/react/features/base/react/components/native/WaitingMessage.js @@ -0,0 +1,186 @@ +// @flow +/* eslint-disable */ +import React, { Component } from 'react/index'; +import { Animated, Easing, Text, SafeAreaView, Image } from 'react-native'; +import styles from './styles'; +import { getLocalizedDateFormatter, translate } from '../../../i18n'; +import { connect } from '../../../redux'; +import { getParticipantCount } from '../../../participants'; +import { getRemoteTracks } from '../../../tracks'; +import jwtDecode from 'jwt-decode'; +import View from 'react-native-webrtc/RTCView'; +import moment from 'moment'; + +const watermarkImg = require('../../../../../../images/watermark.png'); + +type Props = { + _isGuest: boolean, + jwt: Object, + conferenceHasStarted: boolean, + stopAnimation: boolean, + waitingMessageFromProps: string +}; + +type State = { + beforeAppointmentStart: boolean, + appointmentStartAt: string +}; + +class WaitingMessage extends Component { + + _interval; + + constructor(props: Props) { + super(props); + + this.state = { + beforeAppointmentStart: false, + appointmentStartAt: '', + fadeAnim: new Animated.Value(0) + }; + this.animatedValue = new Animated.Value(0); + } + + componentDidMount() { + this._startTimer(); + this._animate(); + } + + _animate() { + this.animatedValue.setValue(0); + Animated.timing( + this.animatedValue, + { + toValue: 1, + duration: 2000, + easing: Easing.linear + } + ) + .start(() => this._animate()); + } + + _startTimer() { + const { jwt, conferenceHasStarted } = this.props; + const jwtPayload = jwt && jwtDecode(jwt); + if (jwtPayload && jwtPayload.context && !conferenceHasStarted) { + const { start_at } = jwtPayload.context || 0; + const appointmentStartTimeStamp = moment(start_at, 'YYYY-MM-DD HH:mm:ss') + .valueOf(); + const now = new Date().getTime(); + if (now < appointmentStartTimeStamp) { + this.setState({ + beforeAppointmentStart: true, + appointmentStartAt: start_at + }, () => { + this._setInterval(appointmentStartTimeStamp); + }); + } + } + } + + _setInterval(appointmentStartTimeStamp) { + this._interval = setInterval(() => { + const { conferenceHasStarted } = this.props; + const now = new Date().getTime(); + + if ((appointmentStartTimeStamp < now) || conferenceHasStarted) { + this.setState({ + beforeAppointmentStart: false + }, () => { + this._stopTimer(); + }); + } + }, 1000); + } + + _stopTimer() { + if (this._interval) { + clearInterval(this._interval); + } + } + + getWaitingMessage() { + const { waitingMessageFromProps } = this.props; + const { beforeAppointmentStart, appointmentStartAt } = this.state; + let header, text; + + header = waitingMessageFromProps ? waitingMessageFromProps.header : 'Waiting for the other participant to join...'; + + text = waitingMessageFromProps ? waitingMessageFromProps.text : 'Sit back, relax and take a moment for yourself.'; + + if (beforeAppointmentStart && appointmentStartAt && !waitingMessageFromProps) { + const time = moment(appointmentStartAt, 'YYYY-MM-DD HH:mm') + .format('YYYY-MM-DD HH:mm'); + header = `Your appointment will begin at ${getLocalizedDateFormatter(time) + .format('hh:mm A')}`; + } + + if (this._isTestMode()) { + header = 'Testing your audio and video...'; + text = 'This is just a test area. Begin your online appointment from your Upcoming Appointments page.'; + } + + return + + { + header + } + + + { + text + } + + ; + } + + _isTestMode() { + const { jwt } = this.props; + const jwtPayload = jwt && jwtDecode(jwt) || null; + const participantId = jwtPayload && jwtPayload.context && jwtPayload.context.user && jwtPayload.context.user.participant_id; + const videoChatSessionId = jwtPayload && jwtPayload.context && jwtPayload.context.video_chat_session_id; + const participantEmail = jwtPayload && jwtPayload.context && jwtPayload.context.user && jwtPayload.context.user.email; + + return jwtPayload && participantId === 0 && videoChatSessionId === 0 && participantEmail === 'test@test.com'; + } + + render() { + const { stopAnimation, conferenceHasStarted } = this.props; + const animate = (stopAnimation || conferenceHasStarted) ? null : this.animatedValue.interpolate({ + inputRange: [ 0, .5, 1 ], + outputRange: [ .1, 1, .1 ] + }); + + const image = ; + + return + + + { + image + } + + { + !conferenceHasStarted && this.getWaitingMessage() + } + + ; + } +} + +function _mapStateToProps(state) { + const { jwt } = state['features/base/jwt']; + const participantCount = getParticipantCount(state); + const remoteTracks = getRemoteTracks(state['features/base/tracks']); + + return { + jwt, + conferenceHasStarted: participantCount > 1 && remoteTracks.length > 0 + }; +} + +export default connect(_mapStateToProps)(translate(WaitingMessage)); diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js index 2847cb414a81..8658e2edb2a8 100644 --- a/react/features/base/react/components/native/styles.js +++ b/react/features/base/react/components/native/styles.js @@ -203,6 +203,44 @@ const SECTION_LIST_STYLES = { } }; +const WATING_MESSAGE_STYLES = { + waitingMessageContainer: { + position: 'absolute', + top: 80, + width: '100%', + flexDirection: 'row', + backgroundColor: 'transparent' + }, + + waitingMessageImage: { + marginRight: 5, + }, + + waitingMessageHeader: { + fontSize: 15, + marginTop: 5, + color: ColorPalette.janeDarkColor, + textAlign: 'left', + backgroundColor: 'transparent' + }, + + waitingMessageText: { + marginTop: 5, + maxWidth: 300, + fontSize: 12, + color: ColorPalette.janeDarkColor, + textAlign: 'left', + backgroundColor: 'transparent' + }, + + watermark: { + width: 60, + height: 50, + marginLeft: 16, + marginRight: 8 + } +}; + export const TINTED_VIEW_DEFAULT = { backgroundColor: ColorPalette.appBackground, opacity: 0.8 @@ -214,5 +252,6 @@ export const TINTED_VIEW_DEFAULT = { */ export default { ...PAGED_LIST_STYLES, - ...SECTION_LIST_STYLES + ...SECTION_LIST_STYLES, + ...WATING_MESSAGE_STYLES }; diff --git a/react/features/base/styles/components/styles/ColorPalette.js b/react/features/base/styles/components/styles/ColorPalette.js index c20339c155b8..b3d8d402d84c 100644 --- a/react/features/base/styles/components/styles/ColorPalette.js +++ b/react/features/base/styles/components/styles/ColorPalette.js @@ -3,6 +3,7 @@ */ const BLACK = '#111111'; +const JANE_DARK_COLOR = '#009097'; /** * The application's color palette. */ @@ -44,4 +45,5 @@ export const ColorPalette = { /** Jane */ jane: '#00c1ca', + janeDarkColor: JANE_DARK_COLOR }; diff --git a/react/features/base/tracks/functions.js b/react/features/base/tracks/functions.js index 0810da23c76b..5e30e8b3ddc8 100644 --- a/react/features/base/tracks/functions.js +++ b/react/features/base/tracks/functions.js @@ -178,6 +178,29 @@ export function getLocalTracks(tracks, includePending = false) { return tracks.filter(t => t.local && (t.jitsiTrack || includePending)); } +/** + * Returns an array containing the remote tracks with or without a (valid) + * {@code JitsiTrack}. + * + * @param {Track[]} tracks - An array containing all remote tracks. + * @param {boolean} [includePending] - Indicates whether a remote track is to be + * returned if it is still pending. A remote track is pending if + * {@code getUserMedia} is still executing to create it and, consequently, its + * {@code jitsiTrack} property is {@code undefined}. By default a pending remote + * track is not returned. + * @returns {Track[]} + */ +export function getRemoteTracks(tracks, includePending = false) { + // XXX A remote track is considered ready only once it has its `jitsiTrack` + // property set by the `TRACK_ADDED` action. Until then there is a stub + // added just before the `getUserMedia` call with a cancellable + // `gumInProgress` property which then can be used to destroy the track that + // has not yet been added to the redux store. Once GUM is cancelled, it will + // never make it to the store nor there will be any + // `TRACK_ADDED`/`TRACK_REMOVED` actions dispatched for it. + return tracks.filter(t => !t.local && (t.jitsiTrack || includePending)); +} + /** * Returns local video track. * diff --git a/react/features/filmstrip/components/native/TileView.js b/react/features/filmstrip/components/native/TileView.js index 4171bf67b837..b606d53b6d85 100644 --- a/react/features/filmstrip/components/native/TileView.js +++ b/react/features/filmstrip/components/native/TileView.js @@ -21,6 +21,8 @@ import { import Thumbnail from './Thumbnail'; import styles from './styles'; +import WaitingMessage + from '../../../base/react/components/native/WaitingMessage'; /** * The type of the React {@link Component} props of {@link TileView}. @@ -134,6 +136,7 @@ class TileView extends Component { return ( + { useConnectivityInfoLabel = { useConnectivityInfoLabel } zOrder = { 0 } zoomEnabled = { true } /> + ); }