From ae0bf876a86237afea34abec7e81193c301481b7 Mon Sep 17 00:00:00 2001 From: zbettenbuk Date: Mon, 12 Feb 2018 09:53:23 -0600 Subject: [PATCH] Add conference notification --- css/_font.scss | 3 + fonts/jitsi.eot | Bin 9452 -> 9500 bytes fonts/jitsi.svg | 1 + fonts/jitsi.ttf | Bin 9296 -> 9344 bytes fonts/jitsi.woff | Bin 9372 -> 9420 bytes fonts/selection.json | 137 +++++---- lang/main.json | 1 + react/features/base/font-icons/jitsi.json | 137 +++++---- .../ConferenceNotification.native.js | 284 ++++++++++++++++++ .../calendar-sync/components/index.js | 1 + .../calendar-sync/components/styles.js | 106 ++++++- react/features/calendar-sync/middleware.js | 18 ++ .../components/Conference.native.js | 3 + .../components/AbstractRecentList.js | 12 +- .../components/RecentList.native.js | 4 +- 15 files changed, 587 insertions(+), 120 deletions(-) create mode 100644 react/features/calendar-sync/components/ConferenceNotification.native.js diff --git a/css/_font.scss b/css/_font.scss index f1a214d4a..9c1a3c300 100644 --- a/css/_font.scss +++ b/css/_font.scss @@ -36,6 +36,9 @@ .icon-navigate_before:before { content: "\e408"; } +.icon-navigate_next:before { + content: "\e409"; +} .icon-public:before { content: "\e80b"; } diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index a5ac4116024524b4bd5a1ad50e68e403741527bd..7984214cd13cf5f4f4c56174e496c0e3eff5e354 100755 GIT binary patch delta 444 zcmaFkIme4tMwNk~VIr$J%VBHwO%omR1+OwNFzf;1gyh`Bg1BPd4HFN@)Gu%Flw@FF ze8IpVual9Qm?CgoWhn!Lq6ko(Sq4ymBZXxnknaQJt7PPsR6Nv@3I*~DfczIZ`N@el zRW8LbFepU;`DM9@6$K2g42OaI3qZa?USe+Q-3hhJf&33ZJMs$hi%S@ofies#2_Sg} zW)?=)$uW$R8966^VDuM%D&;O^C1okaE%`_Cx8zp|4sl_ia>mJtOfr)Tn7B9BG8r(b zFf!&bGBB+I8pF!Kz{#X;uFklMk>Njs&5>%;NtEvlRq%}i$9Tald_bukm8d3E%{6Giv+v45KuYeh;Z#9P9 + diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 7c0742d18df1ef8f346412ee39ee3abb670c6c61..bd462b4cd8a4ec943b3be7e706d9b041a1df2a4f 100755 GIT binary patch delta 469 zcmccM(coFnz{tSBz|GLWz|3IaAFOZ0caiNXP-G7fCnV=47Q_|vZeU$iDd@qXCl&BV!&T1Jf#?A*>7xoJ{KG>Wr%x8U8ca967=aq`-{Lye!XU zWHzx|ACBj@`O3h}0vKz{tSBz|GLWz|3IaAFOZ0caiNHP-G7fCnV=47R2T8u3}(dlmYUOq$d^^ z0BHdr{{fKZNYANE4x zWaO4q+|`nJ4&-|P`A>54lN}S!-@0~yfk8>!f+VKUjXDQTZcQ0yMlWO_Zyx(o=H3>cx8C?c%67lcsKA#@y+0O a;BNv)fF;901}?Ben1GHI-uzjqmk|I#T4=EV diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index 4cea3313334de3b9866f7b15281f5e35eb335ff3..fbcd0f7068fd1442ea8d4ab2ae68131e0d396b85 100755 GIT binary patch delta 513 zcmbQ^dB#($+~3WOfsp|SRL(GPgXsna#>s+AViR?Q>#ru~CKfO-FlGQHj(~7nG4F=- z#A2WrlMay20mTC8IhAQZF%t#`d7!ww&hiFN$&A#*6b1&x1fUu-5EeMDvNQuI2o#$E z^Cy<77t8$qN|$#h*&KOIb-- zN^wj6k^C+BRf0oYn1LDWZ<)yoOx&AwnGBdz7#Z^z8JJc9b+a-sa5AZzt23@*Wcbfu zbL0p!kODI{N3uMZk=evngXsVU#>s+AViR?Q>#rr}CKfO-FlGQHj(~7n9`CC3 z#A2WrlMay20mTC8IhAQZF%t#`c@_}n;S?^)NKH&(U{JIGsxbp$f%)v~Gk}6XF`#}1 z6%gh~VcC$8TT%fOn*!uN0pYt^63=t;lY#2|lzr0G+ z(UV&g%^58x?@?5n{7mt3ejVd9#tn?mm{geASOi$SST?cBu~xC3Vv}JrVC!Mqz;=n9 ziM@$^9{UmYHym~xQ#c-RYH_x4-r, + + /** + * The translate function. + */ + t: Function +}; + +type State = { + + /** + * The event object to display the notification for. + */ + event?: Object +}; + +/** + * Component to display a permanent badge-like notification on the + * conference screen when another meeting is about to start. + */ +class ConferenceNotification extends Component { + updateIntervalId: number; + + /** + * Constructor of the ConferenceNotification component. + * + * @inheritdoc + */ + constructor(props) { + super(props); + + this.state = { + event: undefined + }; + + this._getNotificationContentStyle + = this._getNotificationContentStyle.bind(this); + this._getNotificationPosition + = this._getNotificationPosition.bind(this); + this._maybeDisplayNotification + = this._maybeDisplayNotification.bind(this); + this._onGoToNext = this._onGoToNext.bind(this); + } + + /** + * Implements React Component's componentDidMount. + * + * @inheritdoc + */ + componentDidMount() { + this.updateIntervalId = setInterval( + this._maybeDisplayNotification, + 10 * 1000 + ); + } + + /** + * Implements React Component's componentWillUnmount. + * + * @inheritdoc + */ + componentWillUnmount() { + clearTimeout(this.updateIntervalId); + } + + /** + * Implements the React Components's render method. + * + * @inheritdoc + */ + render() { + const { event } = this.state; + const { t } = this.props; + + if (event) { + return ( + + + + + + + { t('calendarSync.nextMeeting') } + + + { + getLocalizedDateFormatter( + event.startDate + ).fromNow() + } + + + + + + + + + + ); + } + + return null; + } + + _getNotificationContentStyle: () => Array + + /** + * Decides the color of the notification and some additional + * styles based on notificationPosition. + * + * @private + * @returns {Array} + */ + _getNotificationContentStyle() { + const { event } = this.state; + const { _aspectRatio } = this.props; + const now = Date.now(); + const style = [ + styles.notificationContent + ]; + + if (event && event.startDate < now && event.endDate > now) { + style.push(styles.notificationContentPast); + } else { + style.push(styles.notificationContentNext); + } + + if (_aspectRatio === ASPECT_RATIO_NARROW) { + style.push(styles.notificationContentSide); + } else { + style.push(styles.notificationContentTop); + } + + return style; + } + + _getNotificationPosition: () => Object; + + /** + * Decides the position of the notification. + * + * @private + * @returns {Object} + */ + _getNotificationPosition() { + const { _aspectRatio } = this.props; + + if (_aspectRatio === ASPECT_RATIO_NARROW) { + return styles.notificationContainerSide; + } + + return styles.notificationContainerTop; + } + + _maybeDisplayNotification: () => void; + + /** + * Periodically checks if there is an event in the calendar for which we + * need to show a notification. + * + * @private + * @returns {void} + */ + _maybeDisplayNotification() { + const { _currentConferenceURL, _eventList } = this.props; + let eventToShow; + + if (_eventList && _eventList.length) { + const now = Date.now(); + + for (const event of _eventList) { + if (event.url !== _currentConferenceURL) { + if ((!eventToShow + && event.startDate > now + && event.startDate < now + ALERT_MILLISECONDS) + || (event.startDate < now && event.endDate > now) + ) { + eventToShow = event; + } + } + } + } + + this.setState({ + event: eventToShow + }); + } + + _onGoToNext: () => void; + + /** + * Opens the meeting URL that the notification shows. + * + * @private + * @param {string} url - The URL to open. + * @returns {void} + */ + _onGoToNext() { + const { event } = this.state; + + if (event && event.url) { + this.props.dispatch(appNavigate(event.url)); + } + } + +} + +/** + * Maps redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _aspectRatio: Symbol, + * _currentConferenceURL: string, + * _eventList: Array + * }} + */ +export function _mapStateToProps(state: Object) { + const { locationURL } = state['features/base/connection']; + + return { + _aspectRatio: state['features/base/responsive-ui'].aspectRatio, + _currentConferenceURL: + locationURL ? getURLWithoutParams(locationURL)._url : '', + _eventList: state['features/calendar-sync'].events + }; +} + +export default translate(connect(_mapStateToProps)(ConferenceNotification)); diff --git a/react/features/calendar-sync/components/index.js b/react/features/calendar-sync/components/index.js index a1e8409ca..6f467d016 100644 --- a/react/features/calendar-sync/components/index.js +++ b/react/features/calendar-sync/components/index.js @@ -1 +1,2 @@ export { default as MeetingList } from './MeetingList'; +export { default as ConferenceNotification } from './ConferenceNotification'; diff --git a/react/features/calendar-sync/components/styles.js b/react/features/calendar-sync/components/styles.js index 0df69074b..09dc07dfc 100644 --- a/react/features/calendar-sync/components/styles.js +++ b/react/features/calendar-sync/components/styles.js @@ -1,9 +1,8 @@ import { createStyleSheet } from '../../base/styles'; const AVATAR_OPACITY = 0.4; - const AVATAR_SIZE = 65; - +const NOTIFICATION_SIZE = 55; const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; @@ -50,6 +49,105 @@ export default createStyleSheet({ textAlign: 'center' }, + /** + * Style for the actual notification content. + */ + notificationContainer: { + flexDirection: 'row', + justifyContent: 'center', + overflow: 'hidden', + position: 'absolute' + }, + + /** + * Additional style for the container when the notification is displayed + * on the side (narrow view). + */ + notificationContainerSide: { + top: 100 + }, + + /** + * Additional style for the container when the notification is displayed + * on the top (wide view). + */ + notificationContainerTop: { + justifyContent: 'center', + left: 0, + right: 0, + top: 0 + }, + + /** + * The top level container of the notification. + */ + notificationContent: { + alignSelf: 'flex-start', + flexDirection: 'row', + height: NOTIFICATION_SIZE, + justifyContent: 'center', + paddingHorizontal: 10 + }, + + /** + * Color for upcoming meeting notification. + */ + notificationContentNext: { + backgroundColor: '#eeb231' + }, + + /** + * Color for already ongoing meeting notifications. + */ + notificationContentPast: { + backgroundColor: 'red' + }, + + /** + * Additional style for the content when the notification is displayed + * on the side (narrow view). + */ + notificationContentSide: { + borderBottomRightRadius: NOTIFICATION_SIZE, + borderTopRightRadius: NOTIFICATION_SIZE + }, + + /** + * Additional style for the content when the notification is displayed + * on the top (wide view). + */ + notificationContentTop: { + borderBottomLeftRadius: NOTIFICATION_SIZE / 2, + borderBottomRightRadius: NOTIFICATION_SIZE / 2, + paddingHorizontal: 20 + }, + + /** + * The icon of the notification. + */ + notificationIcon: { + color: 'white', + fontSize: 25 + }, + + notificationIconContainer: { + alignItems: 'center', + flexDirection: 'row', + height: NOTIFICATION_SIZE, + justifyContent: 'center' + }, + + notificationText: { + color: 'white', + fontSize: 13 + }, + + notificationTextContainer: { + flexDirection: 'column', + height: NOTIFICATION_SIZE, + justifyContent: 'center' + }, + /** * The top level container style of the list. */ @@ -105,5 +203,9 @@ export default createStyleSheet({ color: OVERLAY_FONT_COLOR, fontSize: 14, fontWeight: 'normal' + }, + + touchableView: { + flexDirection: 'row' } }); diff --git a/react/features/calendar-sync/middleware.js b/react/features/calendar-sync/middleware.js index e11ddcd46..82b345274 100644 --- a/react/features/calendar-sync/middleware.js +++ b/react/features/calendar-sync/middleware.js @@ -117,6 +117,24 @@ function _fetchCalendarEntries(store) { } } + // TEST events to check notification popup. + // TODO: Remove this before a PR. + eventList.push({ + endDate: Date.now() + (60 * 60 * 1000), + id: -1, + startDate: Date.now() + (80 * 1000), + title: 'ShipIt 41', + url: 'https://meet.jit.si/shipit41' + }); + + eventList.push({ + endDate: Date.now() + (2 * 60 * 60 * 1000), + id: -2, + startDate: Date.now() + (60 * 60 * 1000), + title: 'ShipIt 41 demo', + url: 'https://meet.jit.si/shipit41' + }); + store.dispatch(updateCalendarEntryList(eventList.sort((a, b) => a.startDate - b.startDate ).slice(0, MAX_LIST_LENGTH))); diff --git a/react/features/conference/components/Conference.native.js b/react/features/conference/components/Conference.native.js index c900669fb..be26ee8bc 100644 --- a/react/features/conference/components/Conference.native.js +++ b/react/features/conference/components/Conference.native.js @@ -12,6 +12,7 @@ import { DialogContainer } from '../../base/dialog'; import { CalleeInfoContainer } from '../../base/jwt'; import { Container, LoadingIndicator, TintedView } from '../../base/react'; import { createDesiredLocalTracks } from '../../base/tracks'; +import { ConferenceNotification } from '../../calendar-sync'; import { Filmstrip } from '../../filmstrip'; import { LargeVideo } from '../../large-video'; import { setToolboxVisible, Toolbox } from '../../toolbox'; @@ -233,6 +234,8 @@ class Conference extends Component { + + {/* * The dialogs are in the topmost stacking layers. */ diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js index e33eeb0a8..bd8972c78 100644 --- a/react/features/recent-list/components/AbstractRecentList.js +++ b/react/features/recent-list/components/AbstractRecentList.js @@ -13,14 +13,14 @@ type Props = { _recentList: Array, /** - * The redux store's {@code dispatch} function. + * Indicates if the list is disabled or not. */ - dispatch: Dispatch<*>, + disabled: boolean, /** - * Whether {@code AbstractRecentList} is enabled. + * The redux store's {@code dispatch} function. */ - enabled: boolean + dispatch: Dispatch<*> }; /** @@ -40,9 +40,9 @@ export default class AbstractRecentList extends Component { * @returns {void} */ _onJoin(room) { - const { dispatch, enabled } = this.props; + const { dispatch, disabled } = this.props; - enabled && room && dispatch(appNavigate(room)); + !disabled && room && dispatch(appNavigate(room)); } /** diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js index 856700b8a..a568adf9e 100644 --- a/react/features/recent-list/components/RecentList.native.js +++ b/react/features/recent-list/components/RecentList.native.js @@ -53,7 +53,7 @@ class RecentList extends AbstractRecentList { * @returns {ReactElement} */ render() { - const { enabled, _recentList } = this.props; + const { disabled, _recentList } = this.props; if (!_recentList) { return null; @@ -66,7 +66,7 @@ class RecentList extends AbstractRecentList {