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)
Этот коммит содержится в:
@@ -43,7 +43,7 @@ type Features struct {
|
||||
MHPNS *bool `json:"mhpns"`
|
||||
SAML *bool `json:"saml"`
|
||||
PasswordRequirements *bool `json:"password_requirements"`
|
||||
// after we enabled more features for web rtc we'll need to control them with this
|
||||
// after we enabled more features for webrtc we'll need to control them with this
|
||||
Webrtc *bool `json:"webrtc"`
|
||||
FutureFeatures *bool `json:"future_features"`
|
||||
}
|
||||
|
||||
20
webapp/actions/webrtc_actions.jsx
Обычный файл
20
webapp/actions/webrtc_actions.jsx
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
import {WebrtcActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
export function initWebrtc(userId, isCalling) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: WebrtcActionTypes.INITIALIZE,
|
||||
user_id: userId,
|
||||
is_calling: isCalling
|
||||
});
|
||||
}
|
||||
|
||||
export function handle(message) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: message.action,
|
||||
message
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import NotificationStore from 'stores/notification_store.jsx'; //eslint-disable-
|
||||
|
||||
import Client from 'client/web_client.jsx';
|
||||
import WebSocketClient from 'client/web_websocket_client.jsx';
|
||||
import * as WebrtcActions from './webrtc_actions.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
@@ -22,9 +23,7 @@ import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
import * as UserActions from 'actions/user_actions.jsx';
|
||||
import {handleNewPost} from 'actions/post_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const SocketEvents = Constants.SocketEvents;
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import {Constants, SocketEvents, ActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
import {browserHistory} from 'react-router/es6';
|
||||
|
||||
@@ -160,6 +159,10 @@ function handleEvent(msg) {
|
||||
handleHelloEvent(msg);
|
||||
break;
|
||||
|
||||
case SocketEvents.WEBRTC:
|
||||
handleWebrtc(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -293,3 +296,8 @@ function handleHelloEvent(msg) {
|
||||
Client.serverVersion = msg.data.server_version;
|
||||
AsyncClient.checkVersion();
|
||||
}
|
||||
|
||||
function handleWebrtc(msg) {
|
||||
const data = msg.data;
|
||||
return WebrtcActions.handle(data);
|
||||
}
|
||||
@@ -121,6 +121,10 @@ export default class Client {
|
||||
return `${this.url}${this.urlVersion}/users/${userId}`;
|
||||
}
|
||||
|
||||
getWebrtcRoute() {
|
||||
return `${this.url}${this.urlVersion}/webrtc`;
|
||||
}
|
||||
|
||||
setTranslations(messages) {
|
||||
this.translations = messages;
|
||||
}
|
||||
@@ -1803,4 +1807,12 @@ export default class Client {
|
||||
return success(res.body);
|
||||
});
|
||||
}
|
||||
|
||||
webrtcToken(success, error) {
|
||||
request.post(`${this.getWebrtcRoute()}/token`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
end(this.handleResponse.bind(this, 'webrtcToken', success, error));
|
||||
}
|
||||
}
|
||||
|
||||
145
webapp/client/webrtc_client.jsx
Обычный файл
145
webapp/client/webrtc_client.jsx
Обычный файл
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export default class WebrtcClient {
|
||||
constructor() {
|
||||
this.init = this.init.bind(this);
|
||||
this.listDevices = this.listDevices.bind(this);
|
||||
this.isExtensionEnabled = this.isExtensionEnabled.bind(this);
|
||||
this.isWebrtcSupported = this.isWebrtcSupported.bind(this);
|
||||
|
||||
this.initDone = false;
|
||||
this.sessions = {};
|
||||
}
|
||||
|
||||
noop() {} //eslint-disable-line no-empty-function
|
||||
|
||||
// this function is going to be needed when we enable screen sharing
|
||||
isExtensionEnabled() {
|
||||
if (window.navigator.userAgent.match('Chrome')) {
|
||||
const chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10);
|
||||
let maxver = 33;
|
||||
if (window.navigator.userAgent.match('Linux')) {
|
||||
maxver = 35; // "known" crash in chrome 34 and 35 on linux
|
||||
}
|
||||
if (chromever >= 26 && chromever <= maxver) {
|
||||
// Older versions of Chrome don't support this extension-based approach, so lie
|
||||
return true;
|
||||
}
|
||||
return (document.getElementById('mattermost-extension-installed') !== null);
|
||||
}
|
||||
|
||||
// Firefox of others, no need for the extension (but this doesn't mean it will work)
|
||||
return true;
|
||||
}
|
||||
|
||||
init(opts) {
|
||||
const options = opts || {};
|
||||
|
||||
if (this.initDone === true) {
|
||||
// Already initialized
|
||||
return;
|
||||
}
|
||||
|
||||
this.trace = this.noop;
|
||||
this.debug = this.noop;
|
||||
this.log = this.noop;
|
||||
this.warn = this.noop;
|
||||
this.error = this.noop;
|
||||
|
||||
/* eslint-disable */
|
||||
if (options.debug === true || options.debug === 'all') {
|
||||
// Enable all debugging levels
|
||||
this.trace = console.trace.bind(console);
|
||||
this.debug = console.debug.bind(console);
|
||||
this.log = console.log.bind(console);
|
||||
this.warn = console.warn.bind(console);
|
||||
this.error = console.error.bind(console);
|
||||
} else if (Array.isArray(options.debug)) {
|
||||
for (const i in options.debug) {
|
||||
if (options.debug.hasOwnProperty(i)) {
|
||||
const d = options.debug[i];
|
||||
switch (d) {
|
||||
case 'trace':
|
||||
this.trace = console.trace.bind(console);
|
||||
break;
|
||||
case 'debug':
|
||||
this.debug = console.debug.bind(console);
|
||||
break;
|
||||
case 'log':
|
||||
this.log = console.log.bind(console);
|
||||
break;
|
||||
case 'warn':
|
||||
this.warn = console.warn.bind(console);
|
||||
break;
|
||||
case 'error':
|
||||
this.error = console.error.bind(console);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'log', warn', 'error')");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
this.log('Initializing WebRTC Client library');
|
||||
|
||||
// Detect tab close
|
||||
window.onbeforeunload = () => {
|
||||
this.log('Closing window');
|
||||
for (const s in this.sessions) {
|
||||
if (this.sessions.hasOwnProperty(s)) {
|
||||
if (this.sessions[s] && this.sessions[s].destroyOnUnload) {
|
||||
this.log('Destroying session ' + s);
|
||||
this.sessions[s].destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.initDone = true;
|
||||
}
|
||||
|
||||
// Helper method to enumerate devices
|
||||
listDevices(cb) {
|
||||
const callback = (typeof cb == 'function') ? cb : this.noop;
|
||||
|
||||
if (navigator.mediaDevices) {
|
||||
navigator.getUserMedia({audio: true, video: true}, (stream) => {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
this.debug(devices);
|
||||
callback(devices);
|
||||
|
||||
// Get rid of the now useless stream
|
||||
try {
|
||||
stream.stop();
|
||||
} catch (e) {
|
||||
this.error(e);
|
||||
}
|
||||
|
||||
this.stopMedia(stream);
|
||||
});
|
||||
}, (err) => {
|
||||
this.error(err);
|
||||
callback([]);
|
||||
});
|
||||
} else {
|
||||
this.warn('navigator.mediaDevices unavailable');
|
||||
callback([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to check whether WebRTC is supported by this browser
|
||||
isWebrtcSupported() {
|
||||
return window.RTCPeerConnection && navigator.getUserMedia;
|
||||
}
|
||||
|
||||
stopMedia(stream) {
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
1966
webapp/client/webrtc_session.jsx
Обычный файл
1966
webapp/client/webrtc_session.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -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
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -726,6 +726,7 @@
|
||||
"admin.sidebar.users": "Users",
|
||||
"admin.sidebar.usersAndTeams": "Users and Teams",
|
||||
"admin.sidebar.view_statistics": "Site Statistics",
|
||||
"admin.sidebar.webrtc": "WebRTC (Beta)",
|
||||
"admin.sidebarHeader.systemConsole": "System Console",
|
||||
"admin.sql.dataSource": "Data Source:",
|
||||
"admin.sql.driverName": "Driver Name:",
|
||||
@@ -829,7 +830,7 @@
|
||||
"admin.webrtc.gatewayAdminSecretDescription": "Enter your admin secret password to access the Gateway Admin URL.",
|
||||
"admin.webrtc.gatewayAdminSecretExample": "Ex \"PVRzWNN1Tg6szn7IQWvhpAvLByScWxdy\"",
|
||||
"admin.webrtc.gatewayAdminSecretTitle": "Gateway Admin Secret:",
|
||||
"admin.webrtc.gatewayAdminUrlDescription": "Enter https://<mattermost-webrtc-gateway-url>:<port>. 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.",
|
||||
"admin.webrtc.gatewayAdminUrlDescription": "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.",
|
||||
"admin.webrtc.gatewayAdminUrlExample": "Ex \"https://webrtc.mattermost.com:7089/admin\"",
|
||||
"admin.webrtc.gatewayAdminUrlTitle": "Gateway Admin URL:",
|
||||
"admin.webrtc.gatewayWebsocketUrlDescription": "Enter wss://<mattermost-webrtc-gateway-url>:<port>. Make sure you use WS or WSS in your URL depending on your server configuration. This is the websocket used to signal and establish communication between the peers.",
|
||||
@@ -951,6 +952,7 @@
|
||||
"backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks",
|
||||
"backstage_sidebar.integrations.oauthApps": "OAuth 2.0 Applications",
|
||||
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
|
||||
"calling_screen": "Calling",
|
||||
"center_panel.recent": "Click here to jump to recent messages. ",
|
||||
"chanel_header.addMembers": "Add Members",
|
||||
"change_url.close": "Close",
|
||||
@@ -982,6 +984,8 @@
|
||||
"channel_header.setPurpose": "Set {term} Purpose...",
|
||||
"channel_header.viewInfo": "View Info",
|
||||
"channel_header.viewMembers": "View Members",
|
||||
"channel_header.webrtc.call": "Start Video Call",
|
||||
"channel_header.webrtc.unavailable": "New call unavailable until your existing call ends",
|
||||
"channel_info.about": "About",
|
||||
"channel_info.close": "Close",
|
||||
"channel_info.header": "Header:",
|
||||
@@ -1773,6 +1777,7 @@
|
||||
"user.settings.advance.sendTitle": "Send messages on CTRL + ENTER",
|
||||
"user.settings.advance.slashCmd_autocmp": "Enable external application to offer slash command autocomplete",
|
||||
"user.settings.advance.title": "Advanced Settings",
|
||||
"user.settings.advance.webrtc_preview": "Enable the ability to make and receive one-on-one WebRTC calls",
|
||||
"user.settings.custom_theme.awayIndicator": "Away Indicator",
|
||||
"user.settings.custom_theme.buttonBg": "Button BG",
|
||||
"user.settings.custom_theme.buttonColor": "Button Text",
|
||||
@@ -2020,6 +2025,8 @@
|
||||
"user.settings.security.title": "Security Settings",
|
||||
"user.settings.security.viewHistory": "View Access History",
|
||||
"user_list.notFound": "No users found",
|
||||
"user_profile.webrtc.call": "Start Video Call",
|
||||
"user_profile.webrtc.unavailable": "New call unavailable until your existing call ends",
|
||||
"view_image.loading": "Loading ",
|
||||
"view_image_popover.download": "Download",
|
||||
"view_image_popover.file": "File {count} of {total}",
|
||||
@@ -2030,5 +2037,31 @@
|
||||
"web.footer.terms": "Terms",
|
||||
"web.header.back": "Back",
|
||||
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere",
|
||||
"youtube_video.notFound": "Video not found"
|
||||
"webrtc.busy": "{username} is busy.",
|
||||
"webrtc.call": "Call",
|
||||
"webrtc.callEnded": "Call with {username} ended.",
|
||||
"webrtc.cancel": "Cancel call",
|
||||
"webrtc.cancelled": "{username} cancelled the call.",
|
||||
"webrtc.declined": "Your call has been declined by {username}.",
|
||||
"webrtc.disabled": "{username} has WebRTC disabled, and cannot receive calls. To enable the feature, they must go to Account Settings > Advanced > Preview pre-release features and turn on WebRTC.",
|
||||
"webrtc.failed": "There was a problem connecting the video call.",
|
||||
"webrtc.hangup": "Hangup",
|
||||
"webrtc.header": "Call with {username}",
|
||||
"webrtc.inProgress": "You have a call in progress. Please hangup first.",
|
||||
"webrtc.mediaError": "Unable to access camera or microphone.",
|
||||
"webrtc.mute_audio": "Mute microphone",
|
||||
"webrtc.noAnswer": "{username} is not answering the call.",
|
||||
"webrtc.notification.answer": "Answer",
|
||||
"webrtc.notification.decline": "Decline",
|
||||
"webrtc.notification.incoming_call": "{username} is calling you.",
|
||||
"webrtc.notification.returnToCall": "Return to ongoing call with {username}",
|
||||
"webrtc.offline": "{username} is offline.",
|
||||
"webrtc.pause_video": "Turn off camera",
|
||||
"webrtc.unmute_audio": "Unmute microphone",
|
||||
"webrtc.unpause_video": "Turn on camera",
|
||||
"webrtc.unsupported": "{username} client does not support video calls.",
|
||||
"youtube_video.notFound": "Video not found",
|
||||
"channel_header.webrtc.unavailable": "New call unavailable until your existing call ends",
|
||||
"user_profile.webrtc.unavailable": "New call unavailable until your existing call ends",
|
||||
"admin.sidebar.webrtc": "WebRTC (Beta)"
|
||||
}
|
||||
|
||||
Двоичные данные
webapp/images/ring.mp3
Обычный файл
Двоичные данные
webapp/images/ring.mp3
Обычный файл
Двоичный файл не отображается.
@@ -33,6 +33,7 @@
|
||||
"superagent": "2.1.0",
|
||||
"twemoji": "2.0.5",
|
||||
"velocity-animate": "1.2.3",
|
||||
"webrtc-adapter": "1.4.0",
|
||||
"xregexp": "3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -19,3 +19,4 @@
|
||||
@import 'tooltip';
|
||||
@import 'tutorial';
|
||||
@import 'videos';
|
||||
@import 'webrtc';
|
||||
|
||||
297
webapp/sass/components/_webrtc.scss
Обычный файл
297
webapp/sass/components/_webrtc.scss
Обычный файл
@@ -0,0 +1,297 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
.webrtc__user-profile {
|
||||
@include webrtc-button;
|
||||
text-align: center;
|
||||
|
||||
#webrtc-btn {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc__header {
|
||||
@include webrtc-button;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.webrtc__notification--rhs {
|
||||
background: $button--ready;
|
||||
border-radius: 3px 0 0 3px;
|
||||
bottom: 10px;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc__error {
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
|
||||
.control-label {
|
||||
@include border-radius(3px);
|
||||
background: #f2f2f2;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 1em 0 -1em;
|
||||
padding: .7em 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.has-error {
|
||||
.control-label {
|
||||
color: #a94442;
|
||||
font-size: .95em;
|
||||
|
||||
&:before {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #a94442;
|
||||
content: '\F071';
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
margin-right: 4px;
|
||||
text-rendering: auto;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#videos {
|
||||
background: $black;
|
||||
font-size: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
transition: all 1s;
|
||||
width: 100%;
|
||||
|
||||
#main-video video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#local-video {
|
||||
border: 1px solid gray;
|
||||
max-width: 30%;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
transition: opacity 1s;
|
||||
z-index: 2;
|
||||
|
||||
video {
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
-webkit-transform: scale(-1, 1);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
transform: scale(-1, 1);
|
||||
transition: opacity 1s;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc__local-image {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 2;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
height: 64px;
|
||||
max-width: 100%;
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc__remote-image {
|
||||
background-color: $black;
|
||||
border: 1px solid alpha-color($black, .2);
|
||||
padding: 40px 0;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
max-height: 128px;
|
||||
max-width: 128px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc__remote-mute {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc-buttons {
|
||||
margin-top: 1.2em;
|
||||
text-align: center;
|
||||
|
||||
.webrtc-icons__call {
|
||||
border-radius: 48px;
|
||||
|
||||
circle {
|
||||
fill: $button--ready;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
circle {
|
||||
fill: darken($button--ready, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
circle {
|
||||
fill: $video-circle-offline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc-icons__cancel {
|
||||
border-radius: 48px;
|
||||
|
||||
circle {
|
||||
fill: $button--cancel;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
circle {
|
||||
fill: darken($button--cancel, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc-icons {
|
||||
&.active {
|
||||
svg {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
border-radius: 48px;
|
||||
margin: 5px;
|
||||
|
||||
path {
|
||||
&.on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.off {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connecting {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
.loading-screen {
|
||||
background-color: rgba(0,0,0, .3);
|
||||
color: $connecting;
|
||||
margin-left: 5px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
top: 75px;
|
||||
width: 97%;
|
||||
|
||||
.loading__content {
|
||||
.round {
|
||||
background: $connecting !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc-notification {
|
||||
background-color: $webrtc-notification-bg;
|
||||
color: $white;
|
||||
left: calc(50% - 200px);
|
||||
padding: 20px 30px 10px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: calc(50% - 115px);
|
||||
width: 400px;
|
||||
z-index: 99999;
|
||||
|
||||
.webrtc-buttons {
|
||||
margin-top: 1.2em;
|
||||
text-align: center;
|
||||
|
||||
.webrtc-icons__pickup {
|
||||
circle {
|
||||
fill: $button--pickup;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
circle {
|
||||
fill: darken($button--pickup, 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--right--expanded {
|
||||
#videos {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
#main-video {
|
||||
height: 100%;
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webrtc-buttons {
|
||||
bottom: 5px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#videos {
|
||||
#local-video {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
#videos.small {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
.inner-wrap {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
.search-bar__container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -388,7 +388,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.channel__wrap.move--left & {
|
||||
.channel__wrap.move--left &,
|
||||
.channel__wrap.webrtc--show & {
|
||||
position: absolute;
|
||||
right: -400px;
|
||||
top: 14px;
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
position: fixed;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
z-index: 5;
|
||||
z-index: 6;
|
||||
|
||||
&.webrtc {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.sidebar--right__bg {
|
||||
@include single-transition(background-color, .5s, ease);
|
||||
@@ -20,7 +24,8 @@
|
||||
width: 300%;
|
||||
}
|
||||
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
.sidebar--right__bg {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
@media screen and (max-width: 1800px) {
|
||||
.inner-wrap {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
.date-separator,
|
||||
.new-separator {
|
||||
&.hovered--comment {
|
||||
@@ -18,7 +19,8 @@
|
||||
|
||||
@media screen and (max-width: 1440px) {
|
||||
.inner-wrap {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
.help__format-text {
|
||||
display: none;
|
||||
}
|
||||
@@ -53,7 +55,8 @@
|
||||
}
|
||||
|
||||
.inner-wrap {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
.file-overlay {
|
||||
font-size: em(18px);
|
||||
|
||||
|
||||
@@ -779,7 +779,8 @@
|
||||
display: block;
|
||||
width: 290px;
|
||||
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
@include translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -958,7 +959,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
margin: 0;
|
||||
@include translate3d(-100%, 0, 0);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
@include single-transition(all, .5s, ease);
|
||||
@include translateX(100%);
|
||||
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
-webkit-transform: translateX(0) !important;
|
||||
-moz-transform: translateX(0) !important;
|
||||
-ms-transform: translateX(0) !important;
|
||||
@@ -53,7 +54,8 @@
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
margin-right: 0 !important;
|
||||
|
||||
.channel-header__links {
|
||||
@@ -72,7 +74,8 @@
|
||||
}
|
||||
|
||||
.sidebar--right {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
@include translateX(0);
|
||||
}
|
||||
|
||||
@@ -106,7 +109,8 @@
|
||||
}
|
||||
|
||||
.inner-wrap {
|
||||
&.move--left {
|
||||
&.move--left,
|
||||
&.webrtc--show {
|
||||
margin-right: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#channel_view .inner-wrap.move--left {
|
||||
#channel_view .inner-wrap.move--left,
|
||||
#channel_view .inner-wrap.webrtc--show {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,4 +26,74 @@
|
||||
@mixin cursor($value) {
|
||||
cursor: -webkit-$value;
|
||||
cursor: zoom-$value;
|
||||
}
|
||||
}
|
||||
|
||||
// Webrtc button
|
||||
@mixin webrtc-button {
|
||||
.webrtc__button {
|
||||
@include border-radius(50px);
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
&.on,
|
||||
&:hover {
|
||||
background: darken($button--ready, 5%);
|
||||
}
|
||||
|
||||
&:hover circle {
|
||||
fill: darken($button--ready, 5%);
|
||||
}
|
||||
|
||||
circle {
|
||||
fill: $button--ready;
|
||||
|
||||
&.offline {
|
||||
fill: $video-circle-offline;
|
||||
}
|
||||
}
|
||||
|
||||
path {
|
||||
.on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.off {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.on {
|
||||
path {
|
||||
.on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.off {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
circle {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&[disabled] {
|
||||
.webrtc__button {
|
||||
&:hover {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
circle {
|
||||
fill: $video-circle-offline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,13 @@ $border-gray: 1px solid #ddd;
|
||||
|
||||
// Random variables
|
||||
$border-rad: 1px;
|
||||
|
||||
// Webrtc Variables
|
||||
$button--ready: #00A6EE;
|
||||
$video-circle-btn: #2f81b7;
|
||||
$video-circle-hover: #56C303;
|
||||
$video-circle-offline: #aaa;
|
||||
$button--cancel: #E41020;
|
||||
$button--pickup: #73B001;
|
||||
$connecting: $white;
|
||||
$webrtc-notification-bg: rgba(112, 128, 144, .8);
|
||||
110
webapp/stores/webrtc_store.jsx
Обычный файл
110
webapp/stores/webrtc_store.jsx
Обычный файл
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||
import EventEmitter from 'events';
|
||||
import {WebrtcActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
class WebrtcStoreClass extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.video_call_with = null;
|
||||
}
|
||||
|
||||
setVideoCallWith(userId) {
|
||||
this.video_call_with = userId;
|
||||
this.emitBusy(userId !== null);
|
||||
}
|
||||
|
||||
getVideoCallWith() {
|
||||
return this.video_call_with;
|
||||
}
|
||||
|
||||
isBusy() {
|
||||
return this.video_call_with !== null;
|
||||
}
|
||||
|
||||
emitInit(userId, isCaller) {
|
||||
this.emit(WebrtcActionTypes.INITIALIZE, userId, isCaller);
|
||||
}
|
||||
|
||||
addInitListener(callback) {
|
||||
this.on(WebrtcActionTypes.INITIALIZE, callback);
|
||||
}
|
||||
|
||||
removeInitListener(callback) {
|
||||
this.removeListener(WebrtcActionTypes.INITIALIZE, callback);
|
||||
}
|
||||
|
||||
emitBusy(isBusy) {
|
||||
this.emit(WebrtcActionTypes.BUSY, isBusy);
|
||||
}
|
||||
|
||||
addBusyListener(callback) {
|
||||
this.on(WebrtcActionTypes.BUSY, callback);
|
||||
}
|
||||
|
||||
removeBusyListener(callback) {
|
||||
this.removeListener(WebrtcActionTypes.BUSY, callback);
|
||||
}
|
||||
|
||||
emitNotify(message) {
|
||||
this.emit(WebrtcActionTypes.NOTIFY, message);
|
||||
}
|
||||
|
||||
addNotifyListener(callback) {
|
||||
this.on(WebrtcActionTypes.NOTIFY, callback);
|
||||
}
|
||||
|
||||
removeNotifyListener(callback) {
|
||||
this.removeListener(WebrtcActionTypes.NOTIFY, callback);
|
||||
}
|
||||
|
||||
emitChanged(message) {
|
||||
this.emit(WebrtcActionTypes.CHANGED, message);
|
||||
}
|
||||
|
||||
addChangedListener(callback) {
|
||||
this.on(WebrtcActionTypes.CHANGED, callback);
|
||||
}
|
||||
|
||||
removeChangedListener(callback) {
|
||||
this.removeListener(WebrtcActionTypes.CHANGED, callback);
|
||||
}
|
||||
|
||||
emitRhsChanged(isOpen) {
|
||||
this.emit(WebrtcActionTypes.RHS, isOpen);
|
||||
}
|
||||
|
||||
addRhsChangedListener(callback) {
|
||||
this.on(WebrtcActionTypes.RHS, callback);
|
||||
}
|
||||
|
||||
removeRhsChangedListener(callback) {
|
||||
this.removeListener(WebrtcActionTypes.RHS, callback);
|
||||
}
|
||||
}
|
||||
|
||||
var WebrtcStore = new WebrtcStoreClass();
|
||||
WebrtcStore.setMaxListeners(0);
|
||||
|
||||
WebrtcStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
var action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
case WebrtcActionTypes.INITIALIZE:
|
||||
WebrtcStore.emitInit(action.user_id, action.is_calling);
|
||||
break;
|
||||
case WebrtcActionTypes.NOTIFY:
|
||||
WebrtcStore.emitNotify(action.message);
|
||||
break;
|
||||
default:
|
||||
if (action.message) {
|
||||
WebrtcStore.emitChanged(action.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
export default WebrtcStore;
|
||||
@@ -55,7 +55,10 @@ export function createDMIntroMessage(channel, centeredIntro) {
|
||||
</div>
|
||||
<div className='channel-intro-profile'>
|
||||
<strong>
|
||||
<UserProfile user={teammate}/>
|
||||
<UserProfile
|
||||
user={teammate}
|
||||
disablePopover={true}
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
<p className='channel-intro-text'>
|
||||
|
||||
@@ -154,9 +154,55 @@ export const ActionTypes = keyMirror({
|
||||
SUGGESTION_SELECT_PREVIOUS: null
|
||||
});
|
||||
|
||||
export const WebrtcActionTypes = keyMirror({
|
||||
INITIALIZE: null,
|
||||
NOTIFY: null,
|
||||
CHANGED: null,
|
||||
ANSWER: null,
|
||||
DECLINE: null,
|
||||
CANCEL: null,
|
||||
NO_ANSWER: null,
|
||||
BUSY: null,
|
||||
FAILED: null,
|
||||
UNSUPPORTED: null,
|
||||
MUTED: null,
|
||||
IN_PROGRESS: null,
|
||||
DISABLED: null,
|
||||
RHS: null
|
||||
});
|
||||
|
||||
export const UserStatuses = {
|
||||
OFFLINE: 'offline',
|
||||
AWAY: 'away',
|
||||
ONLINE: 'online'
|
||||
};
|
||||
|
||||
export const SocketEvents = {
|
||||
POSTED: 'posted',
|
||||
POST_EDITED: 'post_edited',
|
||||
POST_DELETED: 'post_deleted',
|
||||
CHANNEL_DELETED: 'channel_deleted',
|
||||
CHANNEL_VIEWED: 'channel_viewed',
|
||||
DIRECT_ADDED: 'direct_added',
|
||||
NEW_USER: 'new_user',
|
||||
LEAVE_TEAM: 'leave_team',
|
||||
USER_ADDED: 'user_added',
|
||||
USER_REMOVED: 'user_removed',
|
||||
USER_UPDATED: 'user_updated',
|
||||
TYPING: 'typing',
|
||||
PREFERENCE_CHANGED: 'preference_changed',
|
||||
EPHEMERAL_MESSAGE: 'ephemeral_message',
|
||||
STATUS_CHANGED: 'status_change',
|
||||
HELLO: 'hello',
|
||||
WEBRTC: 'webrtc'
|
||||
};
|
||||
|
||||
export const Constants = {
|
||||
Preferences,
|
||||
SocketEvents,
|
||||
ActionTypes,
|
||||
WebrtcActionTypes,
|
||||
UserStatuses,
|
||||
|
||||
PayloadSources: keyMirror({
|
||||
SERVER_ACTION: null,
|
||||
@@ -183,25 +229,6 @@ export const Constants = {
|
||||
STAT_MAX_ACTIVE_USERS: 20,
|
||||
STAT_MAX_NEW_USERS: 20,
|
||||
|
||||
SocketEvents: {
|
||||
POSTED: 'posted',
|
||||
POST_EDITED: 'post_edited',
|
||||
POST_DELETED: 'post_deleted',
|
||||
CHANNEL_DELETED: 'channel_deleted',
|
||||
CHANNEL_VIEWED: 'channel_viewed',
|
||||
DIRECT_ADDED: 'direct_added',
|
||||
NEW_USER: 'new_user',
|
||||
LEAVE_TEAM: 'leave_team',
|
||||
USER_ADDED: 'user_added',
|
||||
USER_REMOVED: 'user_removed',
|
||||
USER_UPDATED: 'user_updated',
|
||||
TYPING: 'typing',
|
||||
PREFERENCE_CHANGED: 'preference_changed',
|
||||
EPHEMERAL_MESSAGE: 'ephemeral_message',
|
||||
STATUS_CHANGED: 'status_change',
|
||||
HELLO: 'hello'
|
||||
},
|
||||
|
||||
UserUpdateEvents: {
|
||||
USERNAME: 'username',
|
||||
FULLNAME: 'fullname',
|
||||
@@ -218,12 +245,6 @@ export const Constants = {
|
||||
POST: 5
|
||||
},
|
||||
|
||||
UserStatuses: {
|
||||
OFFLINE: 'offline',
|
||||
AWAY: 'away',
|
||||
ONLINE: 'online'
|
||||
},
|
||||
|
||||
SPECIAL_MENTIONS: ['all', 'channel', 'here'],
|
||||
CHARACTER_LIMIT: 4000,
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
||||
@@ -778,9 +799,15 @@ export const Constants = {
|
||||
EMBED_PREVIEW: {
|
||||
label: 'embed_preview',
|
||||
description: 'Show preview snippet of links below message'
|
||||
},
|
||||
WEBRTC_PREVIEW: {
|
||||
label: 'webrtc_preview',
|
||||
description: 'Enable WebRTC one on one calls'
|
||||
}
|
||||
},
|
||||
OVERLAY_TIME_DELAY: 400,
|
||||
WEBRTC_TIME_DELAY: 750,
|
||||
WEBRTC_CLEAR_ERROR_DELAY: 15000,
|
||||
DEFAULT_MAX_USERS_PER_TEAM: 50,
|
||||
MIN_TEAMNAME_LENGTH: 4,
|
||||
MAX_TEAMNAME_LENGTH: 15,
|
||||
|
||||
Ссылка в новой задаче
Block a user