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)
Этот коммит содержится в:
@@ -264,7 +264,7 @@ export default class AdminSidebar extends React.Component {
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.webrtc'
|
||||
defaultMessage='WebRTC'
|
||||
defaultMessage='WebRTC (Beta)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.integrations.webrtc'
|
||||
defaultMessage='Mattermost WebRTC'
|
||||
defaultMessage='Mattermost WebRTC (Beta)'
|
||||
/>
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
renderSettings() {
|
||||
const tosCheckbox = (
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-4'/>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='checkbox'
|
||||
ref='agree'
|
||||
checked={this.state.agree}
|
||||
onChange={this.handleAgreeChange}
|
||||
disabled={!this.state.enableWebrtc}
|
||||
/>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.webrtc.agree'
|
||||
defaultMessage=' I understand and accept the Mattermost Hosted WebRTC Service <a href="https://about.mattermost.com/webrtc-terms/" target="_blank">Terms of Service</a> and <a href="https://about.mattermost.com/webrtc-privacy/" target="_blank">Privacy Policy</a>.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsGroup>
|
||||
<BooleanSetting
|
||||
@@ -79,6 +112,7 @@ export default class WebrtcSettings extends AdminSettings {
|
||||
value={this.state.enableWebrtc}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{tosCheckbox}
|
||||
<TextSetting
|
||||
id='gatewayWebsocketUrl'
|
||||
label={
|
||||
@@ -111,7 +145,7 @@ export default class WebrtcSettings extends AdminSettings {
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.webrtc.gatewayAdminUrlDescription'
|
||||
defaultMessage='Enter https://<mattermost-webrtc-gateway-url>:<port>. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.
|
||||
defaultMessage='Enter https://<mattermost-webrtc-gateway-url>:<port>/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={
|
||||
<FormattedHTMLMessage
|
||||
<FormattedMessage
|
||||
id='admin.webrtc.turnUriDescription'
|
||||
defaultMessage='Enter your TURN URI as turn:<your-turn-url>:<port>. TURN is a standardized network protocol to allow an end host to assist devices to establish a connection by using a relay public IP address if it is located behind a symmetric NAT.'
|
||||
/>
|
||||
@@ -186,7 +220,7 @@ export default class WebrtcSettings extends AdminSettings {
|
||||
}
|
||||
placeholder={Utils.localizeMessage('admin.webrtc.turnUsernameExample', 'Ex "myusername"')}
|
||||
helpText={
|
||||
<FormattedHTMLMessage
|
||||
<FormattedMessage
|
||||
id='admin.webrtc.turnUsernameDescription'
|
||||
defaultMessage='Enter your TURN Server Username.'
|
||||
/>
|
||||
@@ -205,7 +239,7 @@ export default class WebrtcSettings extends AdminSettings {
|
||||
}
|
||||
placeholder={Utils.localizeMessage('admin.webrtc.turnSharedKeyExample', 'Ex "bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg="')}
|
||||
helpText={
|
||||
<FormattedHTMLMessage
|
||||
<FormattedMessage
|
||||
id='admin.webrtc.turnSharedKeyDescription'
|
||||
defaultMessage='Enter your TURN Server Shared Key. This is used to created dynamic passwords to establish the connection. Each password is valid for a short period of time.'
|
||||
/>
|
||||
|
||||
@@ -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 = (
|
||||
<FormattedMessage
|
||||
id='channel_header.webrtc.unavailable'
|
||||
defaultMessage='New call unavailable until your existing call ends'
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
webrtcMessage = (
|
||||
<FormattedMessage
|
||||
id='channel_header.webrtc.call'
|
||||
defaultMessage='Start Video Call'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
webrtc = (
|
||||
<div className='webrtc__header'>
|
||||
<a
|
||||
href='#'
|
||||
onClick={() => this.initWebrtc(contact.id, !isOffline)}
|
||||
disabled={isOffline}
|
||||
>
|
||||
<svg
|
||||
id='webrtc-btn'
|
||||
className='webrtc__button'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<circle
|
||||
className={circleClass}
|
||||
cx='16'
|
||||
cy='16'
|
||||
r='18'
|
||||
>
|
||||
<title>
|
||||
{webrtcMessage}
|
||||
</title>
|
||||
</circle>
|
||||
<path
|
||||
className='off'
|
||||
transform='scale(0.4), translate(17,16)'
|
||||
d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let channelTerm = (
|
||||
@@ -541,6 +630,7 @@ export default class ChannelHeader extends React.Component {
|
||||
<tr>
|
||||
<th>
|
||||
<div className='channel-header__info'>
|
||||
{webrtc}
|
||||
<div className='dropdown'>
|
||||
<a
|
||||
href='#'
|
||||
@@ -564,8 +654,8 @@ export default class ChannelHeader extends React.Component {
|
||||
<OverlayTrigger
|
||||
trigger={'click'}
|
||||
placement='bottom'
|
||||
overlay={popoverContent}
|
||||
rootClose={true}
|
||||
overlay={popoverContent}
|
||||
ref='headerOverlay'
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import {ActionTypes, WebrtcActionTypes} from 'utils/constants.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||
|
||||
import {intlShape, injectIntl, FormattedMessage} from 'react-intl';
|
||||
|
||||
@@ -40,12 +40,18 @@ class LeaveTeamModal extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
GlobalActions.emitLeaveTeam();
|
||||
|
||||
handleSubmit(e) {
|
||||
this.setState({
|
||||
show: false
|
||||
});
|
||||
|
||||
if (WebrtcStore.isBusy()) {
|
||||
WebrtcStore.emitChanged({action: WebrtcActionTypes.IN_PROGRESS});
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalActions.emitLeaveTeam();
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
|
||||
@@ -11,6 +11,17 @@ export default class LoadingScreen extends React.Component {
|
||||
this.state = {};
|
||||
}
|
||||
render() {
|
||||
let message = (
|
||||
<FormattedMessage
|
||||
id='loading_screen.loading'
|
||||
defaultMessage='Loading'
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.message) {
|
||||
message = this.props.message;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='loading-screen'
|
||||
@@ -18,10 +29,7 @@ export default class LoadingScreen extends React.Component {
|
||||
>
|
||||
<div className='loading__content'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='loading_screen.loading'
|
||||
defaultMessage='Loading'
|
||||
/>
|
||||
{message}
|
||||
</h3>
|
||||
<div className='round round-1'></div>
|
||||
<div className='round round-2'></div>
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<li>
|
||||
<Link to={'/' + this.props.teamName + '/emoji'}>
|
||||
<Link
|
||||
onClick={this.handleClick}
|
||||
to={'/' + this.props.teamName + '/emoji'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.emoji'
|
||||
defaultMessage='Custom Emoji'
|
||||
@@ -198,7 +211,10 @@ export default class NavbarDropdown extends React.Component {
|
||||
if (integrationsEnabled && (isAdmin || config.EnableOnlyAdminIntegrations !== 'true')) {
|
||||
integrationsLink = (
|
||||
<li>
|
||||
<Link to={'/' + this.props.teamName + '/integrations'}>
|
||||
<Link
|
||||
to={'/' + this.props.teamName + '/integrations'}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.integrations'
|
||||
defaultMessage='Integrations'
|
||||
@@ -213,6 +229,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
<li>
|
||||
<Link
|
||||
to={'/admin_console'}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.console'
|
||||
@@ -231,6 +248,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
<Link
|
||||
key='newTeam_a'
|
||||
to='/create_team'
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.create'
|
||||
|
||||
@@ -21,6 +21,9 @@ import ErrorBar from 'components/error_bar.jsx';
|
||||
import SidebarRight from 'components/sidebar_right.jsx';
|
||||
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
|
||||
import Navbar from 'components/navbar.jsx';
|
||||
import WebrtcSidebar from './webrtc/components/webrtc_sidebar.jsx';
|
||||
|
||||
import WebrtcNotification from './webrtc/components/webrtc_notification.jsx';
|
||||
|
||||
// Modals
|
||||
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
|
||||
@@ -146,9 +149,11 @@ export default class NeedsTeam extends React.Component {
|
||||
return (
|
||||
<div className='channel-view'>
|
||||
<ErrorBar/>
|
||||
<WebrtcNotification/>
|
||||
<div className='container-fluid'>
|
||||
<SidebarRight/>
|
||||
<SidebarRightMenu teamType={this.state.team.type}/>
|
||||
<WebrtcSidebar/>
|
||||
{content}
|
||||
|
||||
<GetPostLinkModal/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<PostBody
|
||||
post={post}
|
||||
@@ -282,5 +288,6 @@ Post.propTypes = {
|
||||
isCommentMention: React.PropTypes.bool,
|
||||
useMilitaryTime: React.PropTypes.bool.isRequired,
|
||||
isFlagged: React.PropTypes.bool,
|
||||
status: React.PropTypes.string
|
||||
status: React.PropTypes.string,
|
||||
isBusy: React.PropTypes.bool
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@ export default class PostHeader extends React.Component {
|
||||
<UserProfile
|
||||
user={this.props.user}
|
||||
displayNameType={this.props.displayNameType}
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (this.props.isWebrtc) {
|
||||
backToResultsTooltip = (
|
||||
<Tooltip id='backToResultsTooltip'>
|
||||
<FormattedMessage
|
||||
id='rhs_header.backToCallTooltip'
|
||||
defaultMessage='Back to Call'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const expandSidebarTooltip = (
|
||||
@@ -102,7 +111,7 @@ export default class RhsHeaderPost extends React.Component {
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (this.props.fromSearch || this.props.fromFlaggedPosts) {
|
||||
if (this.props.fromSearch || this.props.fromFlaggedPosts || this.props.isWebrtc) {
|
||||
back = (
|
||||
<a
|
||||
href='#'
|
||||
@@ -178,6 +187,7 @@ RhsHeaderPost.defaultProps = {
|
||||
};
|
||||
RhsHeaderPost.propTypes = {
|
||||
isMentionSearch: React.PropTypes.bool,
|
||||
isWebrtc: React.PropTypes.bool,
|
||||
fromSearch: React.PropTypes.string,
|
||||
fromFlaggedPosts: React.PropTypes.bool,
|
||||
toggleSize: React.PropTypes.function,
|
||||
|
||||
@@ -282,6 +282,7 @@ export default class RhsThread extends React.Component {
|
||||
<RhsHeaderPost
|
||||
fromFlaggedPosts={this.props.fromFlaggedPosts}
|
||||
fromSearch={this.props.fromSearch}
|
||||
isWebrtc={this.props.isWebrtc}
|
||||
isMentionSearch={this.props.isMentionSearch}
|
||||
toggleSize={this.props.toggleSize}
|
||||
shrink={this.props.shrink}
|
||||
@@ -362,6 +363,7 @@ RhsThread.defaultProps = {
|
||||
RhsThread.propTypes = {
|
||||
fromSearch: React.PropTypes.string,
|
||||
fromFlaggedPosts: React.PropTypes.bool,
|
||||
isWebrtc: React.PropTypes.bool,
|
||||
isMentionSearch: React.PropTypes.bool,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
useMilitaryTime: React.PropTypes.bool.isRequired,
|
||||
|
||||
@@ -5,11 +5,11 @@ import $ from 'jquery';
|
||||
|
||||
import SearchResults from './search_results.jsx';
|
||||
import RhsThread from './rhs_thread.jsx';
|
||||
|
||||
import SearchStore from 'stores/search_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||
|
||||
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
||||
|
||||
@@ -101,6 +101,8 @@ export default class SidebarRight extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const isOpen = this.state.searchVisible || this.state.postRightVisible;
|
||||
WebrtcStore.emitRhsChanged(isOpen);
|
||||
this.doStrangeThings();
|
||||
}
|
||||
|
||||
@@ -175,6 +177,7 @@ export default class SidebarRight extends React.Component {
|
||||
<RhsThread
|
||||
fromFlaggedPosts={this.state.fromFlaggedPosts}
|
||||
fromSearch={this.state.fromSearch}
|
||||
isWebrtc={WebrtcStore.isBusy()}
|
||||
isMentionSearch={this.state.isMentionSearch}
|
||||
currentUser={this.state.currentUser}
|
||||
useMilitaryTime={this.state.useMilitaryTime}
|
||||
|
||||
@@ -10,12 +10,13 @@ import AboutBuildModal from './about_build_modal.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import WebrtcStore from 'stores/webrtc_store.jsx';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import {Constants, WebrtcActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const Preferences = Constants.Preferences;
|
||||
@@ -33,6 +34,7 @@ export default class SidebarRightMenu extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleAboutModal = this.handleAboutModal.bind(this);
|
||||
this.searchMentions = this.searchMentions.bind(this);
|
||||
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
|
||||
@@ -47,6 +49,13 @@ export default class SidebarRightMenu extends React.Component {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (WebrtcStore.isBusy()) {
|
||||
WebrtcStore.emitChanged({action: WebrtcActionTypes.IN_PROGRESS});
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleAboutModal() {
|
||||
this.setState({showAboutModal: true});
|
||||
}
|
||||
@@ -254,6 +263,7 @@ export default class SidebarRightMenu extends React.Component {
|
||||
<li>
|
||||
<Link
|
||||
to={'/admin_console'}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<i className='icon fa fa-wrench'></i>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -4,22 +4,33 @@
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Client from 'client/web_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 Constants from 'utils/constants.jsx';
|
||||
const UserStatuses = Constants.UserStatuses;
|
||||
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
|
||||
|
||||
import {Popover, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
var id = 0;
|
||||
|
||||
function nextId() {
|
||||
id = id + 1;
|
||||
return id;
|
||||
}
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class UserProfile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.uniqueId = nextId();
|
||||
|
||||
this.initWebrtc = this.initWebrtc.bind(this);
|
||||
this.state = {
|
||||
currentUserId: UserStore.getCurrentId()
|
||||
};
|
||||
}
|
||||
|
||||
initWebrtc() {
|
||||
if (this.props.status !== UserStatuses.OFFLINE && !WebrtcStore.isBusy()) {
|
||||
GlobalActions.emitCloseRightHandSide();
|
||||
WebrtcActions.initWebrtc(this.props.user.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
@@ -43,6 +54,14 @@ export default class UserProfile extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.status !== this.props.status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.isBusy !== this.props.isBusy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -68,6 +87,70 @@ export default class UserProfile extends React.Component {
|
||||
return <div className='user-popover'>{name}</div>;
|
||||
}
|
||||
|
||||
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 = (
|
||||
<FormattedMessage
|
||||
id='user_profile.webrtc.call'
|
||||
defaultMessage='Start Video Call'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.isBusy) {
|
||||
webrtcMessage = (
|
||||
<FormattedMessage
|
||||
id='user_profile.webrtc.unavailable'
|
||||
defaultMessage='New call unavailable until your existing call ends'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
webrtc = (
|
||||
<div
|
||||
className='webrtc__user-profile'
|
||||
key='makeCall'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
onClick={() => this.initWebrtc()}
|
||||
disabled={!isOnline}
|
||||
>
|
||||
<svg
|
||||
id='webrtc-btn'
|
||||
className='webrtc__button'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<circle
|
||||
className={circleClass}
|
||||
cx='16'
|
||||
cy='16'
|
||||
r='18'
|
||||
>
|
||||
<title>
|
||||
{webrtcMessage}
|
||||
</title>
|
||||
</circle>
|
||||
<path
|
||||
className='off'
|
||||
transform='scale(0.4), translate(17,16)'
|
||||
d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var dataContent = [];
|
||||
dataContent.push(
|
||||
<img
|
||||
@@ -97,6 +180,8 @@ export default class UserProfile extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
dataContent.push(webrtc);
|
||||
|
||||
if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) {
|
||||
dataContent.push(
|
||||
<div
|
||||
@@ -150,5 +235,7 @@ UserProfile.propTypes = {
|
||||
overwriteName: React.PropTypes.string,
|
||||
overwriteImage: React.PropTypes.string,
|
||||
disablePopover: React.PropTypes.bool,
|
||||
displayNameType: React.PropTypes.string
|
||||
displayNameType: React.PropTypes.string,
|
||||
status: React.PropTypes.string,
|
||||
isBusy: React.PropTypes.bool
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
|
||||
let preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
|
||||
const advancedSettings = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
|
||||
const settings = {
|
||||
send_on_ctrl_enter: PreferenceStore.get(
|
||||
@@ -55,6 +55,13 @@ export default class AdvancedSettingsDisplay extends React.Component {
|
||||
|
||||
let enabledFeatures = 0;
|
||||
for (const [name, value] of advancedSettings) {
|
||||
const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && global.mm_license.Webrtc === 'true' &&
|
||||
global.mm_config.EnableDeveloper === 'true';
|
||||
|
||||
if (!webrtcEnabled) {
|
||||
preReleaseFeaturesKeys = preReleaseFeaturesKeys.filter((f) => 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 (
|
||||
<FormattedMessage
|
||||
id='user.settings.advance.webrtc_preview'
|
||||
defaultMessage='Enable the ability to make and receive one-on-one WebRTC calls'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
100
webapp/components/webrtc/components/webrtc_header.jsx
Обычный файл
100
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 = (
|
||||
<FormattedMessage
|
||||
id='webrtc.header'
|
||||
defaultMessage='Call with {username}'
|
||||
values={{
|
||||
username: this.props.username
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const closeSidebarTooltip = (
|
||||
<Tooltip id='closeSidebarTooltip'>
|
||||
<FormattedMessage
|
||||
id='rhs_header.closeTooltip'
|
||||
defaultMessage='Close Sidebar'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const expandSidebarTooltip = (
|
||||
<Tooltip id='expandSidebarTooltip'>
|
||||
<FormattedMessage
|
||||
id='rhs_header.expandTooltip'
|
||||
defaultMessage='Expand Sidebar'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const shrinkSidebarTooltip = (
|
||||
<Tooltip id='shrinkSidebarTooltip'>
|
||||
<FormattedMessage
|
||||
id='rhs_header.expandTooltip'
|
||||
defaultMessage='Shrink Sidebar'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='sidebar--right__header'>
|
||||
<span className='sidebar--right__title'>{title}</span>
|
||||
<div className='pull-right'>
|
||||
<button
|
||||
type='button'
|
||||
className='sidebar--right__expand'
|
||||
aria-label='Expand'
|
||||
onClick={this.props.toggleSize}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={expandSidebarTooltip}
|
||||
>
|
||||
<i className='fa fa-expand'/>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={shrinkSidebarTooltip}
|
||||
>
|
||||
<i className='fa fa-compress'/>
|
||||
</OverlayTrigger>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='sidebar--right__close'
|
||||
aria-label='Close'
|
||||
title='Close'
|
||||
onClick={this.props.onClose}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={closeSidebarTooltip}
|
||||
>
|
||||
<i className='fa fa-sign-out'/>
|
||||
</OverlayTrigger>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebrtcHeader.propTypes = {
|
||||
username: React.PropTypes.string.isRequired,
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
toggleSize: React.PropTypes.function
|
||||
};
|
||||
320
webapp/components/webrtc/components/webrtc_notification.jsx
Обычный файл
320
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 = (
|
||||
<img
|
||||
className='user-popover__image'
|
||||
src={profileImgSrc}
|
||||
height='128'
|
||||
width='128'
|
||||
key='user-popover-image'
|
||||
/>
|
||||
);
|
||||
const answerBtn = (
|
||||
<svg
|
||||
className='webrtc-icons__pickup'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='48'
|
||||
height='48'
|
||||
viewBox='-10 -10 68 68'
|
||||
onClick={this.handleAnswer}
|
||||
>
|
||||
<circle
|
||||
cx='24'
|
||||
cy='24'
|
||||
r='34'
|
||||
>
|
||||
<title>
|
||||
<FormattedMessage
|
||||
id='webrtc.notification.answer'
|
||||
defaultMessage='Answer'
|
||||
/>
|
||||
</title>
|
||||
</circle>
|
||||
<path
|
||||
transform='translate(-10,-10)'
|
||||
fill='#fff'
|
||||
d='M29.854,37.627c1.723,1.904 3.679,3.468 5.793,4.684l3.683,-3.334c0.469,-0.424 1.119,-0.517 1.669,-0.302c1.628,0.63 3.331,1.021 5.056,1.174c0.401,0.026 0.795,0.199 1.09,0.525c0.295,0.326 0.433,0.741 0.407,1.153l-0.279,5.593c-0.02,0.418 -0.199,0.817 -0.525,1.112c-0.326,0.296 -0.741,0.434 -1.159,0.413c-6.704,-0.504 -13.238,-3.491 -18.108,-8.87c-4.869,-5.38 -7.192,-12.179 -7.028,-18.899c0.015,-0.413 0.199,-0.817 0.526,-1.113c0.326,-0.295 0.74,-0.433 1.153,-0.407l5.593,0.279c0.407,0.02 0.812,0.193 1.107,0.519c0.29,0.32 0.428,0.735 0.413,1.137c-0.018,1.732 0.202,3.464 0.667,5.147c0.159,0.569 0.003,1.207 -0.466,1.631l-3.683,3.334c1.005,2.219 2.368,4.32 4.091,6.224Z'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const rejectBtn = (
|
||||
<svg
|
||||
className='webrtc-icons__cancel'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='48'
|
||||
height='48'
|
||||
viewBox='-10 -10 68 68'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<circle
|
||||
cx='24'
|
||||
cy='24'
|
||||
r='34'
|
||||
>
|
||||
<title>
|
||||
<FormattedMessage
|
||||
id='webrtc.notification.decline'
|
||||
defaultMessage='Decline'
|
||||
/>
|
||||
</title>
|
||||
</circle>
|
||||
<path
|
||||
transform='scale(0.7), translate(11,10)'
|
||||
d='M24 18c-3.21 0-6.3.5-9.2 1.44v6.21c0 .79-.46 1.47-1.12 1.8-1.95.98-3.74 2.23-5.33 3.7-.36.35-.85.57-1.4.57-.55 0-1.05-.22-1.41-.59L.59 26.18c-.37-.37-.59-.87-.59-1.42 0-.55.22-1.05.59-1.42C6.68 17.55 14.93 14 24 14s17.32 3.55 23.41 9.34c.37.36.59.87.59 1.42 0 .55-.22 1.05-.59 1.41l-4.95 4.95c-.36.36-.86.59-1.41.59-.54 0-1.04-.22-1.4-.57-1.59-1.47-3.38-2.72-5.33-3.7-.66-.33-1.12-1.01-1.12-1.8v-6.21C30.3 18.5 27.21 18 24 18z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const msg = (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='webrtc.notification.incoming_call'
|
||||
defaultMessage='{username} is calling you.'
|
||||
values={{
|
||||
username
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='webrtc-buttons webrtc-icons active'
|
||||
style={{marginTop: '5px'}}
|
||||
>
|
||||
{answerBtn}
|
||||
{rejectBtn}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='webrtc-notification'>
|
||||
<audio
|
||||
ref='ring'
|
||||
src={ring}
|
||||
autoPlay={true}
|
||||
/>
|
||||
<div>
|
||||
{profileImg}
|
||||
</div>
|
||||
{msg}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.rhsOpened && WebrtcStore.isBusy()) {
|
||||
return (
|
||||
<div
|
||||
className='webrtc__notification--rhs'
|
||||
onClick={this.closeRightHandSide}
|
||||
>
|
||||
<i className='fa fa-phone'/>
|
||||
<FormattedMessage
|
||||
id='webrtc.notification.returnToCall'
|
||||
defaultMessage='Return to ongoing call with {username}'
|
||||
values={{
|
||||
username: Utils.displayUsername(WebrtcStore.getVideoCallWith())
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
133
webapp/components/webrtc/components/webrtc_sidebar.jsx
Обычный файл
133
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('<div class="sidebar__overlay"></div>');
|
||||
if (!this.state.videoCallVisible) {
|
||||
$('.app__body .inner-wrap').removeClass('webrtc--show').removeClass('move--right');
|
||||
$('.app__body .webrtc').removeClass('webrtc--show');
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
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 = (
|
||||
<WebrtcController
|
||||
currentUser={this.state.currentUser}
|
||||
userId={this.state.videoCallWithUserId}
|
||||
isCaller={this.state.isCaller}
|
||||
expanded={this.state.expanded}
|
||||
toggleSize={this.toggleSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'sidebar--right webrtc ' + expandedClass}
|
||||
id='sidebar-webrtc'
|
||||
>
|
||||
<div
|
||||
onClick={this.onShrink}
|
||||
className='sidebar--right__bg'
|
||||
/>
|
||||
<div className='sidebar-right-container'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
1214
webapp/components/webrtc/webrtc_controller.jsx
Обычный файл
1214
webapp/components/webrtc/webrtc_controller.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Ссылка в новой задаче
Block a user