From 781ff323db4c70e4ca476f9ef13a04e5aa063585 Mon Sep 17 00:00:00 2001 From: enahum Date: Fri, 16 Sep 2016 15:35:13 -0300 Subject: Webrtc client side (#4026) * WebRTC Server side * WebRTC client side * Bug fixes and improvements * Pushing UI improvements for webrtc (#3728) * Pushing UI improvements for webrtc * Updating webrtc css * PLT-3943 WebRTC P1: bug fixes and improvements * Video resolution set to std, reduce volume of ringtone and flip video horizontally * Fix calling a user B while WebRTC RHS is still opened * Leave RHS opened when call ends, Fix isBusy on popover and channel_header * Fix pre-release feature, RHS & System Console * PLT-3945 - Updating UI for webrtc (#3908) * PLT-3943 Webrtc p1 * Add ongoing call indicator when RHS is opened * UI updates to to webrtc notifcation (#3959) --- webapp/components/admin_console/admin_sidebar.jsx | 2 +- .../components/admin_console/webrtc_settings.jsx | 46 +- webapp/components/channel_header.jsx | 98 +- webapp/components/leave_team_modal.jsx | 16 +- webapp/components/loading_screen.jsx | 19 +- webapp/components/navbar_dropdown.jsx | 24 +- webapp/components/needs_team.jsx | 5 + webapp/components/post_view/components/post.jsx | 9 +- .../post_view/components/post_header.jsx | 6 +- .../components/post_view/components/post_list.jsx | 4 +- .../post_view/post_focus_view_controller.jsx | 14 +- .../components/post_view/post_view_controller.jsx | 14 + webapp/components/rhs_header_post.jsx | 14 +- webapp/components/rhs_thread.jsx | 2 + webapp/components/sidebar_right.jsx | 5 +- webapp/components/sidebar_right_menu.jsx | 12 +- webapp/components/user_profile.jsx | 105 +- .../user_settings/user_settings_advanced.jsx | 16 +- .../components/webrtc/components/webrtc_header.jsx | 100 ++ .../webrtc/components/webrtc_notification.jsx | 320 ++++++ .../webrtc/components/webrtc_sidebar.jsx | 133 +++ webapp/components/webrtc/webrtc_controller.jsx | 1214 ++++++++++++++++++++ 22 files changed, 2135 insertions(+), 43 deletions(-) create mode 100644 webapp/components/webrtc/components/webrtc_header.jsx create mode 100644 webapp/components/webrtc/components/webrtc_notification.jsx create mode 100644 webapp/components/webrtc/components/webrtc_sidebar.jsx create mode 100644 webapp/components/webrtc/webrtc_controller.jsx (limited to 'webapp/components') diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 1e74df05f..8600e1e8c 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -264,7 +264,7 @@ export default class AdminSidebar extends React.Component { title={ } /> diff --git a/webapp/components/admin_console/webrtc_settings.jsx b/webapp/components/admin_console/webrtc_settings.jsx index eac075bfa..19423cf56 100644 --- a/webapp/components/admin_console/webrtc_settings.jsx +++ b/webapp/components/admin_console/webrtc_settings.jsx @@ -15,10 +15,23 @@ export default class WebrtcSettings extends AdminSettings { constructor(props) { super(props); + this.canSave = this.canSave.bind(this); + this.handleAgreeChange = this.handleAgreeChange.bind(this); + this.getConfigFromState = this.getConfigFromState.bind(this); this.renderSettings = this.renderSettings.bind(this); } + canSave() { + return !this.state.enableWebrtc || this.state.agree; + } + + handleAgreeChange(e) { + this.setState({ + agree: e.target.checked + }); + } + getConfigFromState(config) { config.WebrtcSettings.Enable = this.state.enableWebrtc; config.WebrtcSettings.GatewayWebsocketUrl = this.state.gatewayWebsocketUrl; @@ -44,7 +57,8 @@ export default class WebrtcSettings extends AdminSettings { stunURI: settings.StunURI, turnURI: settings.TurnURI, turnUsername: settings.TurnUsername, - turnSharedKey: settings.TurnSharedKey + turnSharedKey: settings.TurnSharedKey, + agree: settings.Enable }; } @@ -53,13 +67,32 @@ export default class WebrtcSettings extends AdminSettings {

); } renderSettings() { + const tosCheckbox = ( +
+
+
+ + +
+
+ ); + return ( + {tosCheckbox} :/admin. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. Mattermost WebRTC uses this URL to obtain valid tokens for each peer to establish the connection.' /> } @@ -167,7 +201,7 @@ export default class WebrtcSettings extends AdminSettings { } placeholder={Utils.localizeMessage('admin.webrtc.turnUriExample', 'Ex "turn:webrtc.mattermost.com:5349"')} helpText={ - @@ -186,7 +220,7 @@ export default class WebrtcSettings extends AdminSettings { } placeholder={Utils.localizeMessage('admin.webrtc.turnUsernameExample', 'Ex "myusername"')} helpText={ - @@ -205,7 +239,7 @@ export default class WebrtcSettings extends AdminSettings { } placeholder={Utils.localizeMessage('admin.webrtc.turnSharedKeyExample', 'Ex "bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg="')} helpText={ - diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index add088913..65c151d8a 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -21,21 +21,26 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import SearchStore from 'stores/search_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import * as WebrtcActions from 'actions/webrtc_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; import Client from 'client/web_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import {getFlaggedPosts} from 'actions/post_actions.jsx'; -import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx'; +import {ActionTypes, Constants, Preferences, UserStatuses} from 'utils/constants.jsx'; import React from 'react'; import {FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router/es6'; import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap'; +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; + export default class ChannelHeader extends React.Component { constructor(props) { super(props); @@ -47,6 +52,8 @@ export default class ChannelHeader extends React.Component { this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.openRecentMentions = this.openRecentMentions.bind(this); this.getFlagged = this.getFlagged.bind(this); + this.initWebrtc = this.initWebrtc.bind(this); + this.onBusy = this.onBusy.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; @@ -64,7 +71,8 @@ export default class ChannelHeader extends React.Component { users: extraInfo.members, userCount: extraInfo.member_count, currentUser: UserStore.getCurrentUser(), - enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true) + enableFormatting: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true), + isBusy: WebrtcStore.isBusy() }; } @@ -85,6 +93,9 @@ export default class ChannelHeader extends React.Component { SearchStore.addSearchChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); + UserStore.addStatusesChangeListener(this.onListenerChange); + WebrtcStore.addChangedListener(this.onListenerChange); + WebrtcStore.addBusyListener(this.onBusy); $('.sidebar--left .dropdown-menu').perfectScrollbar(); document.addEventListener('keydown', this.openRecentMentions); } @@ -95,6 +106,9 @@ export default class ChannelHeader extends React.Component { SearchStore.removeSearchChangeListener(this.onListenerChange); PreferenceStore.removeChangeListener(this.onListenerChange); UserStore.removeChangeListener(this.onListenerChange); + UserStore.removeStatusesChangeListener(this.onListenerChange); + WebrtcStore.removeChangedListener(this.onListenerChange); + WebrtcStore.removeBusyListener(this.onBusy); document.removeEventListener('keydown', this.openRecentMentions); } @@ -204,6 +218,17 @@ export default class ChannelHeader extends React.Component { return true; } + initWebrtc(contactId, isOnline) { + if (isOnline && !this.state.isBusy) { + GlobalActions.emitCloseRightHandSide(); + WebrtcActions.initWebrtc(contactId, true); + } + } + + onBusy(isBusy) { + this.setState({isBusy}); + } + render() { const flagIcon = Constants.FLAG_ICON_SVG; @@ -250,10 +275,12 @@ export default class ChannelHeader extends React.Component { const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isDirect = (this.state.channel.type === 'D'); + let webrtc; if (isDirect) { + const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + let contact; if (this.state.users.length > 1) { - let contact; if (this.state.users[0].id === currentId) { contact = this.state.users[1]; } else { @@ -261,6 +288,68 @@ export default class ChannelHeader extends React.Component { } channelTitle = Utils.displayUsername(contact.id); } + + const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' && + global.mm_config.EnableDeveloper === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); + + if (webrtcEnabled) { + const isOffline = UserStore.getStatus(contact.id) === UserStatuses.OFFLINE; + const busy = this.state.isBusy; + let circleClass = ''; + let webrtcMessage; + + if (isOffline || busy) { + circleClass = 'offline'; + if (busy) { + webrtcMessage = ( + + ); + } + } else { + webrtcMessage = ( + + ); + } + + webrtc = ( + + ); + } } let channelTerm = ( @@ -541,6 +630,7 @@ export default class ChannelHeader extends React.Component {
+ {webrtc}
+ ); + + if (this.props.message) { + message = this.props.message; + } + return (

- + {message}

@@ -36,5 +44,6 @@ LoadingScreen.defaultProps = { position: 'relative' }; LoadingScreen.propTypes = { - position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) + position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']), + message: React.PropTypes.node }; diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index 413942865..c29bf61c3 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -6,12 +6,13 @@ import * as GlobalActions from 'actions/global_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; import AboutBuildModal from './about_build_modal.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; -import Constants from 'utils/constants.jsx'; +import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; import {FormattedMessage} from 'react-intl'; import {Link} from 'react-router/es6'; @@ -30,6 +31,8 @@ export default class NavbarDropdown extends React.Component { this.renderCustomEmojiLink = this.renderCustomEmojiLink.bind(this); + this.handleClick = this.handleClick.bind(this); + this.state = { showUserSettingsModal: false, showAboutModal: false, @@ -38,6 +41,13 @@ export default class NavbarDropdown extends React.Component { }; } + handleClick(e) { + if (WebrtcStore.isBusy()) { + WebrtcStore.emitChanged({action: WebrtcActionTypes.IN_PROGRESS}); + e.preventDefault(); + } + } + handleAboutModal() { this.setState({showAboutModal: true}); } @@ -77,7 +87,10 @@ export default class NavbarDropdown extends React.Component { return (
  • - + - + +
    + {content} diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 7aa0c028e..2dce093ec 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -118,6 +118,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.isBusy !== this.props.isBusy) { + return true; + } + return false; } render() { @@ -246,6 +250,8 @@ export default class Post extends React.Component { displayNameType={this.props.displayNameType} useMilitaryTime={this.props.useMilitaryTime} isFlagged={this.props.isFlagged} + status={this.props.status} + isBusy={this.props.isBusy} /> ); let botIndicator; @@ -98,5 +100,7 @@ PostHeader.propTypes = { compactDisplay: React.PropTypes.bool, displayNameType: React.PropTypes.string, useMilitaryTime: React.PropTypes.bool.isRequired, - isFlagged: React.PropTypes.bool.isRequired + isFlagged: React.PropTypes.bool.isRequired, + status: React.PropTypes.string, + isBusy: React.PropTypes.bool }; diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index a05507703..d686b28e5 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -316,6 +316,7 @@ export default class PostList extends React.Component { useMilitaryTime={this.props.useMilitaryTime} isFlagged={isFlagged} status={status} + isBusy={this.props.isBusy} /> ); @@ -585,5 +586,6 @@ PostList.propTypes = { useMilitaryTime: React.PropTypes.bool.isRequired, isFocusPost: React.PropTypes.bool, flaggedPosts: React.PropTypes.object, - statuses: React.PropTypes.object + statuses: React.PropTypes.object, + isBusy: React.PropTypes.bool }; diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index 7903087e9..dec4ca709 100644 --- a/webapp/components/post_view/post_focus_view_controller.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -9,6 +9,7 @@ import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; import Constants from 'utils/constants.jsx'; const ScrollTypes = Constants.ScrollTypes; @@ -26,6 +27,7 @@ export default class PostFocusView extends React.Component { this.onStatusChange = this.onStatusChange.bind(this); this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this); + this.onBusy = this.onBusy.bind(this); const focusedPostId = PostStore.getFocusedPostId(); @@ -38,13 +40,14 @@ export default class PostFocusView extends React.Component { const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); let statuses; - if (channel && channel.type !== Constants.DM_CHANNEL) { + if (channel) { statuses = Object.assign({}, UserStore.getStatuses()); } this.state = { postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled), currentUser: UserStore.getCurrentUser(), + isBusy: WebrtcStore.isBusy(), profiles, statuses, scrollType: ScrollTypes.POST, @@ -64,6 +67,7 @@ export default class PostFocusView extends React.Component { UserStore.addStatusesChangeListener(this.onStatusChange); EmojiStore.addChangeListener(this.onEmojiChange); PreferenceStore.addChangeListener(this.onPreferenceChange); + WebrtcStore.addBusyListener(this.onBusy); } componentWillUnmount() { @@ -73,6 +77,7 @@ export default class PostFocusView extends React.Component { UserStore.removeStatusesChangeListener(this.onStatusChange); EmojiStore.removeChangeListener(this.onEmojiChange); PreferenceStore.removeChangeListener(this.onPreferenceChange); + WebrtcStore.removeBusyListener(this.onBusy); } onChannelChange() { @@ -113,7 +118,7 @@ export default class PostFocusView extends React.Component { onStatusChange() { const channel = ChannelStore.getCurrent(); let statuses; - if (channel && channel.type !== Constants.DM_CHANNEL) { + if (channel) { statuses = Object.assign({}, UserStore.getStatuses()); } @@ -144,6 +149,10 @@ export default class PostFocusView extends React.Component { this.setState({scrollType: ScrollTypes.FREE}); } + onBusy(isBusy) { + this.setState({isBusy}); + } + render() { const postsToHighlight = {}; postsToHighlight[this.state.scrollPostId] = true; @@ -172,6 +181,7 @@ export default class PostFocusView extends React.Component { emojis={this.state.emojis} flaggedPosts={this.state.flaggedPosts} statuses={this.state.statuses} + isBusy={this.state.isBusy} /> ); } diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 840f71f23..b96374225 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -8,6 +8,7 @@ import PreferenceStore from 'stores/preference_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; import * as Utils from 'utils/utils.jsx'; @@ -30,6 +31,7 @@ export default class PostViewController extends React.Component { this.onPostListScroll = this.onPostListScroll.bind(this); this.onActivate = this.onActivate.bind(this); this.onDeactivate = this.onDeactivate.bind(this); + this.onBusy = this.onBusy.bind(this); const channel = props.channel; let profiles = UserStore.getProfiles(); @@ -54,6 +56,7 @@ export default class PostViewController extends React.Component { channel, postList: PostStore.filterPosts(channel.id, joinLeaveEnabled), currentUser: UserStore.getCurrentUser(), + isBusy: WebrtcStore.isBusy(), profiles, statuses, atTop: PostStore.getVisibilityAtTop(channel.id), @@ -140,6 +143,7 @@ export default class PostViewController extends React.Component { PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator); + WebrtcStore.addBusyListener(this.onBusy); } onDeactivate() { @@ -149,6 +153,7 @@ export default class PostViewController extends React.Component { PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator); + WebrtcStore.removeBusyListener(this.onBusy); } componentWillReceiveProps(nextProps) { @@ -217,6 +222,10 @@ export default class PostViewController extends React.Component { } } + onBusy(isBusy) { + this.setState({isBusy}); + } + shouldComponentUpdate(nextProps, nextState) { if (nextProps.active !== this.props.active) { return true; @@ -294,6 +303,10 @@ export default class PostViewController extends React.Component { return true; } + if (nextState.isBusy !== this.state.isBusy) { + return true; + } + return false; } @@ -326,6 +339,7 @@ export default class PostViewController extends React.Component { lastViewed={this.state.lastViewed} ownNewMessage={this.state.ownNewMessage} statuses={this.state.statuses} + isBusy={this.state.isBusy} /> ); } diff --git a/webapp/components/rhs_header_post.jsx b/webapp/components/rhs_header_post.jsx index 978c58c85..d0d720bb5 100644 --- a/webapp/components/rhs_header_post.jsx +++ b/webapp/components/rhs_header_post.jsx @@ -36,7 +36,7 @@ export default class RhsHeaderPost extends React.Component { handleBack(e) { e.preventDefault(); - if (this.props.fromSearch) { + if (this.props.fromSearch || this.props.isWebrtc) { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_SEARCH_TERM, term: this.props.fromSearch, @@ -82,6 +82,15 @@ export default class RhsHeaderPost extends React.Component { /> ); + } else if (this.props.isWebrtc) { + backToResultsTooltip = ( + + + + ); } const expandSidebarTooltip = ( @@ -102,7 +111,7 @@ export default class RhsHeaderPost extends React.Component { ); - if (this.props.fromSearch || this.props.fromFlaggedPosts) { + if (this.props.fromSearch || this.props.fromFlaggedPosts || this.props.isWebrtc) { back = ( {name}
    ; } + let webrtc; + const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + + const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' && + global.mm_config.EnableDeveloper === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); + + if (webrtcEnabled && this.props.user.id !== this.state.currentUserId) { + const isOnline = this.props.status !== UserStatuses.OFFLINE; + let webrtcMessage; + let circleClass = 'offline'; + if (isOnline && !this.props.isBusy) { + circleClass = ''; + webrtcMessage = ( + + ); + } else if (this.props.isBusy) { + webrtcMessage = ( + + ); + } + + webrtc = ( + + ); + } + var dataContent = []; dataContent.push( f !== 'WEBRTC_PREVIEW'); + } + for (const key of preReleaseFeaturesKeys) { const feature = PreReleaseFeatures[key]; @@ -329,6 +336,13 @@ export default class AdvancedSettingsDisplay extends React.Component { defaultMessage='Show experimental previews of link content, when available' /> ); + case 'WEBRTC_PREVIEW': + return ( + + ); default: return null; } diff --git a/webapp/components/webrtc/components/webrtc_header.jsx b/webapp/components/webrtc/components/webrtc_header.jsx new file mode 100644 index 000000000..a4a9c740c --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_header.jsx @@ -0,0 +1,100 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from 'utils/constants.jsx'; + +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class WebrtcHeader extends React.Component { + render() { + const title = ( + + ); + + const closeSidebarTooltip = ( + + + + ); + + const expandSidebarTooltip = ( + + + + ); + + const shrinkSidebarTooltip = ( + + + + ); + + return ( +
    + {title} +
    + + +
    +
    + ); + } +} + +WebrtcHeader.propTypes = { + username: React.PropTypes.string.isRequired, + onClose: React.PropTypes.func.isRequired, + toggleSize: React.PropTypes.function +}; diff --git a/webapp/components/webrtc/components/webrtc_notification.jsx b/webapp/components/webrtc/components/webrtc_notification.jsx new file mode 100644 index 000000000..5456d6cb8 --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_notification.jsx @@ -0,0 +1,320 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from 'client/web_client.jsx'; +import WebSocketClient from 'client/web_websocket_client.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; + +import * as GlobalActions from 'actions/global_actions.jsx'; +import * as WebrtcActions from 'actions/webrtc_actions.jsx'; +import * as Utils from 'utils/utils.jsx'; +import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; + +import ring from 'images/ring.mp3'; + +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; + +export default class WebrtcNotification extends React.Component { + constructor() { + super(); + + this.mounted = false; + + this.closeNotification = this.closeNotification.bind(this); + this.onIncomingCall = this.onIncomingCall.bind(this); + this.onCancelCall = this.onCancelCall.bind(this); + this.onRhs = this.onRhs.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleAnswer = this.handleAnswer.bind(this); + this.handleTimeout = this.handleTimeout.bind(this); + this.stopRinging = this.stopRinging.bind(this); + this.closeRightHandSide = this.closeRightHandSide.bind(this); + + this.state = { + userCalling: null, + rhsOpened: false + }; + } + + componentDidMount() { + WebrtcStore.addNotifyListener(this.onIncomingCall); + WebrtcStore.addChangedListener(this.onCancelCall); + WebrtcStore.addRhsChangedListener(this.onRhs); + this.mounted = true; + } + + componentWillUnmount() { + WebrtcStore.removeNotifyListener(this.onIncomingCall); + WebrtcStore.removeChangedListener(this.onCancelCall); + WebrtcStore.removeRhsChangedListener(this.onRhs); + if (this.refs.ring) { + this.refs.ring.removeListener('ended', this.handleTimeout); + } + this.mounted = false; + } + + componentDidUpdate() { + if (this.state.userCalling) { + this.refs.ring.addEventListener('ended', this.handleTimeout); + } + } + + closeNotification() { + this.setState({ + userCalling: null + }); + } + + stopRinging() { + if (this.refs.ring) { + this.refs.ring.pause(); + this.refs.ring.currentTime = 0; + } + this.setState({userCalling: null}); + } + + closeRightHandSide(e) { + e.preventDefault(); + GlobalActions.emitCloseRightHandSide(); + } + + onIncomingCall(incoming) { + if (this.mounted) { + const userId = incoming.from_user_id; + const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + const featureEnabled = Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); + + if (featureEnabled) { + if (WebrtcStore.isBusy()) { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.BUSY, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } else if (userMedia) { + WebrtcStore.setVideoCallWith(userId); + this.setState({ + userCalling: UserStore.getProfile(userId) + }); + } else { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.UNSUPPORTED, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } + } else { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.DISABLED, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } + } + } + + onCancelCall(message) { + if (message && message.action !== WebrtcActionTypes.CANCEL) { + return; + } else if (message && message.action === WebrtcActionTypes.CANCEL && this.state.userCalling && message.from_user_id !== this.state.userCalling.id) { + return; + } + + WebrtcStore.setVideoCallWith(null); + this.closeNotification(); + } + + onRhs(rhsOpened) { + this.setState({rhsOpened}); + } + + handleTimeout() { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.NO_ANSWER, + from_user_id: UserStore.getCurrentId(), + to_user_id: this.state.userCalling.id + }); + + this.onCancelCall(); + } + + handleAnswer(e) { + if (e) { + e.preventDefault(); + } + + const caller = this.state.userCalling; + if (caller) { + const callerId = caller.id; + const currentUserId = UserStore.getCurrentId(); + const message = { + action: WebrtcActionTypes.ANSWER, + from_user_id: currentUserId, + to_user_id: callerId + }; + + GlobalActions.emitCloseRightHandSide(); + WebrtcActions.initWebrtc(callerId, false); + WebSocketClient.sendMessage('webrtc', message); + + // delay till next tick (this will give time to listen for events + setTimeout(() => { + //we switch from and to user to handle the event locally + message.from_user_id = callerId; + message.to_user_id = currentUserId; + WebrtcActions.handle(message); + }, 0); + + this.closeNotification(); + } + } + + handleClose(e) { + if (e) { + e.preventDefault(); + } + if (this.state.userCalling) { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.DECLINE, + from_user_id: UserStore.getCurrentId(), + to_user_id: this.state.userCalling.id + }); + } + + this.onCancelCall(); + } + + render() { + const user = this.state.userCalling; + if (user) { + const username = Utils.displayUsername(user.id); + const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.update_at || new Date().getTime()); + const profileImg = ( + + ); + const answerBtn = ( + + + + + + + + + ); + + const rejectBtn = ( + + + + + + + + + ); + + const msg = ( +
    + +
    + {answerBtn} + {rejectBtn} +
    +
    + ); + + return ( +
    +
    + ); + } else if (this.state.rhsOpened && WebrtcStore.isBusy()) { + return ( +
    + + +
    + ); + } + + return
    ; + } +} diff --git a/webapp/components/webrtc/components/webrtc_sidebar.jsx b/webapp/components/webrtc/components/webrtc_sidebar.jsx new file mode 100644 index 000000000..59c25890b --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_sidebar.jsx @@ -0,0 +1,133 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; + +import WebrtcController from '../webrtc_controller.jsx'; +import UserStore from 'stores/user_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import React from 'react'; + +export default class SidebarRight extends React.Component { + constructor(props) { + super(props); + + this.plScrolledToBottom = true; + + this.onShrink = this.onShrink.bind(this); + this.toggleSize = this.toggleSize.bind(this); + this.onInitializeVideoCall = this.onInitializeVideoCall.bind(this); + + this.doStrangeThings = this.doStrangeThings.bind(this); + + this.state = { + expanded: false, + currentUser: UserStore.getCurrentUser(), + videoCallVisible: false, + isCaller: false, + videoCallWithUserId: null + }; + } + + componentDidMount() { + WebrtcStore.addInitListener(this.onInitializeVideoCall); + this.doStrangeThings(); + } + + componentWillUnmount() { + WebrtcStore.removeInitListener(this.onInitializeVideoCall); + } + + shouldComponentUpdate(nextProps, nextState) { + return !Utils.areObjectsEqual(nextState, this.state); + } + + doStrangeThings() { + // We should have a better way to do this stuff + // Hence the function name. + $('.app__body .inner-wrap').removeClass('move--right'); + $('.app__body .inner-wrap').addClass('webrtc--show'); + $('.app__body .sidebar--left').removeClass('move--right'); + $('.app__body .webrtc').addClass('webrtc--show'); + + //$('.sidebar--right').prepend(''); + if (!this.state.videoCallVisible) { + $('.app__body .inner-wrap').removeClass('webrtc--show').removeClass('move--right'); + $('.app__body .webrtc').removeClass('webrtc--show'); + return ( +
    + ); + } + return null; + } + + componentDidUpdate() { + this.doStrangeThings(); + } + + onShrink() { + this.setState({expanded: false}); + } + + toggleSize(e) { + if (e) { + e.preventDefault(); + } + this.setState({expanded: !this.state.expanded}); + } + + onInitializeVideoCall(userId, isCaller) { + let expanded = this.state.expanded; + if (userId === null) { + expanded = false; + } + this.setState({ + videoCallVisible: (userId !== null), + isCaller, + videoCallWithUserId: userId, + expanded + }); + + if (userId !== null) { + this.forceUpdate(); + } + } + + render() { + let content = null; + let expandedClass = ''; + + if (this.state.expanded) { + expandedClass = 'sidebar--right--expanded'; + } + + if (this.state.videoCallVisible) { + content = ( + + ); + } + + return ( +