Massive change. Changed to react-scripts (webpack) for building. Changed to material-ui where applicable. Changed to CSS-in-JS.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import debug from 'debug';
|
||||
|
||||
const APP_NAME = 'multiparty-meeting';
|
||||
|
||||
export default class Logger
|
||||
{
|
||||
constructor(prefix)
|
||||
{
|
||||
if (prefix)
|
||||
{
|
||||
this._debug = debug(`${APP_NAME}:${prefix}`);
|
||||
this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
|
||||
this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._debug = debug(APP_NAME);
|
||||
this._warn = debug(`${APP_NAME}:WARN`);
|
||||
this._error = debug(`${APP_NAME}:ERROR`);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
this._debug.log = console.info.bind(console);
|
||||
this._warn.log = console.warn.bind(console);
|
||||
this._error.log = console.error.bind(console);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
get debug()
|
||||
{
|
||||
return this._debug;
|
||||
}
|
||||
|
||||
get warn()
|
||||
{
|
||||
return this._warn;
|
||||
}
|
||||
|
||||
get error()
|
||||
{
|
||||
return this._error;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
const RoomContext = React.createContext();
|
||||
|
||||
export default RoomContext;
|
||||
|
||||
export function withRoomContext(Component)
|
||||
{
|
||||
return (props) => ( // eslint-disable-line react/display-name
|
||||
<RoomContext.Consumer>
|
||||
{(roomClient) => <Component {...props} roomClient={roomClient} />}
|
||||
</RoomContext.Consumer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
class ChromeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = { })
|
||||
{
|
||||
const state = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
window.addEventListener('message', _onExtensionMessage, false);
|
||||
window.postMessage({ type: 'getStreamId' }, '*');
|
||||
|
||||
function _onExtensionMessage({ data })
|
||||
{
|
||||
if (data.type !== 'gotStreamId')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const constraints = state._toConstraints(options, data.streamId);
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
state._stream = stream;
|
||||
resolve(stream);
|
||||
})
|
||||
.catch((err) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_toConstraints(options, streamId)
|
||||
{
|
||||
const constraints = {
|
||||
video : {
|
||||
mandatory : {
|
||||
chromeMediaSource : 'desktop',
|
||||
chromeMediaSourceId : streamId
|
||||
},
|
||||
optional : [ {
|
||||
googTemporalLayeredScreencast : true
|
||||
} ]
|
||||
},
|
||||
audio : false
|
||||
};
|
||||
|
||||
if (isFinite(options.width))
|
||||
{
|
||||
constraints.video.mandatory.maxWidth = options.width;
|
||||
constraints.video.mandatory.minWidth = options.width;
|
||||
}
|
||||
if (isFinite(options.height))
|
||||
{
|
||||
constraints.video.mandatory.maxHeight = options.height;
|
||||
constraints.video.mandatory.minHeight = options.height;
|
||||
}
|
||||
if (isFinite(options.frameRate))
|
||||
{
|
||||
constraints.video.mandatory.maxFrameRate = options.frameRate;
|
||||
constraints.video.mandatory.minFrameRate = options.frameRate;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class Chrome72ScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class FirefoxScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints(options)
|
||||
{
|
||||
const constraints = {
|
||||
video : {
|
||||
mediaSource : 'window'
|
||||
},
|
||||
audio : false
|
||||
};
|
||||
|
||||
if ('mediaSource' in options)
|
||||
{
|
||||
constraints.video.mediaSource = options.mediaSource;
|
||||
}
|
||||
if (isFinite(options.width))
|
||||
{
|
||||
constraints.video.width = {
|
||||
min : options.width,
|
||||
max : options.width
|
||||
};
|
||||
}
|
||||
if (isFinite(options.height))
|
||||
{
|
||||
constraints.video.height = {
|
||||
min : options.height,
|
||||
max : options.height
|
||||
};
|
||||
}
|
||||
if (isFinite(options.frameRate))
|
||||
{
|
||||
constraints.video.frameRate = {
|
||||
min : options.frameRate,
|
||||
max : options.frameRate
|
||||
};
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class Firefox66ScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class EdgeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultScreenShare
|
||||
{
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ScreenShare
|
||||
{
|
||||
static create(device)
|
||||
{
|
||||
switch (device.flag)
|
||||
{
|
||||
case 'firefox':
|
||||
{
|
||||
if (device.version < 66.0)
|
||||
return new FirefoxScreenShare();
|
||||
else
|
||||
return new Firefox66ScreenShare();
|
||||
}
|
||||
case 'chrome':
|
||||
{
|
||||
if (device.version < 72.0)
|
||||
return new ChromeScreenShare();
|
||||
else
|
||||
return new Chrome72ScreenShare();
|
||||
}
|
||||
case 'msedge':
|
||||
{
|
||||
return new EdgeScreenShare();
|
||||
}
|
||||
default:
|
||||
{
|
||||
return new DefaultScreenShare();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import Logger from './Logger';
|
||||
|
||||
const logger = new Logger('Spotlight');
|
||||
|
||||
export default class Spotlights extends EventEmitter
|
||||
{
|
||||
constructor(maxSpotlights, room)
|
||||
{
|
||||
super();
|
||||
|
||||
this._room = room;
|
||||
this._maxSpotlights = maxSpotlights;
|
||||
this._peerList = [];
|
||||
this._selectedSpotlights = [];
|
||||
this._currentSpotlights = [];
|
||||
this._started = false;
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
const peers = this._room.peers;
|
||||
|
||||
for (const peer of peers)
|
||||
{
|
||||
this._handlePeer(peer);
|
||||
}
|
||||
|
||||
this._handleRoom();
|
||||
|
||||
this._started = true;
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
peerInSpotlights(peerName)
|
||||
{
|
||||
if (this._started)
|
||||
{
|
||||
return this._currentSpotlights.indexOf(peerName) !== -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setPeerSpotlight(peerName)
|
||||
{
|
||||
logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName);
|
||||
|
||||
const index = this._selectedSpotlights.indexOf(peerName);
|
||||
|
||||
if (index !== -1)
|
||||
{
|
||||
this._selectedSpotlights = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._selectedSpotlights = [ peerName ];
|
||||
}
|
||||
|
||||
/*
|
||||
if (index === -1) // We don't have this peer in the list, adding
|
||||
{
|
||||
this._selectedSpotlights.push(peerName);
|
||||
}
|
||||
else // We have this peer, remove
|
||||
{
|
||||
this._selectedSpotlights.splice(index, 1);
|
||||
}
|
||||
*/
|
||||
|
||||
if (this._started)
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handleRoom()
|
||||
{
|
||||
this._room.on('newpeer', (peer) =>
|
||||
{
|
||||
logger.debug(
|
||||
'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
||||
this._handlePeer(peer);
|
||||
});
|
||||
}
|
||||
|
||||
addSpeakerList(speakerList)
|
||||
{
|
||||
this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ];
|
||||
|
||||
if (this._started)
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
|
||||
_handlePeer(peer)
|
||||
{
|
||||
logger.debug('_handlePeer() [peerName:"%s"]', peer.name);
|
||||
|
||||
if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list
|
||||
{
|
||||
peer.on('close', () =>
|
||||
{
|
||||
let index = this._peerList.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
}
|
||||
|
||||
index = this._selectedSpotlights.indexOf(peer.name);
|
||||
|
||||
if (index !== -1) // We have this peer in the list, remove
|
||||
{
|
||||
this._selectedSpotlights.splice(index, 1);
|
||||
}
|
||||
|
||||
this._spotlightsUpdated();
|
||||
});
|
||||
|
||||
logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name);
|
||||
|
||||
this._peerList.push(peer.name);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
handleActiveSpeaker(peerName)
|
||||
{
|
||||
logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName);
|
||||
|
||||
const index = this._peerList.indexOf(peerName);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this._peerList.splice(index, 1);
|
||||
this._peerList = [ peerName ].concat(this._peerList);
|
||||
|
||||
this._spotlightsUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
_spotlightsUpdated()
|
||||
{
|
||||
let spotlights;
|
||||
|
||||
if (this._selectedSpotlights.length > 0)
|
||||
{
|
||||
spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
spotlights = this._peerList;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._arraysEqual(
|
||||
this._currentSpotlights, spotlights.slice(0, this._maxSpotlights)
|
||||
)
|
||||
)
|
||||
{
|
||||
logger.debug('_spotlightsUpdated() | spotlights updated, emitting');
|
||||
|
||||
this._currentSpotlights = spotlights.slice(0, this._maxSpotlights);
|
||||
this.emit('spotlights-updated', this._currentSpotlights);
|
||||
}
|
||||
else
|
||||
logger.debug('_spotlightsUpdated() | spotlights not updated');
|
||||
}
|
||||
|
||||
_arraysEqual(arr1, arr2)
|
||||
{
|
||||
if (arr1.length !== arr2.length)
|
||||
return false;
|
||||
|
||||
for (let i = arr1.length; i--;)
|
||||
{
|
||||
if (arr1[i] !== arr2[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import randomString from 'random-string';
|
||||
import * as stateActions from './stateActions';
|
||||
|
||||
// This returns a redux-thunk action (a function).
|
||||
export const notify = ({ type = 'info', text, timeout }) =>
|
||||
{
|
||||
if (!timeout)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 'info':
|
||||
timeout = 3000;
|
||||
break;
|
||||
case 'error':
|
||||
timeout = 5000;
|
||||
break;
|
||||
default:
|
||||
timeout = 3000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const notification =
|
||||
{
|
||||
id : randomString({ length: 6 }).toLowerCase(),
|
||||
type : type,
|
||||
text : text,
|
||||
timeout : timeout
|
||||
};
|
||||
|
||||
return (dispatch) =>
|
||||
{
|
||||
dispatch(stateActions.addNotification(notification));
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
dispatch(stateActions.removeNotification(notification.id));
|
||||
}, timeout);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,576 @@
|
||||
export const setRoomUrl = (url) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_URL',
|
||||
payload : { url }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomState = (state) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_STATE',
|
||||
payload : { state }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomActiveSpeaker = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_ACTIVE_SPEAKER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomLocked = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_LOCKED'
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomUnLocked = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_UNLOCKED'
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomLockedOut = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_LOCKED_OUT'
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioSuspended = ({ audioSuspended }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_SUSPENDED',
|
||||
payload : { audioSuspended }
|
||||
};
|
||||
};
|
||||
|
||||
export const setSettingsOpen = ({ settingsOpen }) =>
|
||||
({
|
||||
type : 'SET_SETTINGS_OPEN',
|
||||
payload : { settingsOpen }
|
||||
});
|
||||
|
||||
export const setMe = ({ peerName, displayName, displayNameSet, device, loginEnabled }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ME',
|
||||
payload : { peerName, displayName, displayNameSet, device, loginEnabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MEDIA_CAPABILITIES',
|
||||
payload : { canSendMic, canSendWebcam }
|
||||
};
|
||||
};
|
||||
|
||||
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_SCREEN_CAPABILITIES',
|
||||
payload : { canShareScreen, needExtension }
|
||||
};
|
||||
};
|
||||
|
||||
export const setCanChangeAudioDevice = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CAN_CHANGE_AUDIO_DEVICE',
|
||||
payload : flag
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioDevices = (devices) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_DEVICES',
|
||||
payload : { devices }
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamDevices = (devices) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_WEBCAM_DEVICES',
|
||||
payload : { devices }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileSharingSupported = (supported) =>
|
||||
{
|
||||
return {
|
||||
type : 'FILE_SHARING_SUPPORTED',
|
||||
payload : { supported }
|
||||
};
|
||||
};
|
||||
|
||||
export const setDisplayName = (displayName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_DISPLAY_NAME',
|
||||
payload : { displayName }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleAdvancedMode = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_ADVANCED_MODE'
|
||||
};
|
||||
};
|
||||
|
||||
export const setDisplayMode = (mode) =>
|
||||
({
|
||||
type : 'SET_DISPLAY_MODE',
|
||||
payload : { mode }
|
||||
});
|
||||
|
||||
export const setAudioOnlyState = (enabled) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_STATE',
|
||||
payload : { enabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioOnlyInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerVideoInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_VIDEO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerAudioInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_AUDIO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerScreenInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_SCREEN_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMyRaiseHandState = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MY_RAISE_HAND_STATE',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleSettings = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_SETTINGS'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const openToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'OPEN_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const closeToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'CLOSE_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const setToolTab = (toolTab) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_TOOL_TAB',
|
||||
payload : { toolTab }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMyRaiseHandStateInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_RAISE_HAND_STATE',
|
||||
payload : { peerName, raiseHandState }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRestartIceInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_RESTART_ICE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addProducer = (producer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PRODUCER',
|
||||
payload : { producer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeProducer = (producerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PRODUCER',
|
||||
payload : { producerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerPaused = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_PAUSED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerResumed = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_RESUMED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerTrack = (producerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_TRACK',
|
||||
payload : { producerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_WEBCAM_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setScreenShareInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_SCREEN_SHARE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addPeer = (peer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PEER',
|
||||
payload : { peer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removePeer = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PEER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerDisplayName = (displayName, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_DISPLAY_NAME',
|
||||
payload : { displayName, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const addConsumer = (consumer, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CONSUMER',
|
||||
payload : { consumer, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeConsumer = (consumerId, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_CONSUMER',
|
||||
payload : { consumerId, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerPaused = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_PAUSED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerResumed = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_RESUMED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerEffectiveProfile = (consumerId, profile) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_EFFECTIVE_PROFILE',
|
||||
payload : { consumerId, profile }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerTrack = (consumerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_TRACK',
|
||||
payload : { consumerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerVolume = (consumerId, volume) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_VOLUME',
|
||||
payload : { consumerId, volume }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerVolume = (producerId, volume) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_VOLUME',
|
||||
payload : { producerId, volume }
|
||||
};
|
||||
};
|
||||
|
||||
export const addNotification = (notification) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NOTIFICATION',
|
||||
payload : { notification }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeNotification = (notificationId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_NOTIFICATION',
|
||||
payload : { notificationId }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeAllNotifications = () =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_ALL_NOTIFICATIONS'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleChat = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_CHAT'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleConsumerFullscreen = (consumerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_FULLSCREEN_CONSUMER',
|
||||
payload : { consumerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleConsumerWindow = (consumerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_WINDOW_CONSUMER',
|
||||
payload : { consumerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setToolbarsVisible = (toolbarsVisible) => ({
|
||||
type : 'SET_TOOLBARS_VISIBLE',
|
||||
payload : { toolbarsVisible }
|
||||
});
|
||||
|
||||
export const increaseBadge = () =>
|
||||
{
|
||||
return {
|
||||
type : 'INCREASE_BADGE'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleInputDisabled = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_INPUT_DISABLED'
|
||||
};
|
||||
};
|
||||
|
||||
export const addUserMessage = (text) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_USER_MESSAGE',
|
||||
payload : { text }
|
||||
};
|
||||
};
|
||||
|
||||
export const addUserFile = (file) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_USER_FILE',
|
||||
payload : { file }
|
||||
};
|
||||
};
|
||||
|
||||
export const addResponseMessage = (message) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_RESPONSE_MESSAGE',
|
||||
payload : { message }
|
||||
};
|
||||
};
|
||||
|
||||
export const addChatHistory = (chatHistory) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CHAT_HISTORY',
|
||||
payload : { chatHistory }
|
||||
};
|
||||
};
|
||||
|
||||
export const dropMessages = () =>
|
||||
{
|
||||
return {
|
||||
type : 'DROP_MESSAGES'
|
||||
};
|
||||
};
|
||||
|
||||
export const addFile = (file) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_FILE',
|
||||
payload : { file }
|
||||
};
|
||||
};
|
||||
|
||||
export const addFileHistory = (fileHistory) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_FILE_HISTORY',
|
||||
payload : { fileHistory }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileActive = (magnetUri) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_ACTIVE',
|
||||
payload : { magnetUri }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileInActive = (magnetUri) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_INACTIVE',
|
||||
payload : { magnetUri }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileProgress = (magnetUri, progress) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_PROGRESS',
|
||||
payload : { magnetUri, progress }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileDone = (magnetUri, sharedFiles) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_DONE',
|
||||
payload : { magnetUri, sharedFiles }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPicture = (picture) =>
|
||||
({
|
||||
type : 'SET_PICTURE',
|
||||
payload : { picture }
|
||||
});
|
||||
|
||||
export const setPeerPicture = (peerName, picture) =>
|
||||
({
|
||||
type : 'SET_PEER_PICTURE',
|
||||
payload : { peerName, picture }
|
||||
});
|
||||
|
||||
export const loggedIn = () =>
|
||||
({
|
||||
type : 'LOGGED_IN'
|
||||
});
|
||||
|
||||
export const setSelectedPeer = (selectedPeerName) =>
|
||||
({
|
||||
type : 'SET_SELECTED_PEER',
|
||||
payload : { selectedPeerName }
|
||||
});
|
||||
|
||||
export const setSpotlights = (spotlights) =>
|
||||
({
|
||||
type : 'SET_SPOTLIGHTS',
|
||||
payload : { spotlights }
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import BuddyImage from '../../images/buddy.svg';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
width : '12vmin',
|
||||
height : '9vmin',
|
||||
position : 'absolute',
|
||||
bottom : '3%',
|
||||
right : '3%',
|
||||
color : 'rgba(170, 170, 170, 1)',
|
||||
cursor : 'pointer',
|
||||
backgroundImage : `url(${BuddyImage})`,
|
||||
backgroundColor : 'rgba(42, 75, 88, 1)',
|
||||
backgroundPosition : 'bottom',
|
||||
backgroundSize : 'auto 85%',
|
||||
backgroundRepeat : 'no-repeat',
|
||||
border : 'var(--peer-border)',
|
||||
boxShadow : 'var(--peer-shadow)',
|
||||
textAlign : 'center',
|
||||
verticalAlign : 'middle',
|
||||
lineHeight : '1.8vmin',
|
||||
fontSize : '1.7vmin',
|
||||
fontWeight : 'bolder',
|
||||
animation : 'none',
|
||||
'&.pulse' :
|
||||
{
|
||||
animation : 'pulse 0.5s'
|
||||
}
|
||||
},
|
||||
'@keyframes pulse' :
|
||||
{
|
||||
'0%' :
|
||||
{
|
||||
transform : 'scale3d(1, 1, 1)'
|
||||
},
|
||||
'50%' :
|
||||
{
|
||||
transform : 'scale3d(1.2, 1.2, 1.2)'
|
||||
},
|
||||
'100%' :
|
||||
{
|
||||
transform : 'scale3d(1, 1, 1)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class HiddenPeers extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
this.state = { className: '' };
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps)
|
||||
{
|
||||
const { hiddenPeersCount } = this.props;
|
||||
|
||||
if (hiddenPeersCount !== prevProps.hiddenPeersCount)
|
||||
{
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ className: 'pulse' }, () =>
|
||||
{
|
||||
if (this.timeout)
|
||||
{
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() =>
|
||||
{
|
||||
this.setState({ className: '' });
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
hiddenPeersCount,
|
||||
openUsersTab,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classnames(classes.root, this.state.className)} onClick={() => openUsersTab()}>
|
||||
<p>+{hiddenPeersCount} <br /> participant
|
||||
{(hiddenPeersCount === 1) ? null : 's'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HiddenPeers.propTypes =
|
||||
{
|
||||
hiddenPeersCount : PropTypes.number,
|
||||
openUsersTab : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
openUsersTab : () =>
|
||||
{
|
||||
dispatch(stateActions.openToolArea());
|
||||
dispatch(stateActions.setToolTab('users'));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(HiddenPeers));
|
||||
@@ -0,0 +1,332 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import PeerView from '../VideoContainers/PeerView';
|
||||
import ScreenView from '../VideoContainers/ScreenView';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import VideoIcon from '@material-ui/icons/Videocam';
|
||||
import VideoOffIcon from '@material-ui/icons/VideocamOff';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
flex : '100 100 auto',
|
||||
position : 'relative'
|
||||
},
|
||||
viewContainer :
|
||||
{
|
||||
position : 'relative',
|
||||
width : 'var(--me-width)',
|
||||
height : 'var(--me-height)',
|
||||
'&.webcam' :
|
||||
{
|
||||
order : 2
|
||||
},
|
||||
'&.screen' :
|
||||
{
|
||||
order : 1
|
||||
}
|
||||
},
|
||||
controls :
|
||||
{
|
||||
position : 'absolute',
|
||||
right : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
padding : '0.4vmin',
|
||||
zIndex : 20,
|
||||
opacity : 0,
|
||||
transition : 'opacity 0.3s',
|
||||
'&.visible' :
|
||||
{
|
||||
opacity : 1
|
||||
}
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.2vmin',
|
||||
borderRadius : 2,
|
||||
opacity : 0.85,
|
||||
width : 'var(--media-control-button-size)',
|
||||
height : 'var(--media-control-button-size)',
|
||||
backgroundColor : 'var(--media-control-button-color)',
|
||||
'&:hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.unsupported' :
|
||||
{
|
||||
pointerEvents : 'none'
|
||||
},
|
||||
'&.disabled' :
|
||||
{
|
||||
pointerEvents : 'none',
|
||||
backgroundColor : 'var(--media-control-botton-disabled)'
|
||||
},
|
||||
'&.on' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-on)'
|
||||
},
|
||||
'&.off' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-off)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class Me extends React.Component
|
||||
{
|
||||
state = {
|
||||
controlsVisible : false
|
||||
};
|
||||
|
||||
handleMouseOver = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : true
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOut = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : false
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._mounted = false;
|
||||
this._rootNode = null;
|
||||
this._tooltip = true;
|
||||
|
||||
// TODO: Issue when using react-tooltip in Edge:
|
||||
// https://github.com/wwayne/react-tooltip/issues/328
|
||||
if (getDeviceInfo().flag === 'msedge')
|
||||
this._tooltip = false;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
connected,
|
||||
me,
|
||||
advancedMode,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
screenProducer,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
let micState;
|
||||
|
||||
if (!me.canSendMic)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
|
||||
micState = 'on';
|
||||
else
|
||||
micState = 'off';
|
||||
|
||||
let webcamState;
|
||||
|
||||
if (!me.canSendWebcam)
|
||||
webcamState = 'unsupported';
|
||||
else if (webcamProducer)
|
||||
webcamState = 'on';
|
||||
else
|
||||
webcamState = 'off';
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamProducer) &&
|
||||
!webcamProducer.locallyPaused &&
|
||||
!webcamProducer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenProducer) &&
|
||||
!screenProducer.locallyPaused &&
|
||||
!screenProducer.remotelyPaused
|
||||
);
|
||||
|
||||
let tip;
|
||||
|
||||
if (!me.displayNameSet)
|
||||
tip = 'Click on your name to change it';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.root}
|
||||
ref={(node) => (this._rootNode = node)}
|
||||
data-tip={tip}
|
||||
data-tip-disable={!tip}
|
||||
data-type='dark'
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<div className={classnames(classes.viewContainer, 'webcam')}>
|
||||
{ connected ?
|
||||
<div className={classnames(classes.controls, 'visible')}>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘m‘'
|
||||
data-type='dark'
|
||||
data-place='bottom'
|
||||
data-for='me'
|
||||
className={classnames(classes.button, 'mic', micState, {
|
||||
disabled : me.audioInProgress,
|
||||
visible : micState === 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ?
|
||||
roomClient.muteMic() :
|
||||
roomClient.unmuteMic();
|
||||
}}
|
||||
>
|
||||
{ micState === 'on' ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</div>
|
||||
<ReactTooltip
|
||||
id='me'
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
className={classnames(classes.button, 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress,
|
||||
visible : webcamState === 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
>
|
||||
{ webcamState === 'on' ?
|
||||
<VideoIcon />
|
||||
:
|
||||
<VideoOffIcon />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<PeerView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
peer={me}
|
||||
audioTrack={micProducer ? micProducer.track : null}
|
||||
volume={micProducer ? micProducer.volume : null}
|
||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
audioCodec={micProducer ? micProducer.codec : null}
|
||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||
onChangeDisplayName={(displayName) =>
|
||||
{
|
||||
roomClient.changeDisplayName(displayName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ screenProducer ?
|
||||
<div className={classnames(classes.viewContainer, 'screen')}>
|
||||
<ScreenView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenProducer ? screenProducer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenCodec={screenProducer ? screenProducer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._mounted = true;
|
||||
|
||||
if (this._tooltip)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (!this._mounted || this.props.me.displayNameSet)
|
||||
return;
|
||||
|
||||
ReactTooltip.show(this._rootNode);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
if (this._tooltip)
|
||||
{
|
||||
if (nextProps.me.displayNameSet)
|
||||
ReactTooltip.hide(this._rootNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Me.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
connected : PropTypes.bool.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const micProducer =
|
||||
producersArray.find((producer) => producer.source === 'mic');
|
||||
const webcamProducer =
|
||||
producersArray.find((producer) => producer.source === 'webcam');
|
||||
const screenProducer =
|
||||
producersArray.find((producer) => producer.source === 'screen');
|
||||
|
||||
return {
|
||||
connected : state.room.state === 'connected',
|
||||
me : state.me,
|
||||
micProducer : micProducer,
|
||||
webcamProducer : webcamProducer,
|
||||
screenProducer : screenProducer
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(withStyles(styles)(Me)));
|
||||
@@ -0,0 +1,397 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import PeerView from '../VideoContainers/PeerView';
|
||||
import ScreenView from '../VideoContainers/ScreenView';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import NewWindowIcon from '@material-ui/icons/OpenInNew';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
flex : '100 100 auto',
|
||||
position : 'relative'
|
||||
},
|
||||
viewContainer :
|
||||
{
|
||||
position : 'relative',
|
||||
flexGrow : 1,
|
||||
'&.webcam' :
|
||||
{
|
||||
order : 2
|
||||
},
|
||||
'&.screen' :
|
||||
{
|
||||
order : 1,
|
||||
maxWidth : '50%'
|
||||
}
|
||||
},
|
||||
controls :
|
||||
{
|
||||
position : 'absolute',
|
||||
right : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin',
|
||||
zIndex : 20,
|
||||
opacity : 0,
|
||||
transition : 'opacity 0.3s',
|
||||
'&.visible' :
|
||||
{
|
||||
opacity : 1
|
||||
}
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.2vmin',
|
||||
borderRadius : 2,
|
||||
opacity : 0.85,
|
||||
width : 'var(--media-control-button-size)',
|
||||
height : 'var(--media-control-button-size)',
|
||||
backgroundColor : 'var(--media-control-button-color)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
'&:hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.unsupported' :
|
||||
{
|
||||
pointerEvents : 'none'
|
||||
},
|
||||
'&.disabled' :
|
||||
{
|
||||
pointerEvents : 'none',
|
||||
backgroundColor : 'var(--media-control-botton-disabled)'
|
||||
},
|
||||
'&.on' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-on)'
|
||||
},
|
||||
'&.off' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-off)'
|
||||
}
|
||||
},
|
||||
pausedVideo :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 11,
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
left : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
borderRadius : 6,
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 20,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
},
|
||||
incompatibleVideo :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 10,
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
left : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
borderRadius : 6,
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 20,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class Peer extends Component
|
||||
{
|
||||
state = {
|
||||
controlsVisible : false
|
||||
};
|
||||
|
||||
handleMouseOver = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : true
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOut = () =>
|
||||
{
|
||||
this.setState({
|
||||
controlsVisible : false
|
||||
});
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
toggleConsumerFullscreen,
|
||||
toggleConsumerWindow,
|
||||
style,
|
||||
windowConsumer,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
let videoProfile;
|
||||
|
||||
if (webcamConsumer)
|
||||
videoProfile = webcamConsumer.profile;
|
||||
|
||||
let screenProfile;
|
||||
|
||||
if (screenConsumer)
|
||||
screenProfile = screenConsumer.profile;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(classes.root, {
|
||||
screen : screenConsumer
|
||||
})}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
{ videoVisible && !webcamConsumer.supported ?
|
||||
<div className={classes.incompatibleVideo}>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
{ !videoVisible ?
|
||||
<div className={classes.pausedVideo}>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classnames(classes.viewContainer, 'webcam')} style={style}>
|
||||
<div
|
||||
className={classnames(classes.controls, {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames(classes.button, {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames(classes.button, {
|
||||
disabled : !videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<NewWindowIcon />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames(classes.button, 'fullscreen', {
|
||||
disabled : !videoVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PeerView
|
||||
advancedMode={advancedMode}
|
||||
peer={peer}
|
||||
volume={micConsumer ? micConsumer.volume : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ screenConsumer ?
|
||||
<div className={classnames(classes.viewContainer, 'screen')} style={style}>
|
||||
<div
|
||||
className={classnames(classes.controls, {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames(classes.button, 'newwindow', {
|
||||
disabled : !screenVisible ||
|
||||
(windowConsumer === screenConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
}}
|
||||
>
|
||||
<NewWindowIcon />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames(classes.button, 'fullscreen', {
|
||||
disabled : !screenVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(screenConsumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenIcon />
|
||||
</div>
|
||||
</div>
|
||||
<ScreenView
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenConsumer ? screenConsumer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenProfile={screenProfile}
|
||||
screenCodec={screenConsumer ? screenConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Peer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
windowConsumer : PropTypes.number,
|
||||
streamDimensions : PropTypes.object,
|
||||
style : PropTypes.object,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer,
|
||||
windowConsumer : state.room.windowConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
},
|
||||
toggleConsumerWindow : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerWindow(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Peer)));
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RIEInput } from 'riek';
|
||||
|
||||
export default class EditableInput extends React.Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
value,
|
||||
propName,
|
||||
className,
|
||||
classLoading,
|
||||
classInvalid,
|
||||
editProps,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<RIEInput
|
||||
value={value}
|
||||
propName={propName}
|
||||
className={className}
|
||||
classLoading={classLoading}
|
||||
classInvalid={classInvalid}
|
||||
shouldBlockWhileLoading
|
||||
editProps={editProps}
|
||||
change={(data) => onChange(data)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps)
|
||||
{
|
||||
if (nextProps.value === this.props.value)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EditableInput.propTypes =
|
||||
{
|
||||
value : PropTypes.string,
|
||||
propName : PropTypes.string.isRequired,
|
||||
className : PropTypes.string,
|
||||
classLoading : PropTypes.string,
|
||||
classInvalid : PropTypes.string,
|
||||
editProps : PropTypes.any,
|
||||
onChange : PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import ScreenIcon from '@material-ui/icons/ScreenShare';
|
||||
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import LockIcon from '@material-ui/icons/Lock';
|
||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||
import HandOff from '../../images/icon-hand-black.svg';
|
||||
import HandOn from '../../images/icon-hand-white.svg';
|
||||
import LeaveIcon from '@material-ui/icons/Cancel';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
position : 'fixed',
|
||||
zIndex : 500,
|
||||
top : '50%',
|
||||
transform : 'translate(0%, -50%)',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
left : '1.0em',
|
||||
width : '2.6em'
|
||||
},
|
||||
fab :
|
||||
{
|
||||
margin: theme.spacing.unit
|
||||
}
|
||||
});
|
||||
|
||||
class Sidebar extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
toolbarsVisible,
|
||||
me,
|
||||
screenProducer,
|
||||
locked,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
let screenState;
|
||||
|
||||
if (me.needExtension)
|
||||
{
|
||||
screenState = 'need-extension';
|
||||
}
|
||||
else if (!me.canShareScreen)
|
||||
{
|
||||
screenState = 'unsupported';
|
||||
}
|
||||
else if (screenProducer)
|
||||
{
|
||||
screenState = 'on';
|
||||
}
|
||||
else
|
||||
{
|
||||
screenState = 'off';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(classes.root, 'room-controls', {
|
||||
'visible' : toolbarsVisible
|
||||
})}
|
||||
>
|
||||
<Fab
|
||||
aria-label='Share screen'
|
||||
className={classes.fab}
|
||||
disabled={!me.canShareScreen || me.screenShareInProgress}
|
||||
color={screenState === 'on' ? 'secondary' : 'default'}
|
||||
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
roomClient.disableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'off':
|
||||
{
|
||||
roomClient.enableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'need-extension':
|
||||
{
|
||||
roomClient.installExtension();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ screenState === 'on' || screenState === 'unsupported' ?
|
||||
<ScreenOffIcon/>
|
||||
:null
|
||||
}
|
||||
{ screenState === 'off' ?
|
||||
<ScreenIcon/>
|
||||
:null
|
||||
}
|
||||
{ screenState === 'need-extension' ?
|
||||
<ExtensionIcon/>
|
||||
:null
|
||||
}
|
||||
</Fab>
|
||||
|
||||
<Fab
|
||||
aria-label='Room lock'
|
||||
className={classes.fab}
|
||||
color={locked ? 'secondary' : 'default'}
|
||||
onClick={() =>
|
||||
{
|
||||
if (locked)
|
||||
{
|
||||
roomClient.unlockRoom();
|
||||
}
|
||||
else
|
||||
{
|
||||
roomClient.lockRoom();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ locked ?
|
||||
<LockIcon />
|
||||
:
|
||||
<LockOpenIcon />
|
||||
}
|
||||
</Fab>
|
||||
|
||||
<Fab
|
||||
aria-label='Raise hand'
|
||||
className={classes.fab}
|
||||
disabled={me.raiseHandInProgress}
|
||||
color={me.raiseHand ? 'secondary' : 'default'}
|
||||
onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
|
||||
>
|
||||
<Avatar alt='Hand' src={me.raiseHand ? HandOn : HandOff} />
|
||||
</Fab>
|
||||
|
||||
<Fab
|
||||
aria-label='Leave meeting'
|
||||
className={classes.fab}
|
||||
color='secondary'
|
||||
onClick={() => roomClient.close()}
|
||||
>
|
||||
<LeaveIcon />
|
||||
</Fab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
toolbarsVisible : PropTypes.bool.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
locked : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
toolbarsVisible : state.room.toolbarsVisible,
|
||||
screenProducer : Object.values(state.producers)
|
||||
.find((producer) => producer.source === 'screen'),
|
||||
me : state.me,
|
||||
locked : state.room.locked
|
||||
});
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(Sidebar)));
|
||||
@@ -0,0 +1,108 @@
|
||||
const key = {
|
||||
fullscreenEnabled : 0,
|
||||
fullscreenElement : 1,
|
||||
requestFullscreen : 2,
|
||||
exitFullscreen : 3,
|
||||
fullscreenchange : 4,
|
||||
fullscreenerror : 5
|
||||
};
|
||||
|
||||
const webkit = [
|
||||
'webkitFullscreenEnabled',
|
||||
'webkitFullscreenElement',
|
||||
'webkitRequestFullscreen',
|
||||
'webkitExitFullscreen',
|
||||
'webkitfullscreenchange',
|
||||
'webkitfullscreenerror'
|
||||
];
|
||||
|
||||
const moz = [
|
||||
'mozFullScreenEnabled',
|
||||
'mozFullScreenElement',
|
||||
'mozRequestFullScreen',
|
||||
'mozCancelFullScreen',
|
||||
'mozfullscreenchange',
|
||||
'mozfullscreenerror'
|
||||
];
|
||||
|
||||
const ms = [
|
||||
'msFullscreenEnabled',
|
||||
'msFullscreenElement',
|
||||
'msRequestFullscreen',
|
||||
'msExitFullscreen',
|
||||
'MSFullscreenChange',
|
||||
'MSFullscreenError'
|
||||
];
|
||||
|
||||
export default class FullScreen
|
||||
{
|
||||
constructor(document)
|
||||
{
|
||||
this.document = document;
|
||||
this.vendor = (
|
||||
('fullscreenEnabled' in this.document && Object.keys(key)) ||
|
||||
(webkit[0] in this.document && webkit) ||
|
||||
(moz[0] in this.document && moz) ||
|
||||
(ms[0] in this.document && ms) ||
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
requestFullscreen(element)
|
||||
{
|
||||
element[this.vendor[key.requestFullscreen]]();
|
||||
}
|
||||
|
||||
requestFullscreenFunction(element)
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
element[this.vendor[key.requestFullscreen]];
|
||||
}
|
||||
|
||||
addEventListener(type, handler)
|
||||
{
|
||||
this.document.addEventListener(this.vendor[key[type]], handler);
|
||||
}
|
||||
|
||||
removeEventListener(type, handler)
|
||||
{
|
||||
this.document.removeEventListener(this.vendor[key[type]], handler);
|
||||
}
|
||||
|
||||
get exitFullscreen()
|
||||
{
|
||||
return this.document[this.vendor[key.exitFullscreen]].bind(this.document);
|
||||
}
|
||||
|
||||
get fullscreenEnabled()
|
||||
{
|
||||
return Boolean(this.document[this.vendor[key.fullscreenEnabled]]);
|
||||
}
|
||||
set fullscreenEnabled(val) {}
|
||||
|
||||
get fullscreenElement()
|
||||
{
|
||||
return this.document[this.vendor[key.fullscreenElement]];
|
||||
}
|
||||
set fullscreenElement(val) {}
|
||||
|
||||
get onfullscreenchange()
|
||||
{
|
||||
return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()];
|
||||
}
|
||||
|
||||
set onfullscreenchange(handler)
|
||||
{
|
||||
this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler;
|
||||
}
|
||||
|
||||
get onfullscreenerror()
|
||||
{
|
||||
return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()];
|
||||
}
|
||||
|
||||
set onfullscreenerror(handler)
|
||||
{
|
||||
this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.LoadingView
|
||||
{
|
||||
height : 100%;
|
||||
width : 100%;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import './LoadingView.css';
|
||||
|
||||
const LoadingView = () =>
|
||||
{
|
||||
return (
|
||||
<div className='LoadingView' />
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingView;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import MessageList from './MessageList';
|
||||
import ChatInput from './ChatInput';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
width : '100%',
|
||||
height : '100%'
|
||||
}
|
||||
});
|
||||
|
||||
const Chat = (props) =>
|
||||
{
|
||||
const {
|
||||
classes
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<MessageList />
|
||||
<ChatInput />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
Chat.propTypes =
|
||||
{
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Chat);
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import InputBase from '@material-ui/core/InputBase';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import SendIcon from '@material-ui/icons/Send';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : theme.spacing.unit,
|
||||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
borderRadius : 0
|
||||
},
|
||||
input :
|
||||
{
|
||||
marginLeft : 8,
|
||||
flex : 1
|
||||
},
|
||||
iconButton :
|
||||
{
|
||||
padding : 10
|
||||
}
|
||||
});
|
||||
|
||||
class ChatInput extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
message : ''
|
||||
};
|
||||
}
|
||||
|
||||
createNewMessage = (text, sender, name, picture) =>
|
||||
({
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
});
|
||||
|
||||
handleChange = (e) =>
|
||||
{
|
||||
this.setState({ message: e.target.value });
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
displayName,
|
||||
picture,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder='Enter chat message...'
|
||||
value={this.state.message || ''}
|
||||
onChange={this.handleChange}
|
||||
onKeyPress={(ev) =>
|
||||
{
|
||||
if (ev.key === 'Enter')
|
||||
{
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.state.message && this.state.message !== '')
|
||||
{
|
||||
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
|
||||
|
||||
roomClient.sendChatMessage(message);
|
||||
|
||||
this.setState({ message: '' });
|
||||
}
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<IconButton
|
||||
color='primary'
|
||||
className={classes.iconButton}
|
||||
aria-label='Send'
|
||||
onClick={() =>
|
||||
{
|
||||
if (this.state.message && this.state.message !== '')
|
||||
{
|
||||
const message = this.createNewMessage(this.state.message, 'response', displayName, picture);
|
||||
|
||||
roomClient.sendChatMessage(message);
|
||||
|
||||
this.setState({ message: '' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChatInput.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
picture : PropTypes.string,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
displayName : state.me.displayName,
|
||||
picture : state.me.picture
|
||||
});
|
||||
|
||||
export default withRoomContext(
|
||||
connect(mapStateToProps)(withStyles(styles)(ChatInput))
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import marked from 'marked';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const linkRenderer = new marked.Renderer();
|
||||
|
||||
linkRenderer.link = (href, title, text) =>
|
||||
{
|
||||
title = title ? title : href;
|
||||
text = text ? text : href;
|
||||
|
||||
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
|
||||
};
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
marginBottom : theme.spacing.unit,
|
||||
padding : theme.spacing.unit
|
||||
},
|
||||
selfMessage :
|
||||
{
|
||||
marginLeft : 'auto',
|
||||
},
|
||||
remoteMessage :
|
||||
{
|
||||
marginRight : 'auto'
|
||||
},
|
||||
text :
|
||||
{
|
||||
'& p' :
|
||||
{
|
||||
margin : 0
|
||||
}
|
||||
},
|
||||
content :
|
||||
{
|
||||
marginLeft : theme.spacing.unit
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem',
|
||||
alignSelf : 'center'
|
||||
},
|
||||
});
|
||||
|
||||
const Message = (props) =>
|
||||
{
|
||||
const {
|
||||
self,
|
||||
picture,
|
||||
text,
|
||||
time,
|
||||
name,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className={classnames(
|
||||
classes.root,
|
||||
self ? classes.selfMessage : classes.remoteMessage
|
||||
)}
|
||||
>
|
||||
<img alt='Avatar' className={classes.avatar} src={picture} />
|
||||
<div className={classes.content}>
|
||||
<Typography
|
||||
className={classes.text}
|
||||
variant='subtitle1'
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html : marked.parse(
|
||||
text,
|
||||
{ sanitize: true, renderer: linkRenderer }
|
||||
) }}
|
||||
/>
|
||||
<Typography variant='caption'>{self ? 'Me' : name} - {time}</Typography>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
Message.propTypes =
|
||||
{
|
||||
self : PropTypes.bool,
|
||||
picture : PropTypes.string,
|
||||
text : PropTypes.string,
|
||||
time : PropTypes.string,
|
||||
name : PropTypes.string,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Message);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Message from './Message';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
height : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
alignItems : 'center',
|
||||
overflowY : 'auto',
|
||||
padding : theme.spacing.unit
|
||||
}
|
||||
});
|
||||
|
||||
class MessageList extends React.Component
|
||||
{
|
||||
componentDidMount()
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate()
|
||||
{
|
||||
return this.node.scrollTop
|
||||
+ this.node.offsetHeight === this.node.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, shouldScroll)
|
||||
{
|
||||
if (shouldScroll)
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeString(time)
|
||||
{
|
||||
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
chatmessages,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={(node) => { this.node = node; }}>
|
||||
{
|
||||
chatmessages.map((message, index) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
|
||||
const picture = (message.sender === 'response' ?
|
||||
message.picture : this.props.myPicture) || EmptyAvatar;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={index}
|
||||
self={message.sender === 'client'}
|
||||
picture={picture}
|
||||
text={message.text}
|
||||
time={this.getTimeString(messageTime)}
|
||||
name={message.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageList.propTypes =
|
||||
{
|
||||
chatmessages : PropTypes.array,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
chatmessages : state.chatmessages
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles)(MessageList));
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import magnet from 'magnet-uri';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
width : '100%',
|
||||
padding : theme.spacing.unit,
|
||||
boxShadow : '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
|
||||
'&:not(:last-child)' :
|
||||
{
|
||||
marginBottom : theme.spacing.unit
|
||||
}
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem'
|
||||
},
|
||||
text :
|
||||
{
|
||||
margin : 0,
|
||||
padding : theme.spacing.unit
|
||||
},
|
||||
fileContent :
|
||||
{
|
||||
display : 'flex',
|
||||
alignItems : 'center'
|
||||
},
|
||||
fileInfo :
|
||||
{
|
||||
display : 'flex',
|
||||
alignItems : 'center',
|
||||
padding : theme.spacing.unit
|
||||
},
|
||||
button :
|
||||
{
|
||||
marginRight : 'auto'
|
||||
}
|
||||
});
|
||||
|
||||
class File extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
file,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<img alt='Peer avatar' className={classes.avatar} src={file.picture || EmptyAvatar} />
|
||||
|
||||
<div className={classes.fileContent}>
|
||||
{ file.files ?
|
||||
<Fragment>
|
||||
<Typography className={classes.text}>
|
||||
File finished downloading
|
||||
</Typography>
|
||||
|
||||
{ file.files.map((sharedFile, i) => (
|
||||
<div className={classes.fileInfo} key={i}>
|
||||
<Typography className={classes.text}>
|
||||
{sharedFile.name}
|
||||
</Typography>
|
||||
<Button
|
||||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.saveFile(sharedFile);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
:null
|
||||
}
|
||||
<Typography className={classes.text}>
|
||||
{ file.me ?
|
||||
'You shared a file'
|
||||
:
|
||||
`${file.displayName} shared a file`
|
||||
}
|
||||
</Typography>
|
||||
|
||||
{ !file.active && !file.files ?
|
||||
<div className={classes.fileInfo}>
|
||||
<Typography className={classes.text}>
|
||||
{magnet.decode(file.magnetUri).dn}
|
||||
</Typography>
|
||||
{ torrentSupport ?
|
||||
<Button
|
||||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
:
|
||||
<Typography className={classes.text}>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
{ file.timeout ?
|
||||
<Typography className={classes.text}>
|
||||
If this process takes a long time, there might not be anyone seeding
|
||||
this torrent. Try asking someone to reupload the file that you want.
|
||||
</Typography>
|
||||
:null
|
||||
}
|
||||
|
||||
{ file.active ?
|
||||
<progress value={file.progress} />
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(File)));
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import File from './File';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
height : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
alignItems : 'center',
|
||||
overflowY : 'auto',
|
||||
padding : theme.spacing.unit
|
||||
}
|
||||
});
|
||||
|
||||
class FileList extends Component
|
||||
{
|
||||
componentDidMount()
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate()
|
||||
{
|
||||
return this.node.scrollTop
|
||||
+ this.node.offsetHeight === this.node.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, shouldScroll)
|
||||
{
|
||||
if (shouldScroll)
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
files,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={(node) => { this.node = node; }}>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileList.propTypes =
|
||||
{
|
||||
files : PropTypes.object.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(FileList));
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import FileList from './FileList';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
width : '100%',
|
||||
height : '100%'
|
||||
},
|
||||
input :
|
||||
{
|
||||
display : 'none'
|
||||
},
|
||||
button :
|
||||
{
|
||||
margin : theme.spacing.unit
|
||||
}
|
||||
});
|
||||
|
||||
class FileSharing extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._fileInput = React.createRef();
|
||||
}
|
||||
|
||||
handleFileChange = async (event) =>
|
||||
{
|
||||
if (event.target.files.length > 0)
|
||||
{
|
||||
this.props.roomClient.shareFiles(event.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<input
|
||||
ref={this._fileInput}
|
||||
className={classes.input}
|
||||
type='file'
|
||||
onChange={this.handleFileChange}
|
||||
id='share-files-button'
|
||||
/>
|
||||
<label htmlFor='share-files-button'>
|
||||
<Button
|
||||
variant='contained'
|
||||
component='span'
|
||||
className={classes.button}
|
||||
disabled={!torrentSupport}
|
||||
>
|
||||
{buttonDescription}
|
||||
</Button>
|
||||
</label>
|
||||
|
||||
<FileList />
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(FileSharing)));
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import Badge from '@material-ui/core/Badge';
|
||||
import Chat from './Chat/Chat';
|
||||
import FileSharing from './FileSharing/FileSharing';
|
||||
import ParticipantList from './ParticipantList/ParticipantList';
|
||||
|
||||
const tabs =
|
||||
[
|
||||
'chat',
|
||||
'files',
|
||||
'users'
|
||||
];
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
backgroundColor : theme.palette.background.paper
|
||||
}
|
||||
});
|
||||
|
||||
class MeetingDrawer extends React.Component
|
||||
{
|
||||
handleChange = (event, value) =>
|
||||
{
|
||||
this.props.setToolTab(tabs[value]);
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
currentToolTab,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position='static' color='default'>
|
||||
<Tabs
|
||||
value={tabs.indexOf(currentToolTab)}
|
||||
onChange={this.handleChange}
|
||||
indicatorColor='primary'
|
||||
textColor='primary'
|
||||
variant='fullWidth'
|
||||
>
|
||||
<Tab
|
||||
label=
|
||||
{
|
||||
<Badge color='secondary' badgeContent={unreadMessages}>
|
||||
Chat
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
label=
|
||||
{
|
||||
<Badge color='secondary' badgeContent={unreadFiles}>
|
||||
File sharing
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<Tab label='Participants' />
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
{currentToolTab === 'chat' && <Chat />}
|
||||
{currentToolTab === 'files' && <FileSharing />}
|
||||
{currentToolTab === 'users' && <ParticipantList />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MeetingDrawer.propTypes =
|
||||
{
|
||||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.isRequired,
|
||||
unreadMessages : PropTypes.number.isRequired,
|
||||
unreadFiles : PropTypes.number.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
unreadMessages : state.toolarea.unreadMessages,
|
||||
unreadFiles : state.toolarea.unreadFiles
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : stateActions.setToolTab
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(MeetingDrawer));
|
||||
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
import HandIcon from '../../../images/icon-hand-white.svg';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
display : 'flex'
|
||||
},
|
||||
listPeer :
|
||||
{
|
||||
display : 'flex'
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem'
|
||||
},
|
||||
peerInfo :
|
||||
{
|
||||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : '0.5rem',
|
||||
flexGrow : 1,
|
||||
alignItems : 'center'
|
||||
},
|
||||
indicators :
|
||||
{
|
||||
left : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
transition : 'opacity 0.3s'
|
||||
},
|
||||
icon :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.3rem',
|
||||
borderRadius : 2,
|
||||
backgroundPosition : 'center',
|
||||
backgroundSize : '75%',
|
||||
backgroundRepeat : 'no-repeat',
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.5)',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : 'var(--media-control-button-size)',
|
||||
height : 'var(--media-control-button-size)',
|
||||
opacity : 0.85,
|
||||
'&:hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.raise-hand' :
|
||||
{
|
||||
backgroundImage : `url(${HandIcon})`,
|
||||
opacity : 1
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ListMe = (props) =>
|
||||
{
|
||||
const {
|
||||
me,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
const picture = me.picture || EmptyAvatar;
|
||||
|
||||
return (
|
||||
<li className={classes.root}>
|
||||
<div className={classes.listPeer}>
|
||||
<img alt='My avatar' className={classes.avatar} src={picture} />
|
||||
|
||||
<div className={classes.peerInfo}>
|
||||
{me.displayName}
|
||||
</div>
|
||||
|
||||
<div className={classes.indicators}>
|
||||
{ me.raisedHand ?
|
||||
<div className={classnames(classes.icon, 'raise-hand')} />
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ListMe.propTypes =
|
||||
{
|
||||
me : appPropTypes.Me.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
me : state.me
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(ListMe));
|
||||
@@ -0,0 +1,294 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import MicIcon from '@material-ui/icons/Mic';
|
||||
import MicOffIcon from '@material-ui/icons/MicOff';
|
||||
import ScreenIcon from '@material-ui/icons/ScreenShare';
|
||||
import ScreenOffIcon from '@material-ui/icons/StopScreenShare';
|
||||
import EmptyAvatar from '../../../images/avatar-empty.jpeg';
|
||||
import HandIcon from '../../../images/icon-hand-white.svg';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'auto',
|
||||
display : 'flex'
|
||||
},
|
||||
listPeer :
|
||||
{
|
||||
display : 'flex'
|
||||
},
|
||||
avatar :
|
||||
{
|
||||
borderRadius : '50%',
|
||||
height : '2rem'
|
||||
},
|
||||
peerInfo :
|
||||
{
|
||||
fontSize : '1rem',
|
||||
border : 'none',
|
||||
display : 'flex',
|
||||
paddingLeft : '0.5rem',
|
||||
flexGrow : 1,
|
||||
alignItems : 'center'
|
||||
},
|
||||
indicators :
|
||||
{
|
||||
left : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
transition : 'opacity 0.3s'
|
||||
},
|
||||
icon :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.3rem',
|
||||
borderRadius : 2,
|
||||
backgroundPosition : 'center',
|
||||
backgroundSize : '75%',
|
||||
backgroundRepeat : 'no-repeat',
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.5)',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : 'var(--media-control-button-size)',
|
||||
height : 'var(--media-control-button-size)',
|
||||
opacity : 0.85,
|
||||
'&:hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.on' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.off' :
|
||||
{
|
||||
opacity : 0.2
|
||||
},
|
||||
'&.raise-hand' :
|
||||
{
|
||||
backgroundImage : `url(${HandIcon})`
|
||||
}
|
||||
},
|
||||
volumeContainer :
|
||||
{
|
||||
float : 'right',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
width : '1vmin',
|
||||
position : 'relative',
|
||||
backgroundSize : '75%'
|
||||
},
|
||||
bar :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.3rem',
|
||||
backgroundSize : '75%',
|
||||
backgroundRepeat : 'no-repeat',
|
||||
backgroundColor : 'rgba(0, 0, 0, 1)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
width : 3,
|
||||
borderRadius : 6,
|
||||
transitionDuration : '0.25s',
|
||||
position : 'absolute',
|
||||
bottom : 0,
|
||||
'&.level0' : { height: 0 },
|
||||
'&.level1' : { height: '0.2vh' },
|
||||
'&.level2' : { height: '0.4vh' },
|
||||
'&.level3' : { height: '0.6vh' },
|
||||
'&.level4' : { height: '0.8vh' },
|
||||
'&.level5' : { height: '1.0vh' },
|
||||
'&.level6' : { height: '1.2vh' },
|
||||
'&.level7' : { height: '1.4vh' },
|
||||
'&.level8' : { height: '1.6vh' },
|
||||
'&.level9' : { height: '1.8vh' },
|
||||
'&.level10' : { height: '2.0vh' }
|
||||
},
|
||||
controls :
|
||||
{
|
||||
float : 'right',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center'
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.3rem',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.5)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : 'var(--media-control-button-size)',
|
||||
height : 'var(--media-control-button-size)',
|
||||
opacity : 0.85,
|
||||
'&:hover' :
|
||||
{
|
||||
opacity : 1
|
||||
},
|
||||
'&.unsupported' :
|
||||
{
|
||||
pointerEvents : 'none'
|
||||
},
|
||||
'&.disabled' :
|
||||
{
|
||||
pointerEvents : 'none',
|
||||
backgroundColor : 'var(--media-control-botton-disabled)'
|
||||
},
|
||||
'&.on' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-on)'
|
||||
},
|
||||
'&.off' :
|
||||
{
|
||||
backgroundColor : 'var(--media-control-botton-off)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ListPeer = (props) =>
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
peer,
|
||||
micConsumer,
|
||||
screenConsumer,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const picture = peer.picture || EmptyAvatar;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<img alt='Peer avatar' className={classes.avatar} src={picture} />
|
||||
|
||||
<div className={classes.peerInfo}>
|
||||
{peer.displayName}
|
||||
</div>
|
||||
<div className={classes.indicators}>
|
||||
{ peer.raiseHandState ?
|
||||
<div className={
|
||||
classnames(
|
||||
classes.icon, 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
<div className={classes.volumeContainer}>
|
||||
<div className={classnames(classes.bar, `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className={classes.controls}>
|
||||
{ screenConsumer ?
|
||||
<div
|
||||
className={classnames(classes.button, 'screen', {
|
||||
on : screenVisible,
|
||||
off : !screenVisible,
|
||||
disabled : peer.peerScreenInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
|
||||
}}
|
||||
>
|
||||
{ screenVisible ?
|
||||
<ScreenIcon />
|
||||
:
|
||||
<ScreenOffIcon />
|
||||
}
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
<div
|
||||
className={classnames(classes.button, 'mic', {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
>
|
||||
{ micEnabled ?
|
||||
<MicIcon />
|
||||
:
|
||||
<MicOffIcon />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListPeer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(ListPeer)));
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListPeer from './ListPeer';
|
||||
import ListMe from './ListMe';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
width : '100%',
|
||||
overflowY : 'auto',
|
||||
padding : 6
|
||||
},
|
||||
list :
|
||||
{
|
||||
listStyleType : 'none',
|
||||
padding : theme.spacing.unit,
|
||||
boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
|
||||
backgroundColor : 'rgba(255, 255, 255, 1)'
|
||||
},
|
||||
listheader :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
fontWeight : 'bolder'
|
||||
},
|
||||
listItem :
|
||||
{
|
||||
padding : '0.5rem',
|
||||
width : '100%',
|
||||
overflow : 'hidden',
|
||||
cursor : 'pointer',
|
||||
'&.selected' :
|
||||
{
|
||||
backgroundColor: 'rgba(55, 126, 255, 1)'
|
||||
},
|
||||
'&:not(:last-child)' :
|
||||
{
|
||||
borderBottom : '1px solid #CBCBCB'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class ParticipantList extends React.Component
|
||||
{
|
||||
componentDidMount()
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate()
|
||||
{
|
||||
return this.node.scrollTop
|
||||
+ this.node.offsetHeight === this.node.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, shouldScroll)
|
||||
{
|
||||
if (shouldScroll)
|
||||
{
|
||||
this.node.scrollTop = this.node.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peers,
|
||||
selectedPeerName,
|
||||
spotlights,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={(node) => { this.node = node; }}>
|
||||
<ul className={classes.list}>
|
||||
<li className={classes.listheader}>Me:</li>
|
||||
<ListMe />
|
||||
</ul>
|
||||
<br />
|
||||
<ul className={classes.list}>
|
||||
<li className={classes.listheader}>Participants in Spotlight:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return (spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames(classes.listItem, {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<br />
|
||||
<ul className={classes.list}>
|
||||
<li className={classes.listheader}>Passive Participants:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return !(spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames(classes.listItem, {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ParticipantList.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peersArray = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights : state.room.spotlights
|
||||
};
|
||||
};
|
||||
|
||||
const ParticipantListContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(ParticipantList)));
|
||||
|
||||
export default ParticipantListContainer;
|
||||
@@ -0,0 +1,210 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Peer from '../Containers/Peer';
|
||||
import HiddenPeers from '../Containers/HiddenPeers';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const RATIO = 1.334;
|
||||
const PADDING = 100;
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
flexWrap : 'wrap',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
alignContent : 'center',
|
||||
paddingTop : 70,
|
||||
paddingBottom : 30
|
||||
},
|
||||
peerContainer :
|
||||
{
|
||||
overflow : 'hidden',
|
||||
flex : '0 0 auto',
|
||||
margin : 6,
|
||||
boxShadow : 'var(--peer-shadow)',
|
||||
border : 'var(--peer-border)',
|
||||
transitionProperty : 'border-color',
|
||||
'&.active-speaker' :
|
||||
{
|
||||
borderColor : 'var(--active-speaker-border-color)'
|
||||
},
|
||||
'&.selected' :
|
||||
{
|
||||
borderColor : 'var(--selected-peer-border-color)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class Democratic extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
peerWidth : 400,
|
||||
peerHeight : 300
|
||||
};
|
||||
|
||||
this.peersRef = React.createRef();
|
||||
}
|
||||
|
||||
updateDimensions = debounce(() =>
|
||||
{
|
||||
if (!this.peersRef.current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const n = this.props.boxes;
|
||||
|
||||
if (n === 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.peersRef.current.clientWidth;
|
||||
const height = this.peersRef.current.clientHeight - PADDING;
|
||||
|
||||
let x, y, space;
|
||||
|
||||
for (let rows = 1; rows < 100; rows = rows + 1)
|
||||
{
|
||||
x = width / Math.ceil(n / rows);
|
||||
y = x / RATIO;
|
||||
if (height < (y * rows))
|
||||
{
|
||||
y = height / rows;
|
||||
x = RATIO * y;
|
||||
break;
|
||||
}
|
||||
space = height - (y * (rows));
|
||||
if (space < y)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x))
|
||||
{
|
||||
this.setState({
|
||||
peerWidth : 0.9 * x,
|
||||
peerHeight : 0.9 * y
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
window.addEventListener('resize', this.updateDimensions);
|
||||
const observer = new ResizeObserver(this.updateDimensions);
|
||||
|
||||
observer.observe(this.peersRef.current);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
window.removeEventListener('resize', this.updateDimensions);
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
this.updateDimensions();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
activeSpeakerName,
|
||||
peers,
|
||||
spotlights,
|
||||
spotlightsLength,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const style =
|
||||
{
|
||||
'width' : this.state.peerWidth,
|
||||
'height' : this.state.peerHeight
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root} ref={this.peersRef}>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
return (
|
||||
<div
|
||||
key={peerName}
|
||||
className={classnames(classes.peerContainer, {
|
||||
'selected' : this.props.selectedPeerName === peerName,
|
||||
'active-speaker' : peerName === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return('');
|
||||
}
|
||||
})}
|
||||
{ spotlightsLength < Object.keys(peers).length ?
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length - spotlightsLength}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Democratic.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.object.isRequired,
|
||||
boxes : PropTypes.number,
|
||||
activeSpeakerName : PropTypes.string,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlightsLength : PropTypes.number,
|
||||
spotlights : PropTypes.array.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const spotlights = state.room.spotlights;
|
||||
const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
|
||||
const boxes = spotlightsLength + Object.values(state.consumers)
|
||||
.filter((consumer) => consumer.source === 'screen').length;
|
||||
|
||||
return {
|
||||
peers : state.peers,
|
||||
boxes,
|
||||
activeSpeakerName : state.room.activeSpeakerName,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights,
|
||||
spotlightsLength
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(withStyles(styles)(Democratic));
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSnackbar } from 'notistack';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
|
||||
class Notifications extends Component
|
||||
{
|
||||
displayed = [];
|
||||
|
||||
storeDisplayed = (id) =>
|
||||
{
|
||||
this.displayed = [ ...this.displayed, id ];
|
||||
};
|
||||
|
||||
shouldComponentUpdate({ notifications: newNotifications = [] })
|
||||
{
|
||||
const { notifications: currentNotifications } = this.props;
|
||||
|
||||
let notExists = false;
|
||||
|
||||
for (let i = 0; i < newNotifications.length; i += 1)
|
||||
{
|
||||
if (notExists) continue;
|
||||
|
||||
notExists = notExists ||
|
||||
!currentNotifications.filter(({ id }) => newNotifications[i].id === id).length;
|
||||
}
|
||||
|
||||
return notExists;
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
const { notifications = [] } = this.props;
|
||||
|
||||
notifications.forEach((notification) =>
|
||||
{
|
||||
// Do nothing if snackbar is already displayed
|
||||
if (this.displayed.includes(notification.id)) return;
|
||||
// Display snackbar using notistack
|
||||
this.props.enqueueSnackbar(notification.text,
|
||||
{
|
||||
variant : notification.type,
|
||||
autoHideDuration : notification.timeout,
|
||||
anchorOrigin : {
|
||||
vertical : 'bottom',
|
||||
horizontal : 'left'
|
||||
}
|
||||
}
|
||||
);
|
||||
// Keep track of snackbars that we've displayed
|
||||
this.storeDisplayed(notification.id);
|
||||
// Dispatch action to remove snackbar from redux store
|
||||
this.props.removeNotification(notification.id);
|
||||
});
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.propTypes =
|
||||
{
|
||||
notifications : PropTypes.array.isRequired,
|
||||
enqueueSnackbar : PropTypes.func.isRequired,
|
||||
removeNotification : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
notifications : state.notifications
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
({
|
||||
removeNotification : (notificationId) =>
|
||||
dispatch(stateActions.removeNotification({ notificationId }))
|
||||
});
|
||||
|
||||
export default withSnackbar(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Notifications)
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import PeerAudio from './PeerAudio';
|
||||
|
||||
const AudioPeer = ({ micConsumer }) =>
|
||||
{
|
||||
return (
|
||||
<PeerAudio
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeer.propTypes =
|
||||
{
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
name : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
|
||||
return {
|
||||
micConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const AudioPeerContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeer);
|
||||
|
||||
export default AudioPeerContainer;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import AudioPeer from './AudioPeer';
|
||||
|
||||
const AudioPeers = ({ peers }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='AudioPeers'>
|
||||
{
|
||||
Object.values(peers).map((peer) =>
|
||||
{
|
||||
return (
|
||||
<AudioPeer
|
||||
key={peer.name}
|
||||
name={peer.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AudioPeers.propTypes =
|
||||
{
|
||||
peers : PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
({
|
||||
peers : state.peers
|
||||
});
|
||||
|
||||
const AudioPeersContainer = connect(
|
||||
mapStateToProps
|
||||
)(AudioPeers);
|
||||
|
||||
export default AudioPeersContainer;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class PeerAudio extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
// Latest received audio track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<audio
|
||||
ref='audio'
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack } = this.props;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack } = nextProps;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
_setTrack(audioTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
|
||||
const { audio } = this.refs;
|
||||
|
||||
if (audioTrack)
|
||||
{
|
||||
const stream = new MediaStream();
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
audio.srcObject = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.srcObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PeerAudio.propTypes =
|
||||
{
|
||||
audioTrack : PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,406 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import classnames from 'classnames';
|
||||
import { withRoomContext } from '../RoomContext';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as stateActions from '../actions/stateActions';
|
||||
import Draggable from 'react-draggable';
|
||||
import { idle } from '../utils';
|
||||
import FullScreen from './FullScreen';
|
||||
import CookieConsent from 'react-cookie-consent';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer';
|
||||
import Hidden from '@material-ui/core/Hidden';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Badge from '@material-ui/core/Badge';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import Notifications from './Notifications/Notifications';
|
||||
import MeetingDrawer from './MeetingDrawer/MeetingDrawer';
|
||||
import Democratic from './MeetingViews/Democratic';
|
||||
import Me from './Containers/Me';
|
||||
import AudioPeers from './PeerAudio/AudioPeers';
|
||||
import FullScreenView from './VideoContainers/FullScreenView';
|
||||
import VideoWindow from './VideoWindow/VideoWindow';
|
||||
import Sidebar from './Controls/Sidebar';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import Settings from './Settings/Settings';
|
||||
|
||||
const TIMEOUT = 10 * 1000;
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
display : 'flex',
|
||||
width : '100%',
|
||||
height : '100%'
|
||||
},
|
||||
message :
|
||||
{
|
||||
position : 'fixed',
|
||||
top : '50%',
|
||||
left : '50%',
|
||||
transform : 'translateX(-50%) translateY(-50%)',
|
||||
width : '30vw',
|
||||
padding : 'theme.unit.spacing * 2'
|
||||
},
|
||||
menuButton :
|
||||
{
|
||||
margin : 0,
|
||||
padding : 0
|
||||
},
|
||||
toolbar : theme.mixins.toolbar,
|
||||
drawerPaper :
|
||||
{
|
||||
width : '30vw',
|
||||
[theme.breakpoints.down('lg')] :
|
||||
{
|
||||
width : '40vw'
|
||||
},
|
||||
[theme.breakpoints.down('md')] :
|
||||
{
|
||||
width : '50vw'
|
||||
},
|
||||
[theme.breakpoints.down('sm')] :
|
||||
{
|
||||
width : '70vw'
|
||||
},
|
||||
[theme.breakpoints.down('xs')] :
|
||||
{
|
||||
width : '90vw'
|
||||
}
|
||||
},
|
||||
grow :
|
||||
{
|
||||
flexGrow : 1
|
||||
},
|
||||
title :
|
||||
{
|
||||
display : 'none',
|
||||
marginLeft : 20,
|
||||
[theme.breakpoints.up('sm')] :
|
||||
{
|
||||
display : 'block'
|
||||
}
|
||||
},
|
||||
actionButtons :
|
||||
{
|
||||
display : 'flex'
|
||||
},
|
||||
meContainer :
|
||||
{
|
||||
position : 'fixed',
|
||||
zIndex : 110,
|
||||
overflow : 'hidden',
|
||||
boxShadow : 'var(--me-shadow)',
|
||||
transitionProperty : 'border-color',
|
||||
transitionDuration : '0.15s',
|
||||
top : '8%',
|
||||
left : '1%',
|
||||
border : 'var(--me-border)',
|
||||
'&.active-speaker' :
|
||||
{
|
||||
borderColor : 'var(--active-speaker-border-color)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class Room extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.fullscreen = new FullScreen(document);
|
||||
|
||||
this.state = {
|
||||
drawerOpen : false,
|
||||
fullscreen : false
|
||||
};
|
||||
}
|
||||
|
||||
waitForHide = idle(() =>
|
||||
{
|
||||
this.props.setToolbarsVisible(false);
|
||||
}, TIMEOUT);
|
||||
|
||||
handleMovement = () =>
|
||||
{
|
||||
// If the toolbars were hidden, show them again when
|
||||
// the user moves their cursor.
|
||||
if (!this.props.room.toolbarsVisible)
|
||||
{
|
||||
this.props.setToolbarsVisible(true);
|
||||
}
|
||||
|
||||
this.waitForHide();
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', this.handleMovement);
|
||||
window.addEventListener('touchstart', this.handleMovement);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', this.handleMovement);
|
||||
window.removeEventListener('touchstart', this.handleMovement);
|
||||
}
|
||||
|
||||
handleToggleFullscreen = () =>
|
||||
{
|
||||
if (this.fullscreen.fullscreenElement)
|
||||
{
|
||||
this.fullscreen.exitFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fullscreen.requestFullscreen(document.documentElement);
|
||||
}
|
||||
};
|
||||
|
||||
handleFullscreenChange = () =>
|
||||
{
|
||||
this.setState({
|
||||
fullscreen : this.fullscreen.fullscreenElement !== null
|
||||
});
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
amActiveSpeaker,
|
||||
setSettingsOpen,
|
||||
toolAreaOpen,
|
||||
toggleToolArea,
|
||||
unread,
|
||||
classes,
|
||||
theme
|
||||
} = this.props;
|
||||
|
||||
if (room.audioSuspended)
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.message}>
|
||||
<Typography>This webpage required sound and video to play, please click to allow.</Typography>
|
||||
<Button
|
||||
variant='contained'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.notify('Joining.');
|
||||
roomClient.resumeAudio();
|
||||
}}
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (room.lockedOut)
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.message}>
|
||||
<Typography>This room is locked at the moment, try again later.</Typography>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<CookieConsent>
|
||||
This website uses cookies to enhance the user experience.
|
||||
</CookieConsent>
|
||||
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
|
||||
<VideoWindow advancedMode={room.advancedMode} />
|
||||
|
||||
<AudioPeers />
|
||||
|
||||
<Notifications />
|
||||
|
||||
<CssBaseline />
|
||||
|
||||
<AppBar
|
||||
position='fixed'
|
||||
>
|
||||
<Toolbar>
|
||||
<Badge
|
||||
color='secondary'
|
||||
badgeContent={unread}
|
||||
>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
aria-label='Open drawer'
|
||||
onClick={() => toggleToolArea()}
|
||||
className={classes.menuButton}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Badge>
|
||||
<Typography
|
||||
className={classes.title}
|
||||
variant='h6'
|
||||
color='inherit'
|
||||
noWrap
|
||||
>
|
||||
Multiparty meeting
|
||||
</Typography>
|
||||
<div className={classes.grow} />
|
||||
<div className={classes.actionButtons}>
|
||||
{ this.fullscreen.fullscreenEnabled ?
|
||||
<IconButton
|
||||
aria-label='Fullscreen'
|
||||
color='inherit'
|
||||
onClick={this.handleToggleFullscreen}
|
||||
>
|
||||
{ this.state.fullscreen ?
|
||||
<FullScreenExitIcon />
|
||||
:
|
||||
<FullScreenIcon />
|
||||
}
|
||||
</IconButton>
|
||||
:null
|
||||
}
|
||||
<IconButton
|
||||
aria-label='Settings'
|
||||
color='inherit'
|
||||
onClick={() => setSettingsOpen(!room.settingsOpen)}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
{ me.loginEnabled ?
|
||||
<IconButton
|
||||
aria-label='Account'
|
||||
color='inherit'
|
||||
onClick={() => me.loggedIn ? roomClient.logout() : roomClient.login() }
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<nav>
|
||||
<Hidden implementation='css'>
|
||||
<SwipeableDrawer
|
||||
variant='temporary'
|
||||
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
|
||||
open={toolAreaOpen}
|
||||
onClose={() => toggleToolArea()}
|
||||
onOpen={() => toggleToolArea()}
|
||||
classes={{
|
||||
paper : classes.drawerPaper
|
||||
}}
|
||||
>
|
||||
<MeetingDrawer />
|
||||
</SwipeableDrawer>
|
||||
</Hidden>
|
||||
</nav>
|
||||
<Democratic advancedMode={room.advancedMode} />
|
||||
<Draggable handle='.me-handle' bounds='body' cancel='.display-name'>
|
||||
<div
|
||||
className={classnames(classes.meContainer, 'me-handle', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me
|
||||
advancedMode={room.advancedMode}
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<Settings />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||
toolAreaOpen : PropTypes.bool.isRequired,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
setToolbarsVisible : PropTypes.func.isRequired,
|
||||
setSettingsOpen : PropTypes.func.isRequired,
|
||||
toggleToolArea : PropTypes.func.isRequired,
|
||||
unread : PropTypes.number.isRequired,
|
||||
classes : PropTypes.object.isRequired,
|
||||
theme : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const screenProducer =
|
||||
producersArray.find((producer) => producer.source === 'screen');
|
||||
|
||||
return {
|
||||
room : state.room,
|
||||
me : state.me,
|
||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
|
||||
screenProducer : screenProducer,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||
unread : state.toolarea.unreadMessages +
|
||||
state.toolarea.unreadFiles
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
setToolbarsVisible : (visible) =>
|
||||
{
|
||||
dispatch(stateActions.setToolbarsVisible(visible));
|
||||
},
|
||||
setSettingsOpen : (settingsOpen) =>
|
||||
{
|
||||
dispatch(stateActions.setSettingsOpen({ settingsOpen }));
|
||||
},
|
||||
toggleToolArea : () =>
|
||||
{
|
||||
dispatch(stateActions.toggleToolArea());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles, { withTheme: true })(Room)));
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { withRoomContext } from '../../RoomContext';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
},
|
||||
device :
|
||||
{
|
||||
width : '20vw',
|
||||
padding : theme.spacing.unit * 2
|
||||
},
|
||||
formControl :
|
||||
{
|
||||
display : 'flex',
|
||||
}
|
||||
});
|
||||
|
||||
const Settings = ({
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
onToggleAdvancedMode,
|
||||
handleChangeMode,
|
||||
handleCloseSettings,
|
||||
classes
|
||||
}) =>
|
||||
{
|
||||
let webcams;
|
||||
|
||||
if (me.webcamDevices)
|
||||
webcams = Array.from(me.webcamDevices.values());
|
||||
else
|
||||
webcams = [];
|
||||
|
||||
let audioDevices;
|
||||
|
||||
if (me.audioDevices)
|
||||
audioDevices = Array.from(me.audioDevices.values());
|
||||
else
|
||||
audioDevices = [];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className={classes.root}
|
||||
open={room.settingsOpen}
|
||||
onClose={() => handleCloseSettings({ settingsOpen: false })}
|
||||
>
|
||||
<DialogTitle id="form-dialog-title">Settings</DialogTitle>
|
||||
<form className={classes.device} autoComplete='off'>
|
||||
<FormControl className={classes.formControl}>
|
||||
<Select
|
||||
value={me.selectedWebcam || ''}
|
||||
onChange={(event) => event.target.value ? roomClient.changeWebcam(event.target.value) : null }
|
||||
displayEmpty
|
||||
name='Camera'
|
||||
autoWidth
|
||||
className={classes.selectEmpty}
|
||||
// disabled={!me.canChangeWebcam}
|
||||
>
|
||||
<MenuItem value='' />
|
||||
{ webcams.map((webcam, index) =>
|
||||
{
|
||||
return (
|
||||
<MenuItem key={index} value={webcam.deviceId}>{webcam.label}</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Select camera device
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
<form className={classes.device} autoComplete='off'>
|
||||
<FormControl className={classes.formControl}>
|
||||
<Select
|
||||
value={me.selectedAudioDevice || ''}
|
||||
onChange={(event) => event.target.value ? roomClient.changeAudioDevice(event.target.value) : null }
|
||||
displayEmpty
|
||||
name='Audio device'
|
||||
autoWidth
|
||||
className={classes.selectEmpty}
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
>
|
||||
<MenuItem value='' />
|
||||
{ audioDevices.map((audio, index) =>
|
||||
{
|
||||
return (
|
||||
<MenuItem key={index} value={audio.deviceId}>{audio.label}</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
{ me.canChangeAudioDevice ?
|
||||
'Select audio device'
|
||||
:
|
||||
'Unable to select audio device'
|
||||
}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
<DialogActions>
|
||||
<Button onClick={() => handleCloseSettings({ settingsOpen: false })} color='primary'>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||
handleChangeMode : PropTypes.func.isRequired,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
room : state.room
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||
handleChangeMode : stateActions.setDisplayMode,
|
||||
handleCloseSettings : stateActions.setSettingsOpen
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Settings)));
|
||||
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import FullView from './FullView';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
position : 'absolute',
|
||||
top : 0,
|
||||
left : 0,
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
zIndex : 20000
|
||||
},
|
||||
controls :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20020,
|
||||
right : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin'
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.2vmin',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(255, 255, 255, 0.7)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : '5vmin',
|
||||
height : '5vmin'
|
||||
},
|
||||
icon :
|
||||
{
|
||||
fontSize : '5vmin'
|
||||
},
|
||||
incompatibleVideo :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20010,
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
left : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
borderRadius : 6,
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 15,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const FullScreenView = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerFullscreen,
|
||||
toolbarsVisible,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{ consumerVisible && !consumer.supported ?
|
||||
<div className={classes.incompatibleVideo}>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classes.controls}>
|
||||
<div
|
||||
className={classnames(classes.button, 'room-controls', {
|
||||
visible : toolbarsVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(consumer);
|
||||
}}
|
||||
>
|
||||
<FullScreenExitIcon className={classes.icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullScreenView.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toolbarsVisible : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.fullScreenConsumer],
|
||||
toolbarsVisible : state.room.toolbarsVisible
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(FullScreenView));
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
position : 'relative',
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
overflow : 'hidden'
|
||||
},
|
||||
video :
|
||||
{
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
objectFit : 'contain',
|
||||
userSelect : 'none',
|
||||
transitionProperty : 'opacity',
|
||||
transitionDuration : '.15s',
|
||||
backgroundColor : 'rgba(0, 0, 0, 1)',
|
||||
'&.hidden' :
|
||||
{
|
||||
opacity : 0,
|
||||
transitionDuration : '0s'
|
||||
},
|
||||
'&.loading' :
|
||||
{
|
||||
filter : 'blur(5px)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class FullView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
this.video = React.createRef();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<video
|
||||
ref={this.video}
|
||||
className={classnames(classes.video, {
|
||||
hidden : !videoVisible,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={Boolean(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { videoTrack } = this.props;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
const { videoTrack } = this.props;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
}
|
||||
|
||||
_setTracks(videoTrack)
|
||||
{
|
||||
if (this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
const video = this.video.current;
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
const stream = new MediaStream();
|
||||
|
||||
stream.addTrack(videoTrack);
|
||||
video.srcObject = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FullView.propTypes =
|
||||
{
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool,
|
||||
videoProfile : PropTypes.string,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(FullView);
|
||||
@@ -0,0 +1,431 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import EditableInput from '../Controls/EditableInput';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root:
|
||||
{
|
||||
position : 'relative',
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
overflow : 'hidden',
|
||||
backgroundColor : 'var(--peer-bg-color)',
|
||||
backgroundImage : 'var(--peer-empty-avatar)',
|
||||
backgroundPosition : 'bottom',
|
||||
backgroundSize : 'auto 85%',
|
||||
backgroundRepeat : 'no-repeat'
|
||||
},
|
||||
video :
|
||||
{
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
objectFit : 'cover',
|
||||
userSelect : 'none',
|
||||
transitionProperty : 'opacity',
|
||||
transitionDuration : '.15s',
|
||||
backgroundColor : 'var(--peer-video-bg-color)',
|
||||
'&.is-me' :
|
||||
{
|
||||
transform : 'scaleX(-1)'
|
||||
},
|
||||
'&.hidden' :
|
||||
{
|
||||
opacity : 0,
|
||||
transitionDuration : '0s'
|
||||
},
|
||||
'&.loading' :
|
||||
{
|
||||
filter : 'blur(5px)'
|
||||
}
|
||||
},
|
||||
info :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 10,
|
||||
top : '0.6vmin',
|
||||
left : '0.6vmin',
|
||||
bottom : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'space-between'
|
||||
},
|
||||
media :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
display : 'flex',
|
||||
flexDirection : 'row'
|
||||
},
|
||||
box :
|
||||
{
|
||||
padding : '0.4vmin',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.25)',
|
||||
'& p' :
|
||||
{
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
margin : 0,
|
||||
color : 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize : 10,
|
||||
|
||||
'&:last-child' :
|
||||
{
|
||||
marginBottom : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
peer :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'flex-end',
|
||||
position : 'absolute',
|
||||
bottom : '0.6vmin',
|
||||
left : 0,
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.25)',
|
||||
padding : '0.5vmin',
|
||||
alignItems : 'flex-start'
|
||||
},
|
||||
displayNameEdit :
|
||||
{
|
||||
fontSize : 14,
|
||||
fontWeight : 400,
|
||||
color : 'rgba(255, 255, 255, 0.85)',
|
||||
border : 'none',
|
||||
borderBottom : '1px solid #aeff00',
|
||||
backgroundColor : 'transparent'
|
||||
},
|
||||
displayNameStatic :
|
||||
{
|
||||
userSelect : 'none',
|
||||
cursor : 'text',
|
||||
fontSize : 14,
|
||||
fontWeight : 400,
|
||||
color : 'rgba(255, 255, 255, 0.85)',
|
||||
'&:hover' :
|
||||
{
|
||||
backgroundColor: 'rgb(174, 255, 0, 0.25)'
|
||||
}
|
||||
},
|
||||
deviceInfo :
|
||||
{
|
||||
marginTop : '0.4vmin',
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'flex-end',
|
||||
'& span' :
|
||||
{
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 11,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
},
|
||||
volume :
|
||||
{
|
||||
position : 'absolute',
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
right : 2,
|
||||
width : 10,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
},
|
||||
volumeBar :
|
||||
{
|
||||
width : 6,
|
||||
borderRadius : 6,
|
||||
background : 'rgba(yellow, 0.65)',
|
||||
transitionProperty : 'height background-color',
|
||||
transitionDuration : '0.25s',
|
||||
'&.level0' :
|
||||
{
|
||||
height : 0,
|
||||
backgroundColor : 'rgba(255, 255, 0, 0.65)'
|
||||
},
|
||||
'&.level1' :
|
||||
{
|
||||
height : '10%',
|
||||
backgroundColor : 'rgba(255, 255, 0, 0.65)'
|
||||
},
|
||||
'&.level2' :
|
||||
{
|
||||
height : '20%',
|
||||
backgroundColor : 'rgba(255, 255, 0, 0.65)'
|
||||
},
|
||||
'&.level3' :
|
||||
{
|
||||
height : '30%',
|
||||
backgroundColor : 'rgba(255, 255, 0, 0.65)'
|
||||
},
|
||||
'&.level4' :
|
||||
{
|
||||
height : '40%',
|
||||
backgroundColor : 'rgba(255, 165, 0, 0.65)'
|
||||
},
|
||||
'&.level5' :
|
||||
{
|
||||
height : '50%',
|
||||
backgroundColor : 'rgba(255, 165, 0, 0.65)'
|
||||
},
|
||||
'&.level6' :
|
||||
{
|
||||
height : '60%',
|
||||
backgroundColor : 'rgba(255, 0, 0, 0.65)'
|
||||
},
|
||||
'&.level7' :
|
||||
{
|
||||
height : '70%',
|
||||
backgroundColor : 'rgba(255, 0, 0, 0.65)'
|
||||
},
|
||||
'&.level8' :
|
||||
{
|
||||
height : '80%',
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.65)'
|
||||
},
|
||||
'&.level9' :
|
||||
{
|
||||
height : '90%',
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.65)'
|
||||
},
|
||||
'&.level10' :
|
||||
{
|
||||
height : '100%',
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.65)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class PeerView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
volume : 0, // Integer from 0 to 10.,
|
||||
videoWidth : null,
|
||||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._videoResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
peer,
|
||||
volume,
|
||||
advancedMode,
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
onChangeDisplayName,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
videoWidth,
|
||||
videoHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.info}>
|
||||
{ advancedMode ?
|
||||
<div className={classes.media}>
|
||||
<div className={classes.box}>
|
||||
{ audioCodec ?
|
||||
<p>{audioCodec}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{ videoCodec ?
|
||||
<p>{videoCodec} {videoProfile}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{ (videoVisible && videoWidth !== null) ?
|
||||
<p>{videoWidth}x{videoHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<div className={classes.peer}>
|
||||
{ isMe ?
|
||||
<EditableInput
|
||||
value={peer.displayName}
|
||||
propName='displayName'
|
||||
className={classnames(classes.displayNameEdit, 'display-name')}
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 30,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ displayName }) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
:
|
||||
<span className={classes.displayNameStatic}>
|
||||
{peer.displayName}
|
||||
</span>
|
||||
}
|
||||
|
||||
{ advancedMode ?
|
||||
<div className={classes.deviceInfo}>
|
||||
<span>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames(classes.video, {
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isMe}
|
||||
/>
|
||||
|
||||
<div className={classes.volume}>
|
||||
<div className={classnames(classes.volumeBar, `level${volume}`)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack, videoTrack } = this.props;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack, videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
|
||||
}
|
||||
|
||||
_setTracks(audioTrack, videoTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (audioTrack || videoTrack)
|
||||
{
|
||||
const stream = new MediaStream();
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideVideoResolution()
|
||||
{
|
||||
this.setState({ videoWidth: null, videoHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
audioTrack : PropTypes.any,
|
||||
volume : PropTypes.number,
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
videoProfile : PropTypes.string,
|
||||
audioCodec : PropTypes.string,
|
||||
videoCodec : PropTypes.string,
|
||||
onChangeDisplayName : PropTypes.func,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(PeerView);
|
||||
@@ -0,0 +1,246 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root:
|
||||
{
|
||||
position : 'relative',
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
overflow : 'hidden',
|
||||
backgroundColor : 'var(--peer-bg-color)',
|
||||
backgroundImage : 'var(--peer-empty-avatar)',
|
||||
backgroundPosition : 'bottom',
|
||||
backgroundSize : 'auto 85%',
|
||||
backgroundRepeat : 'no-repeat'
|
||||
},
|
||||
video :
|
||||
{
|
||||
flex : '100 100 auto',
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
objectFit : 'cover',
|
||||
userSelect : 'none',
|
||||
transitionProperty : 'opacity',
|
||||
transitionDuration : '.15s',
|
||||
backgroundColor : 'var(--peer-video-bg-color)',
|
||||
'&.is-me' :
|
||||
{
|
||||
transform : 'scaleX(-1)'
|
||||
},
|
||||
'&.hidden' :
|
||||
{
|
||||
opacity : 0,
|
||||
transitionDuration : '0s'
|
||||
},
|
||||
'&.loading' :
|
||||
{
|
||||
filter : 'blur(5px)'
|
||||
}
|
||||
},
|
||||
info :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 10,
|
||||
top : '0.6vmin',
|
||||
left : '0.6vmin',
|
||||
bottom : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'space-between'
|
||||
},
|
||||
media :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
display : 'flex',
|
||||
flexDirection : 'row'
|
||||
},
|
||||
box :
|
||||
{
|
||||
padding : '0.4vmin',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(0, 0, 0, 0.25)',
|
||||
'& p' :
|
||||
{
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
margin : 0,
|
||||
color : 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize : 10,
|
||||
|
||||
'&:last-child' :
|
||||
{
|
||||
marginBottom : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class ScreenView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
screenWidth : null,
|
||||
screenHeight : null
|
||||
};
|
||||
|
||||
// Latest received screen track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._screenTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._screenResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
advancedMode,
|
||||
screenVisible,
|
||||
screenProfile,
|
||||
screenCodec,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
screenWidth,
|
||||
screenHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<div className={classes.info}>
|
||||
{ advancedMode ?
|
||||
<div className={classnames(classes.media, { 'is-me': isMe })}>
|
||||
{ screenVisible ?
|
||||
<div className={classes.box}>
|
||||
{ screenCodec ?
|
||||
<p>{screenCodec} {screenProfile}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{ (screenVisible && screenWidth !== null) ?
|
||||
<p>{screenWidth}x{screenHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames(classes.video, {
|
||||
hidden : !screenVisible,
|
||||
'is-me' : isMe,
|
||||
loading : screenProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={Boolean(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { screenTrack } = this.props;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { screenTrack } = nextProps;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
_setTracks(screenTrack)
|
||||
{
|
||||
if (this._screenTrack === screenTrack)
|
||||
return;
|
||||
|
||||
this._screenTrack = screenTrack;
|
||||
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
this._hideScreenResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (screenTrack)
|
||||
{
|
||||
const stream = new MediaStream();
|
||||
|
||||
if (screenTrack)
|
||||
stream.addTrack(screenTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (screenTrack)
|
||||
this._showScreenResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showScreenResolution()
|
||||
{
|
||||
this._screenResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { screenWidth, screenHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
screenWidth : video.videoWidth,
|
||||
screenHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideScreenResolution()
|
||||
{
|
||||
this.setState({ screenWidth: null, screenHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
ScreenView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
advancedMode : PropTypes.bool,
|
||||
screenTrack : PropTypes.any,
|
||||
screenVisible : PropTypes.bool,
|
||||
screenProfile : PropTypes.string,
|
||||
screenCodec : PropTypes.string,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ScreenView);
|
||||
@@ -0,0 +1,359 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import FullScreen from '../FullScreen';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
|
||||
const styles = () =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
position : 'absolute',
|
||||
top : 0,
|
||||
left : 0,
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
zIndex : 20000
|
||||
},
|
||||
controls :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20020,
|
||||
right : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : '0.4vmin'
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.2vmin',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(255, 255, 255, 0.7)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : '5vmin',
|
||||
height : '5vmin'
|
||||
},
|
||||
icon :
|
||||
{
|
||||
fontSize : '5vmin'
|
||||
},
|
||||
incompatibleVideo :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20010,
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
left : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
borderRadius : 6,
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 15,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class NewWindow extends React.PureComponent
|
||||
{
|
||||
static defaultProps =
|
||||
{
|
||||
url : '',
|
||||
name : 'Multiparty Meeting',
|
||||
title : 'Multiparty Meeting',
|
||||
features : { width: '800px', height: '600px' },
|
||||
onBlock : null,
|
||||
onUnload : null,
|
||||
center : 'parent',
|
||||
copyStyles : true
|
||||
};
|
||||
|
||||
handleToggleFullscreen = () =>
|
||||
{
|
||||
if (this.fullscreen.fullscreenElement)
|
||||
{
|
||||
this.fullscreen.exitFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fullscreen.requestFullscreen(this.window.document.documentElement);
|
||||
}
|
||||
};
|
||||
|
||||
handleFullscreenChange = () =>
|
||||
{
|
||||
this.setState({
|
||||
fullscreen : this.fullscreen.fullscreenElement !== null
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.window = null;
|
||||
this.windowCheckerInterval = null;
|
||||
this.released = false;
|
||||
this.fullscreen = null;
|
||||
|
||||
this.state = {
|
||||
mounted : false,
|
||||
fullscreen : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
if (!this.state.mounted)
|
||||
return null;
|
||||
|
||||
return ReactDOM.createPortal([
|
||||
<div key='newwindow' className={classes.root}>
|
||||
<div className={classes.controls}>
|
||||
{this.fullscreen.fullscreenEnabled && (
|
||||
<div
|
||||
className={classes.button}
|
||||
onClick={this.handleToggleFullscreen}
|
||||
data-tip='Fullscreen'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
>
|
||||
{ this.state.fullscreen ?
|
||||
<FullScreenExitIcon className={classes.icon} />
|
||||
:
|
||||
<FullScreenIcon className={classes.icon} />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
], this.container);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this.openChild();
|
||||
// eslint-disable-next-line react/no-did-mount-set-state
|
||||
this.setState({ mounted: true });
|
||||
|
||||
this.fullscreen = new FullScreen(this.window.document);
|
||||
|
||||
if (this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
}
|
||||
|
||||
openChild()
|
||||
{
|
||||
const {
|
||||
url,
|
||||
title,
|
||||
name,
|
||||
features,
|
||||
onBlock,
|
||||
center
|
||||
} = this.props;
|
||||
|
||||
if (center === 'parent')
|
||||
{
|
||||
features.left =
|
||||
(window.top.outerWidth / 2) + window.top.screenX - (features.width / 2);
|
||||
features.top =
|
||||
(window.top.outerHeight / 2) + window.top.screenY - (features.height / 2);
|
||||
}
|
||||
else if (center === 'screen')
|
||||
{
|
||||
const screenLeft =
|
||||
window.screenLeft !== undefined ? window.screenLeft : window.screen.left;
|
||||
const screenTop =
|
||||
window.screenTop !== undefined ? window.screenTop : window.screen.top;
|
||||
|
||||
const width = window.innerWidth
|
||||
? window.innerWidth
|
||||
: document.documentElement.clientWidth
|
||||
? document.documentElement.clientWidth
|
||||
: window.screen.width;
|
||||
const height = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: window.screen.height;
|
||||
|
||||
features.left = (width / 2) - (features.width / 2) + screenLeft;
|
||||
features.top = (height / 2) - (features.height / 2) + screenTop;
|
||||
}
|
||||
|
||||
this.window = window.open(url, name, toWindowFeatures(features));
|
||||
|
||||
this.windowCheckerInterval = setInterval(() =>
|
||||
{
|
||||
if (!this.window || this.window.closed)
|
||||
{
|
||||
this.release();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
if (this.window)
|
||||
{
|
||||
this.window.document.title = title;
|
||||
this.window.document.body.appendChild(this.container);
|
||||
|
||||
if (this.props.copyStyles)
|
||||
{
|
||||
setTimeout(() => copyStyles(document, this.window.document), 0);
|
||||
}
|
||||
|
||||
this.window.addEventListener('beforeunload', () => this.release());
|
||||
}
|
||||
else if (typeof onBlock === 'function')
|
||||
{
|
||||
onBlock(null);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this.window)
|
||||
{
|
||||
if (this.fullscreen && this.fullscreen.fullscreenEnabled)
|
||||
{
|
||||
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
}
|
||||
|
||||
this.window.close();
|
||||
}
|
||||
}
|
||||
|
||||
release()
|
||||
{
|
||||
if (this.released)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.released = true;
|
||||
|
||||
clearInterval(this.windowCheckerInterval);
|
||||
|
||||
const { onUnload } = this.props;
|
||||
|
||||
if (typeof onUnload === 'function')
|
||||
{
|
||||
onUnload(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NewWindow.propTypes = {
|
||||
children : PropTypes.node,
|
||||
url : PropTypes.string,
|
||||
name : PropTypes.string,
|
||||
title : PropTypes.string,
|
||||
features : PropTypes.object,
|
||||
onUnload : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
center : PropTypes.oneOf([ 'parent', 'screen' ]),
|
||||
copyStyles : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
function copyStyles(source, target)
|
||||
{
|
||||
Array.from(source.styleSheets).forEach((styleSheet) =>
|
||||
{
|
||||
let rules;
|
||||
|
||||
try
|
||||
{
|
||||
rules = styleSheet.cssRules;
|
||||
}
|
||||
catch (err) {}
|
||||
|
||||
if (rules)
|
||||
{
|
||||
const newStyleEl = source.createElement('style');
|
||||
|
||||
Array.from(styleSheet.cssRules).forEach((cssRule) =>
|
||||
{
|
||||
const { cssText, type } = cssRule;
|
||||
|
||||
let returnText = cssText;
|
||||
|
||||
if ([ 3, 5 ].includes(type))
|
||||
{
|
||||
returnText = cssText
|
||||
.split('url(')
|
||||
.map((line) =>
|
||||
{
|
||||
if (line[1] === '/')
|
||||
{
|
||||
return `${line.slice(0, 1)}${
|
||||
window.location.origin
|
||||
}${line.slice(1)}`;
|
||||
}
|
||||
|
||||
return line;
|
||||
})
|
||||
.join('url(');
|
||||
}
|
||||
|
||||
newStyleEl.appendChild(source.createTextNode(returnText));
|
||||
});
|
||||
|
||||
target.head.appendChild(newStyleEl);
|
||||
}
|
||||
else if (styleSheet.href)
|
||||
{
|
||||
const newLinkEl = source.createElement('link');
|
||||
|
||||
newLinkEl.rel = 'stylesheet';
|
||||
newLinkEl.href = styleSheet.href;
|
||||
target.head.appendChild(newLinkEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toWindowFeatures(obj)
|
||||
{
|
||||
return Object.keys(obj)
|
||||
.reduce((features, name) =>
|
||||
{
|
||||
const value = obj[name];
|
||||
|
||||
if (typeof value === 'boolean')
|
||||
{
|
||||
features.push(`${name}=${value ? 'yes' : 'no'}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
features.push(`${name}=${value}`);
|
||||
}
|
||||
|
||||
return features;
|
||||
}, [])
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export default withStyles(styles)(NewWindow);
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NewWindow from './NewWindow';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../actions/stateActions';
|
||||
import FullView from '../VideoContainers/FullView';
|
||||
|
||||
const VideoWindow = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerWindow
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<NewWindow onUnload={toggleConsumerWindow}>
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</NewWindow>
|
||||
);
|
||||
};
|
||||
|
||||
VideoWindow.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.windowConsumer]
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerWindow : () =>
|
||||
{
|
||||
dispatch(stateActions.toggleConsumerWindow());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const VideoWindowContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(VideoWindow);
|
||||
|
||||
export default VideoWindowContainer;
|
||||
@@ -0,0 +1,91 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Room = PropTypes.shape(
|
||||
{
|
||||
url : PropTypes.string.isRequired,
|
||||
state : PropTypes.oneOf(
|
||||
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
});
|
||||
|
||||
export const Device = PropTypes.shape(
|
||||
{
|
||||
flag : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
version : PropTypes.string
|
||||
});
|
||||
|
||||
export const Me = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
displayNameSet : PropTypes.bool.isRequired,
|
||||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
restartIceInProgress : PropTypes.bool.isRequired
|
||||
});
|
||||
|
||||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
export const Peer = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
device : Device.isRequired,
|
||||
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
});
|
||||
|
||||
export const Consumer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
peerName : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
supported : PropTypes.bool.isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string
|
||||
});
|
||||
|
||||
export const Notification = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
|
||||
timeout : PropTypes.number
|
||||
});
|
||||
|
||||
export const Message = PropTypes.shape(
|
||||
{
|
||||
type : PropTypes.string,
|
||||
component : PropTypes.string,
|
||||
text : PropTypes.string,
|
||||
sender : PropTypes.string
|
||||
});
|
||||
|
||||
export const FileEntryProps = PropTypes.shape(
|
||||
{
|
||||
data : PropTypes.shape({
|
||||
name : PropTypes.string.isRequired,
|
||||
picture : PropTypes.string,
|
||||
file : PropTypes.shape({
|
||||
magnet : PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
me : PropTypes.bool
|
||||
}).isRequired,
|
||||
notify : PropTypes.func.isRequired
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import jsCookie from 'js-cookie';
|
||||
|
||||
const USER_COOKIE = 'multiparty-meeting.user';
|
||||
const DEVICES_COOKIE = 'multiparty-meeting.devices';
|
||||
|
||||
export function getUser()
|
||||
{
|
||||
return jsCookie.getJSON(USER_COOKIE);
|
||||
}
|
||||
|
||||
export function setUser({ displayName })
|
||||
{
|
||||
jsCookie.set(USER_COOKIE, { displayName });
|
||||
}
|
||||
|
||||
export function getDevices()
|
||||
{
|
||||
return jsCookie.getJSON(DEVICES_COOKIE);
|
||||
}
|
||||
|
||||
export function setDevices({ webcamEnabled })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||
}
|
||||
|
||||
export function setAudioDevice({ audioDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
|
||||
}
|
||||
|
||||
export function setVideoDevice({ videoDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 313 KiB |
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
id="svg4347"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="buddy.svg">
|
||||
<defs
|
||||
id="defs4349" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="5.3985295"
|
||||
inkscape:cy="12.974855"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showborder="true"
|
||||
inkscape:window-width="979"
|
||||
inkscape:window-height="809"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata4352">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1004.3622)">
|
||||
<rect
|
||||
style="fill:none"
|
||||
y="1004.8992"
|
||||
x="1.3571341"
|
||||
height="47.070076"
|
||||
width="44.699638"
|
||||
id="rect4438" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 29.874052,1037.8071 c -0.170513,-1.8828 -0.105106,-3.1968 -0.105106,-4.917 0.852576,-0.4474 2.380226,-3.2994 2.638334,-5.7089 0.670389,-0.054 1.727363,-0.7089 2.036865,-3.2912 0.167015,-1.3862 -0.496371,-2.1665 -0.900473,-2.4118 1.090848,-3.2807 3.356621,-13.4299 -4.190513,-14.4788 -0.776672,-1.364 -2.765648,-2.0544 -5.350262,-2.0544 -10.340807,0.1905 -11.588155,7.8088 -9.321205,16.5332 -0.402943,0.2453 -1.066322,1.0256 -0.900475,2.4118 0.310672,2.5823 1.366476,3.2362 2.036857,3.2912 0.256949,2.4083 1.845322,5.2615 2.700243,5.7089 0,1.7202 0.06426,3.0342 -0.106272,4.917 -2.046213,5.5007 -15.8522501,3.9567 -16.4899366,14.5661 H 46.303254 c -0.636524,-10.6094 -14.38299,-9.0654 -16.429202,-14.5661 z"
|
||||
id="path4440"
|
||||
style="opacity:0.5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 96 96"
|
||||
style="enable-background:new 0 0 96 96;"
|
||||
xml:space="preserve">
|
||||
<metadata
|
||||
id="metadata11"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs9" />
|
||||
<path
|
||||
style="fill:#000000;stroke-width:0.40677965"
|
||||
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||
id="path3710"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 96 96"
|
||||
style="enable-background:new 0 0 96 96;"
|
||||
xml:space="preserve">
|
||||
<metadata
|
||||
id="metadata11"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs9" />
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:0.40677965"
|
||||
d="m 33.894283,77.837288 c -1.428534,-1.845763 -3.909722,-5.220659 -5.513751,-7.499764 -1.60403,-2.279109 -4.323663,-5.940126 -6.043631,-8.135593 -5.698554,-7.273973 -6.224902,-8.044795 -6.226676,-9.118803 -0.0034,-2.075799 2.81181,-4.035355 4.9813,-3.467247 0.50339,0.131819 2.562712,1.72771 4.576272,3.546423 4.238418,3.828283 6.617166,5.658035 7.355654,5.658035 0.82497,0 1.045415,-1.364294 0.567453,-3.511881 C 33.348583,54.219654 31.1088,48.20339 28.613609,41.938983 23.524682,29.162764 23.215312,27.731034 25.178629,26.04226 c 2.443255,-2.101599 4.670178,-1.796504 6.362271,0.87165 0.639176,1.007875 2.666245,5.291978 4.504599,9.520229 1.838354,4.228251 3.773553,8.092718 4.300442,8.587705 l 0.957981,0.899977 0.419226,-1.102646 c 0.255274,-0.671424 0.419225,-6.068014 0.419225,-13.799213 0,-13.896836 -0.0078,-13.84873 2.44517,-15.1172 1.970941,-1.019214 4.2259,-0.789449 5.584354,0.569005 l 1.176852,1.176852 0.483523,11.738402 c 0.490017,11.896027 0.826095,14.522982 1.911266,14.939402 1.906224,0.731486 2.21601,-0.184677 4.465407,-13.206045 1.239206,-7.173539 1.968244,-10.420721 2.462128,-10.966454 1.391158,-1.537215 4.742705,-1.519809 6.295208,0.03269 1.147387,1.147388 1.05469,3.124973 -0.669503,14.283063 -0.818745,5.298489 -1.36667,10.090163 -1.220432,10.67282 0.14596,0.581557 0.724796,1.358395 1.286298,1.726306 0.957759,0.627548 1.073422,0.621575 1.86971,-0.09655 0.466837,-0.421011 1.761787,-2.595985 2.877665,-4.833273 2.564176,-5.141059 3.988466,-6.711864 6.085822,-6.711864 2.769954,0 3.610947,2.927256 2.139316,7.446329 C 78.799497,44.318351 66.752066,77.28024 65.51653,80.481356 65.262041,81.140709 64.18139,81.19322 50.866695,81.19322 H 36.491617 Z"
|
||||
id="path3710"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
--background: url('./images/background.svg');
|
||||
--background-color: rgba(114, 119, 143, 1.0);
|
||||
|
||||
--circle-button-color: rgba(255, 255, 255, 0.3);
|
||||
--circle-button-toggled-color: rgba(255, 255, 255, 0.7);
|
||||
--circle-button-unsupported-color: rgba(212, 34, 65, 0.7);
|
||||
--circle-button-diabled-color: rgba(255, 255, 255, 0.5);
|
||||
--circle-button-size: 2.5em;
|
||||
|
||||
--media-control-button-color: rgba(255, 255, 255, 0.85);
|
||||
--media-control-botton-on: rgba(255, 255, 255, 0.7);
|
||||
--media-control-botton-off: rgba(212, 34, 65, 0.7);
|
||||
--media-control-botton-disabled: rgba(255, 255, 255, 0.5);
|
||||
--media-control-button-size: 1.5em;
|
||||
|
||||
--me-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12);
|
||||
--me-border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
--me-width: 20vmin;
|
||||
--me-height: 15vmin;
|
||||
|
||||
--peer-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12);
|
||||
--peer-border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
--peer-empty-avatar: url('./images/buddy.svg');
|
||||
--peer-bg-color: rgba(42, 75, 88, 0.9);
|
||||
--peer-video-bg-color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
--chat-message-color: rgba(0, 0, 0, 0.1);
|
||||
--chat-input-bg-color: rgba(255, 255, 255, 1.0);
|
||||
--chat-input-text-color: rgba(0, 0, 0, 1.0);
|
||||
--chat-send-bg-color: rgba(170, 238, 255, 1.0);
|
||||
|
||||
--filesharing-bg-color: rgba(170, 238, 255, 1.0);
|
||||
|
||||
--notification-info-bg-color: rgba(10, 29, 38, 0.75);
|
||||
--notification-info-text-color: rgba(255, 255, 255, 0.65);
|
||||
--notification-error-bg-color: rgba(255, 25, 20, 0.65);
|
||||
--notification-error-text-color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
--active-speaker-border-color: rgba(255, 255, 255, 1.0);
|
||||
--selected-peer-border-color: rgba(55, 126, 255, 1.0);
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
background-color: var(--background-color);
|
||||
background-image: var(--background);
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#multiparty-meeting
|
||||
{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import domready from 'domready';
|
||||
import UrlParse from 'url-parse';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import randomString from 'random-string';
|
||||
import Logger from './Logger';
|
||||
import debug from 'debug';
|
||||
import RoomClient from './RoomClient';
|
||||
import RoomContext from './RoomContext';
|
||||
import * as cookiesManager from './cookiesManager';
|
||||
import * as stateActions from './actions/stateActions';
|
||||
import Room from './components/Room';
|
||||
import LoadingView from './components/LoadingView';
|
||||
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
|
||||
import { PersistGate } from 'redux-persist/lib/integration/react';
|
||||
import { persistor, store } from './store';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
import './index.css';
|
||||
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
{
|
||||
debug.enable('* -engine* -socket* -RIE* *WARN* *ERROR*');
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
let roomClient;
|
||||
|
||||
RoomClient.init({ store });
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette :
|
||||
{
|
||||
primary :
|
||||
{
|
||||
main : '#313131'
|
||||
}
|
||||
},
|
||||
typography :
|
||||
{
|
||||
useNextVariants : true
|
||||
}
|
||||
});
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
run();
|
||||
});
|
||||
|
||||
function run()
|
||||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
let roomId = (urlParser.pathname).substr(1)
|
||||
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
|
||||
const produce = urlParser.query.produce !== 'false';
|
||||
let displayName = urlParser.query.displayName;
|
||||
const useSimulcast = urlParser.query.simulcast === 'true';
|
||||
|
||||
if (!roomId)
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
|
||||
urlParser.query.roomId = roomId;
|
||||
window.history.pushState('', '', urlParser.toString());
|
||||
}
|
||||
|
||||
// Get the effective/shareable Room URL.
|
||||
const roomUrlParser = new UrlParse(window.location.href, true);
|
||||
|
||||
for (const key of Object.keys(roomUrlParser.query))
|
||||
{
|
||||
// Don't keep some custom params.
|
||||
switch (key)
|
||||
{
|
||||
case 'roomId':
|
||||
case 'simulcast':
|
||||
break;
|
||||
default:
|
||||
delete roomUrlParser.query[key];
|
||||
}
|
||||
}
|
||||
delete roomUrlParser.hash;
|
||||
|
||||
const roomUrl = roomUrlParser.toString();
|
||||
|
||||
// Get displayName from cookie (if not already given as param).
|
||||
const userCookie = cookiesManager.getUser() || {};
|
||||
let displayNameSet;
|
||||
|
||||
if (!displayName)
|
||||
displayName = userCookie.displayName;
|
||||
|
||||
if (displayName)
|
||||
{
|
||||
displayNameSet = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayName = 'Guest';
|
||||
displayNameSet = false;
|
||||
}
|
||||
|
||||
// Get current device.
|
||||
const device = getDeviceInfo();
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setRoomUrl(roomUrl));
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled: window.config.loginEnabled }));
|
||||
|
||||
roomClient = new RoomClient(
|
||||
{ roomId, peerName, displayName, device, useSimulcast, produce });
|
||||
|
||||
global.CLIENT = roomClient;
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<PersistGate loading={<LoadingView />} persistor={persistor}>
|
||||
<RoomContext.Provider value={roomClient}>
|
||||
<SnackbarProvider>
|
||||
<Room />
|
||||
</SnackbarProvider>
|
||||
</RoomContext.Provider>
|
||||
</PersistGate>
|
||||
</MuiThemeProvider>
|
||||
</Provider>,
|
||||
document.getElementById('multiparty-meeting')
|
||||
);
|
||||
}
|
||||
|
||||
serviceWorker.unregister();
|
||||
@@ -0,0 +1,43 @@
|
||||
import
|
||||
{
|
||||
createNewMessage
|
||||
} from './helper';
|
||||
|
||||
const chatmessages = (state = [], action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_NEW_USER_MESSAGE':
|
||||
{
|
||||
const { text } = action.payload;
|
||||
|
||||
const message = createNewMessage(text, 'client', 'Me', undefined);
|
||||
|
||||
return [ ...state, message ];
|
||||
}
|
||||
|
||||
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||
{
|
||||
const { message } = action.payload;
|
||||
|
||||
return [ ...state, message ];
|
||||
}
|
||||
|
||||
case 'ADD_CHAT_HISTORY':
|
||||
{
|
||||
const { chatHistory } = action.payload;
|
||||
|
||||
return [ ...state, ...chatHistory ];
|
||||
}
|
||||
|
||||
case 'DROP_MESSAGES':
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default chatmessages;
|
||||
@@ -0,0 +1,84 @@
|
||||
const initialState = {};
|
||||
|
||||
const consumers = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const { consumer } = action.payload;
|
||||
|
||||
return { ...state, [consumer.id]: consumer };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[consumerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_PAUSED':
|
||||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
newConsumer = { ...consumer, locallyPaused: true };
|
||||
else
|
||||
newConsumer = { ...consumer, remotelyPaused: true };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_VOLUME':
|
||||
{
|
||||
const { consumerId, volume } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, volume };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_RESUMED':
|
||||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
newConsumer = { ...consumer, locallyPaused: false };
|
||||
else
|
||||
newConsumer = { ...consumer, remotelyPaused: false };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_EFFECTIVE_PROFILE':
|
||||
{
|
||||
const { consumerId, profile } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, profile };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_TRACK':
|
||||
{
|
||||
const { consumerId, track } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, track };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default consumers;
|
||||
@@ -0,0 +1,101 @@
|
||||
const files = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_FILE':
|
||||
{
|
||||
const { file } = action.payload;
|
||||
|
||||
const newFile = {
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
};
|
||||
|
||||
return { ...state, [file.magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'ADD_FILE_HISTORY':
|
||||
{
|
||||
const { fileHistory } = action.payload;
|
||||
const newFileHistory = {};
|
||||
|
||||
// eslint-disable-next-line
|
||||
fileHistory.map((file) =>
|
||||
{
|
||||
const newFile =
|
||||
{
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
};
|
||||
|
||||
newFileHistory[file.magnetUri] = newFile;
|
||||
});
|
||||
|
||||
return { ...state, ...newFileHistory };
|
||||
}
|
||||
|
||||
case 'SET_FILE_ACTIVE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, active: true };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_INACTIVE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, active: false };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_PROGRESS':
|
||||
{
|
||||
const { magnetUri, progress } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, progress: progress };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_DONE':
|
||||
{
|
||||
const { magnetUri, sharedFiles } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = {
|
||||
...file,
|
||||
files : sharedFiles,
|
||||
progress : 1,
|
||||
active : false,
|
||||
timeout : false
|
||||
};
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'REMOVE_FILE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
|
||||
return state.filter((file) => file.magnetUri !== magnetUri);
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default files;
|
||||
@@ -0,0 +1,11 @@
|
||||
export function createNewMessage(text, sender, name, picture)
|
||||
{
|
||||
return {
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
const initialState =
|
||||
{
|
||||
name : null,
|
||||
displayName : null,
|
||||
displayNameSet : false,
|
||||
device : null,
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
canShareScreen : false,
|
||||
needExtension : false,
|
||||
canChangeAudioDevice : false,
|
||||
audioDevices : null,
|
||||
canChangeWebcam : false,
|
||||
webcamDevices : null,
|
||||
webcamInProgress : false,
|
||||
audioInProgress : false,
|
||||
screenShareInProgress : false,
|
||||
loginEnabled : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
raiseHand : false,
|
||||
raiseHandInProgress : false,
|
||||
restartIceInProgress : false,
|
||||
picture : null,
|
||||
selectedWebcam : null,
|
||||
selectedAudioDevice : null,
|
||||
loggedIn : false
|
||||
};
|
||||
|
||||
const me = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ME':
|
||||
{
|
||||
const {
|
||||
peerName,
|
||||
displayName,
|
||||
displayNameSet,
|
||||
device,
|
||||
loginEnabled
|
||||
} = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
name : peerName,
|
||||
displayName,
|
||||
displayNameSet,
|
||||
device,
|
||||
loginEnabled
|
||||
};
|
||||
}
|
||||
|
||||
case 'LOGGED_IN':
|
||||
return { ...state, loggedIn: true };
|
||||
|
||||
case 'USER_LOGOUT':
|
||||
return { ...state, loggedIn: false };
|
||||
|
||||
case 'CHANGE_WEBCAM':
|
||||
{
|
||||
return { ...state, selectedWebcam: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'CHANGE_AUDIO_DEVICE':
|
||||
{
|
||||
return { ...state, selectedAudioDevice: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'SET_MEDIA_CAPABILITIES':
|
||||
{
|
||||
const { canSendMic, canSendWebcam } = action.payload;
|
||||
|
||||
return { ...state, canSendMic, canSendWebcam };
|
||||
}
|
||||
|
||||
case 'SET_SCREEN_CAPABILITIES':
|
||||
{
|
||||
const { canShareScreen, needExtension } = action.payload;
|
||||
|
||||
return { ...state, canShareScreen, needExtension };
|
||||
}
|
||||
|
||||
case 'SET_CAN_CHANGE_AUDIO_DEVICE':
|
||||
{
|
||||
const canChangeAudioDevice = action.payload;
|
||||
|
||||
return { ...state, canChangeAudioDevice };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
||||
return { ...state, audioDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_CAN_CHANGE_WEBCAM':
|
||||
{
|
||||
const canChangeWebcam = action.payload;
|
||||
|
||||
return { ...state, canChangeWebcam };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
||||
return { ...state, webcamDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, audioInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, webcamInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_SCREEN_SHARE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, screenShareInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_NAME':
|
||||
{
|
||||
let { displayName } = action.payload;
|
||||
|
||||
// Be ready for undefined displayName (so keep previous one).
|
||||
if (!displayName)
|
||||
displayName = state.displayName;
|
||||
|
||||
return { ...state, displayName, displayNameSet: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_STATE':
|
||||
{
|
||||
const { enabled } = action.payload;
|
||||
|
||||
return { ...state, audioOnly: enabled };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, audioOnlyInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_MY_RAISE_HAND_STATE':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, raiseHand: flag };
|
||||
}
|
||||
|
||||
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, raiseHandInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_RESTART_ICE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, restartIceInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_PICTURE':
|
||||
{
|
||||
return { ...state, picture: action.payload.picture };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default me;
|
||||
@@ -0,0 +1,29 @@
|
||||
const notifications = (state = [], action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_NOTIFICATION':
|
||||
{
|
||||
const { notification } = action.payload;
|
||||
|
||||
return [ ...state, notification ];
|
||||
}
|
||||
|
||||
case 'REMOVE_NOTIFICATION':
|
||||
{
|
||||
const { notificationId } = action.payload;
|
||||
|
||||
return state.filter((notification) => notification.id !== notificationId);
|
||||
}
|
||||
|
||||
case 'REMOVE_ALL_NOTIFICATIONS':
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default notifications;
|
||||
@@ -0,0 +1,98 @@
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
const peer = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PEER':
|
||||
return action.payload.peer;
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
return { ...state, displayName: action.payload.displayName };
|
||||
|
||||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||
return { ...state, peerVideoInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||
return { ...state, peerAudioInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||
return { ...state, peerScreenInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
return { ...state, raiseHandState: action.payload.raiseHandState };
|
||||
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const consumers = [ ...state.consumers, action.payload.consumer.id ];
|
||||
|
||||
return { ...state, consumers };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const consumers = state.consumers.filter((consumer) =>
|
||||
consumer !== action.payload.consumerId);
|
||||
|
||||
return { ...state, consumers };
|
||||
}
|
||||
|
||||
case 'SET_PEER_PICTURE':
|
||||
{
|
||||
return { ...state, picture: action.payload.picture };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const peers = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PEER':
|
||||
{
|
||||
return { ...state, [action.payload.peer.name]: peer(undefined, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_PEER':
|
||||
{
|
||||
return omit(state, [ action.payload.peerName ]);
|
||||
}
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
case 'SET_PEER_PICTURE':
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
|
||||
if (!oldPeer)
|
||||
{
|
||||
throw new Error('no Peer found');
|
||||
}
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
|
||||
// NOTE: This means that the Peer was closed before, so it's ok.
|
||||
if (!oldPeer)
|
||||
return state;
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default peers;
|
||||
@@ -0,0 +1,75 @@
|
||||
const initialState = {};
|
||||
|
||||
const producers = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PRODUCER':
|
||||
{
|
||||
const { producer } = action.payload;
|
||||
|
||||
return { ...state, [producer.id]: producer };
|
||||
}
|
||||
|
||||
case 'REMOVE_PRODUCER':
|
||||
{
|
||||
const { producerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[producerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_PAUSED':
|
||||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
newProducer = { ...producer, locallyPaused: true };
|
||||
else
|
||||
newProducer = { ...producer, remotelyPaused: true };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_VOLUME':
|
||||
{
|
||||
const { producerId, volume } = action.payload;
|
||||
const producer = state[producerId];
|
||||
const newProducer = { ...producer, volume };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_RESUMED':
|
||||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
newProducer = { ...producer, locallyPaused: false };
|
||||
else
|
||||
newProducer = { ...producer, remotelyPaused: false };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_TRACK':
|
||||
{
|
||||
const { producerId, track } = action.payload;
|
||||
const producer = state[producerId];
|
||||
const newProducer = { ...producer, track };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default producers;
|
||||
@@ -0,0 +1,152 @@
|
||||
const initialState =
|
||||
{
|
||||
url : null,
|
||||
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||
locked : false,
|
||||
lockedOut : false,
|
||||
audioSuspended : false,
|
||||
activeSpeakerName : null,
|
||||
torrentSupport : false,
|
||||
showSettings : false,
|
||||
advancedMode : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : 'democratic',
|
||||
selectedPeerName : null,
|
||||
spotlights : [],
|
||||
settingsOpen : false
|
||||
};
|
||||
|
||||
const room = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ROOM_URL':
|
||||
{
|
||||
const { url } = action.payload;
|
||||
|
||||
return { ...state, url };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_STATE':
|
||||
{
|
||||
const roomState = action.payload.state;
|
||||
|
||||
if (roomState === 'connected')
|
||||
return { ...state, state: roomState };
|
||||
else
|
||||
return { ...state, state: roomState, activeSpeakerName: null };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_LOCKED':
|
||||
{
|
||||
return { ...state, locked: true };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_UNLOCKED':
|
||||
{
|
||||
return { ...state, locked: false };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_LOCKED_OUT':
|
||||
{
|
||||
return { ...state, lockedOut: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_SUSPENDED':
|
||||
{
|
||||
const { audioSuspended } = action.payload;
|
||||
|
||||
return { ...state, audioSuspended };
|
||||
}
|
||||
|
||||
case 'SET_SETTINGS_OPEN':
|
||||
{
|
||||
const { settingsOpen } = action.payload;
|
||||
|
||||
return { ...state, settingsOpen };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_ACTIVE_SPEAKER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
|
||||
return { ...state, activeSpeakerName: peerName };
|
||||
}
|
||||
|
||||
case 'FILE_SHARING_SUPPORTED':
|
||||
{
|
||||
const { supported } = action.payload;
|
||||
|
||||
return { ...state, torrentSupport: supported };
|
||||
}
|
||||
|
||||
case 'TOGGLE_SETTINGS':
|
||||
{
|
||||
const showSettings = !state.showSettings;
|
||||
|
||||
return { ...state, showSettings };
|
||||
}
|
||||
|
||||
case 'TOGGLE_ADVANCED_MODE':
|
||||
{
|
||||
const advancedMode = !state.advancedMode;
|
||||
|
||||
return { ...state, advancedMode };
|
||||
}
|
||||
|
||||
case 'TOGGLE_FULLSCREEN_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const currentConsumer = state.fullScreenConsumer;
|
||||
|
||||
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
|
||||
}
|
||||
|
||||
case 'TOGGLE_WINDOW_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const currentConsumer = state.windowConsumer;
|
||||
|
||||
if (currentConsumer === consumerId)
|
||||
return { ...state, windowConsumer: null };
|
||||
else
|
||||
return { ...state, windowConsumer: consumerId };
|
||||
}
|
||||
|
||||
case 'SET_TOOLBARS_VISIBLE':
|
||||
{
|
||||
const { toolbarsVisible } = action.payload;
|
||||
|
||||
return { ...state, toolbarsVisible };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_MODE':
|
||||
return { ...state, mode: action.payload.mode };
|
||||
|
||||
case 'SET_SELECTED_PEER':
|
||||
{
|
||||
const { selectedPeerName } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
selectedPeerName : state.selectedPeerName === selectedPeerName ?
|
||||
null : selectedPeerName
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_SPOTLIGHTS':
|
||||
{
|
||||
const { spotlights } = action.payload;
|
||||
|
||||
return { ...state, spotlights };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default room;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import room from './room';
|
||||
import me from './me';
|
||||
import producers from './producers';
|
||||
import peers from './peers';
|
||||
import consumers from './consumers';
|
||||
import notifications from './notifications';
|
||||
import chatmessages from './chatmessages';
|
||||
import toolarea from './toolarea';
|
||||
import files from './files';
|
||||
|
||||
export default combineReducers({
|
||||
room,
|
||||
me,
|
||||
producers,
|
||||
peers,
|
||||
consumers,
|
||||
notifications,
|
||||
chatmessages,
|
||||
toolarea,
|
||||
files
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
const initialState =
|
||||
{
|
||||
toolAreaOpen : false,
|
||||
currentToolTab : 'chat', // chat, settings, users
|
||||
unreadMessages : 0,
|
||||
unreadFiles : 0
|
||||
};
|
||||
|
||||
const toolarea = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'TOGGLE_TOOL_AREA':
|
||||
{
|
||||
const toolAreaOpen = !state.toolAreaOpen;
|
||||
const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = toolAreaOpen && state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'OPEN_TOOL_AREA':
|
||||
{
|
||||
const toolAreaOpen = true;
|
||||
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'CLOSE_TOOL_AREA':
|
||||
{
|
||||
const toolAreaOpen = false;
|
||||
|
||||
return { ...state, toolAreaOpen };
|
||||
}
|
||||
|
||||
case 'SET_TOOL_TAB':
|
||||
{
|
||||
const { toolTab } = action.payload;
|
||||
const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = toolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||
{
|
||||
if (state.toolAreaOpen && state.currentToolTab === 'chat')
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, unreadMessages: state.unreadMessages + 1 };
|
||||
}
|
||||
|
||||
case 'ADD_FILE':
|
||||
{
|
||||
if (state.toolAreaOpen && state.currentToolTab === 'files')
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, unreadFiles: state.unreadFiles + 1 };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default toolarea;
|
||||
@@ -0,0 +1,126 @@
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '[::1]' ||
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config)
|
||||
{
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator)
|
||||
{
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
|
||||
if (publicUrl.origin !== window.location.origin)
|
||||
return;
|
||||
|
||||
window.addEventListener('load', () =>
|
||||
{
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost)
|
||||
{
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
navigator.serviceWorker.ready.then(() =>
|
||||
{
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config)
|
||||
{
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) =>
|
||||
{
|
||||
registration.onupdatefound = () =>
|
||||
{
|
||||
const installingWorker = registration.installing;
|
||||
|
||||
if (installingWorker == null)
|
||||
return;
|
||||
|
||||
installingWorker.onstatechange = () =>
|
||||
{
|
||||
if (installingWorker.state === 'installed')
|
||||
{
|
||||
if (navigator.serviceWorker.controller)
|
||||
{
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
if (config && config.onUpdate)
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess)
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config)
|
||||
{
|
||||
fetch(swUrl)
|
||||
.then((response) =>
|
||||
{
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1))
|
||||
{
|
||||
navigator.serviceWorker.ready.then((registration) =>
|
||||
{
|
||||
registration.unregister().then(() =>
|
||||
{
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister()
|
||||
{
|
||||
if ('serviceWorker' in navigator)
|
||||
{
|
||||
navigator.serviceWorker.ready.then((registration) =>
|
||||
{
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
createStore,
|
||||
applyMiddleware,
|
||||
compose
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { persistStore, persistReducer } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
|
||||
import rootReducer from './reducers/rootReducer';
|
||||
|
||||
const persistConfig =
|
||||
{
|
||||
key : 'root',
|
||||
storage : storage,
|
||||
stateReconciler : autoMergeLevel2,
|
||||
whitelist : []
|
||||
};
|
||||
|
||||
const reduxMiddlewares =
|
||||
[
|
||||
thunk
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
{
|
||||
const reduxLogger = createLogger(
|
||||
{
|
||||
// filter VOLUME level actions from log
|
||||
predicate : (getState, action) => ! (action.type === 'SET_PRODUCER_VOLUME'
|
||||
|| action.type === 'SET_CONSUMER_VOLUME'),
|
||||
duration : true,
|
||||
timestamp : false,
|
||||
level : 'log',
|
||||
logErrors : true
|
||||
});
|
||||
|
||||
reduxMiddlewares.push(reduxLogger);
|
||||
}
|
||||
|
||||
const composeEnhancers =
|
||||
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) :
|
||||
compose;
|
||||
|
||||
const enhancer = composeEnhancers(
|
||||
applyMiddleware(...reduxMiddlewares)
|
||||
);
|
||||
|
||||
const pReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
export const store = createStore(
|
||||
pReducer,
|
||||
enhancer
|
||||
);
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getSignalingUrl(peerName, roomId)
|
||||
{
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
const port = process.env.NODE_ENV !== 'production' ? window.config.developmentPort : window.location.port;
|
||||
|
||||
const url = `wss://${hostname}:${port}/?peerName=${peerName}&roomId=${roomId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Create a function which will call the callback function
|
||||
* after the given amount of milliseconds has passed since
|
||||
* the last time the callback function was called.
|
||||
*/
|
||||
export const idle = (callback, delay) =>
|
||||
{
|
||||
let handle;
|
||||
|
||||
return () =>
|
||||
{
|
||||
if (handle)
|
||||
{
|
||||
clearTimeout(handle);
|
||||
}
|
||||
|
||||
handle = setTimeout(callback, delay);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user