Major cleanup. Moved a lot of CSS variables out too root of CSS for easier styling.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import MessageList from './MessageList';
|
||||
|
||||
class Chat extends Component
|
||||
{
|
||||
createNewMessage(text, sender, name, picture)
|
||||
{
|
||||
return {
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
senderPlaceHolder,
|
||||
disabledInput,
|
||||
autofocus,
|
||||
displayName,
|
||||
picture
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='Chat'>
|
||||
<MessageList />
|
||||
<form
|
||||
data-component='Sender'
|
||||
onSubmit={(e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
const userInput = e.target.message.value;
|
||||
|
||||
if (userInput)
|
||||
{
|
||||
const message = this.createNewMessage(userInput, 'response', displayName, picture);
|
||||
|
||||
roomClient.sendChatMessage(message);
|
||||
}
|
||||
e.target.message.value = '';
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type='text'
|
||||
className='new-message'
|
||||
name='message'
|
||||
placeholder={senderPlaceHolder}
|
||||
disabled={disabledInput}
|
||||
autoFocus={autofocus}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
className='send'
|
||||
value='Send'
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
senderPlaceHolder : PropTypes.string,
|
||||
disabledInput : PropTypes.bool,
|
||||
autofocus : PropTypes.bool,
|
||||
displayName : PropTypes.string,
|
||||
picture : PropTypes.string
|
||||
};
|
||||
|
||||
Chat.defaultProps =
|
||||
{
|
||||
senderPlaceHolder : 'Type a message...',
|
||||
autofocus : false,
|
||||
displayName : null
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
disabledInput : state.chatbehavior.disabledInput,
|
||||
displayName : state.me.displayName,
|
||||
picture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const ChatContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(Chat));
|
||||
|
||||
export default ChatContainer;
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
|
||||
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>`);
|
||||
};
|
||||
|
||||
class MessageList extends Component
|
||||
{
|
||||
getTimeString(time)
|
||||
{
|
||||
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
chatmessages
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='MessageList' id='messages'>
|
||||
<Choose>
|
||||
<When condition={chatmessages.length > 0}>
|
||||
{
|
||||
chatmessages.map((message, i) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
|
||||
const picture = (message.sender === 'response' ?
|
||||
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div className='message' key={i}>
|
||||
<div className={message.sender}>
|
||||
<img className='message-avatar' src={picture} />
|
||||
|
||||
<div className='message-content'>
|
||||
<div
|
||||
className='message-text'
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html : marked.parse(
|
||||
message.text,
|
||||
{ sanitize: true, renderer: linkRenderer }
|
||||
) }}
|
||||
/>
|
||||
|
||||
<span className='message-time'>
|
||||
{message.name} - {this.getTimeString(messageTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div className='empty'>
|
||||
<p>No one has said anything yet...</p>
|
||||
</div>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageList.propTypes =
|
||||
{
|
||||
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
myPicture : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
chatmessages : state.chatmessages,
|
||||
myPicture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const MessageListContainer = compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(MessageList);
|
||||
|
||||
export default MessageListContainer;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import WebTorrent from 'webtorrent';
|
||||
import dragDrop from 'drag-drop';
|
||||
import { shareFiles } from './index';
|
||||
|
||||
export const configureDragDrop = () =>
|
||||
{
|
||||
if (WebTorrent.WEBRTC_SUPPORT)
|
||||
{
|
||||
dragDrop('body', async (files) => await shareFiles(files));
|
||||
}
|
||||
};
|
||||
|
||||
export const HoldingOverlay = () => (
|
||||
<div id='holding-overlay'>
|
||||
Drop files here to share them
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import magnet from 'magnet-uri';
|
||||
|
||||
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
class File extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
file
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='file-entry'>
|
||||
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
|
||||
|
||||
<div className='file-content'>
|
||||
<Choose>
|
||||
<When condition={file.me}>
|
||||
<p>You shared a file.</p>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>{file.displayName} shared a file.</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={!file.active && !file.files}>
|
||||
<div className='file-info'>
|
||||
<Choose>
|
||||
<When condition={torrentSupport}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/download-icon.svg' />
|
||||
</span>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<p>{magnet.decode(file.magnetUri).dn}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={file.timeout}>
|
||||
<Fragment>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</Fragment>
|
||||
</If>
|
||||
|
||||
<If condition={file.active}>
|
||||
<progress value={file.progress} />
|
||||
</If>
|
||||
|
||||
<If condition={file.files}>
|
||||
<Fragment>
|
||||
<p>File finished downloading.</p>
|
||||
|
||||
{file.files.map((sharedFile, i) => (
|
||||
<div className='file-info' key={i}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.saveFile(sharedFile);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/save-icon.svg' />
|
||||
</span>
|
||||
|
||||
<p>{sharedFile.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(File));
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
import File from './File';
|
||||
|
||||
class FileList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
files
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
files : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(FileList);
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import FileList from './FileList';
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () =>
|
||||
{
|
||||
if (this.props.torrentSupport)
|
||||
{
|
||||
// We want to open the file dialog when we click a button
|
||||
// instead of actually rendering the input element itself.
|
||||
this._fileInput.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
<div data-component='FileSharing'>
|
||||
<div className='sharing-toolbar'>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
ref={this._fileInput}
|
||||
type='file'
|
||||
onChange={this.handleFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div
|
||||
type='button'
|
||||
onClick={this.handleClick}
|
||||
className={classNames('share-file', {
|
||||
disabled : !torrentSupport
|
||||
})}
|
||||
>
|
||||
<span>{buttonDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(FileSharing));
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Me } from '../../appPropTypes';
|
||||
|
||||
const ListMe = ({ me }) =>
|
||||
{
|
||||
const picture = me.picture || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<li className='list-item me'>
|
||||
<div data-component='ListPeer'>
|
||||
<img className='avatar' src={picture} />
|
||||
|
||||
<div className='peer-info'>
|
||||
{me.displayName}
|
||||
</div>
|
||||
|
||||
<div className='indicators'>
|
||||
{me.raisedHand && (
|
||||
<div className='icon raise-hand on' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ListMe.propTypes = {
|
||||
me : Me.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
me : state.me
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(ListMe);
|
||||
@@ -0,0 +1,124 @@
|
||||
import React 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';
|
||||
|
||||
const ListPeer = (props) =>
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
peer,
|
||||
micConsumer,
|
||||
screenConsumer
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const picture = peer.picture || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div data-component='ListPeer'>
|
||||
<img className='avatar' src={picture} />
|
||||
|
||||
<div className='peer-info'>
|
||||
{peer.displayName}
|
||||
</div>
|
||||
<div className='indicators'>
|
||||
<If condition={peer.raiseHandState}>
|
||||
<div className={
|
||||
classnames(
|
||||
'icon', 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className='controls'>
|
||||
<If condition={screenConsumer}>
|
||||
<div
|
||||
className={classnames('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);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<div
|
||||
className={classnames('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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListPeer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
const ListPeerContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(ListPeer));
|
||||
|
||||
export default ListPeerContainer;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListPeer from './ListPeer';
|
||||
import ListMe from './ListMe';
|
||||
|
||||
const ParticipantList =
|
||||
({
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peers,
|
||||
selectedPeerName,
|
||||
spotlights
|
||||
}) => (
|
||||
<div data-component='ParticipantList'>
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Me:</li>
|
||||
<ListMe />
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Participants in Spotlight:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return (spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Passive Participants:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return !(spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
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
|
||||
};
|
||||
|
||||
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
|
||||
)(ParticipantList));
|
||||
|
||||
export default ParticipantListContainer;
|
||||
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import * as stateActions from '../../../redux/stateActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'react-dropdown';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
const modes = [ {
|
||||
value : 'democratic',
|
||||
label : 'Democratic view'
|
||||
}, {
|
||||
value : 'filmstrip',
|
||||
label : 'Filmstrip view'
|
||||
} ];
|
||||
|
||||
const findOption = (options, value) => options.find((option) => option.value === value);
|
||||
|
||||
const Settings = ({
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
onToggleAdvancedMode,
|
||||
handleChangeMode
|
||||
}) =>
|
||||
{
|
||||
let webcams;
|
||||
|
||||
if (me.webcamDevices)
|
||||
webcams = Array.from(me.webcamDevices.values());
|
||||
else
|
||||
webcams = [];
|
||||
|
||||
let audioDevices;
|
||||
let audioDevicesText;
|
||||
|
||||
if (me.canChangeAudioDevice)
|
||||
audioDevicesText = 'Select audio input device';
|
||||
else
|
||||
audioDevicesText = 'Unable to select audio input device';
|
||||
|
||||
if (me.audioDevices)
|
||||
audioDevices = Array.from(me.audioDevices.values());
|
||||
else
|
||||
audioDevices = [];
|
||||
|
||||
return (
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
options={webcams}
|
||||
value={findOption(webcams, me.selectedWebcam)}
|
||||
onChange={(webcam) => roomClient.changeWebcam(webcam.value)}
|
||||
placeholder={'Select camera'}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
options={audioDevices}
|
||||
value={findOption(audioDevices, me.selectedAudioDevice)}
|
||||
onChange={(device) => roomClient.changeAudioDevice(device.value)}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘a‘'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<input
|
||||
id='room-mode'
|
||||
type='checkbox'
|
||||
checked={room.advancedMode}
|
||||
onChange={onToggleAdvancedMode}
|
||||
/>
|
||||
<label htmlFor='room-mode'>Advanced mode</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tip='keyboard shortcut: type a digit'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<Dropdown
|
||||
options={modes}
|
||||
value={findOption(modes, room.mode)}
|
||||
onChange={(mode) => handleChangeMode(mode.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||
handleChangeMode : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
room : state.room
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||
handleChangeMode : stateActions.setDisplayMode
|
||||
};
|
||||
|
||||
const SettingsContainer = withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Settings));
|
||||
|
||||
export default SettingsContainer;
|
||||
@@ -3,10 +3,10 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import ParticipantList from '../ParticipantList/ParticipantList';
|
||||
import Chat from '../Chat/Chat';
|
||||
import Settings from '../Settings';
|
||||
import FileSharing from '../FileSharing/FileSharing';
|
||||
import ParticipantList from './ParticipantList/ParticipantList';
|
||||
import Chat from './Chat/Chat';
|
||||
import Settings from './Settings/Settings';
|
||||
import FileSharing from './FileSharing/FileSharing';
|
||||
import TabHeader from './TabHeader';
|
||||
|
||||
class ToolArea extends React.Component
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
/**
|
||||
* A higher order component which scrolls the user to the bottom of the
|
||||
* wrapped component, provided that the user already was at the bottom
|
||||
* of the wrapped component. Useful for chats and similar use cases.
|
||||
* @param {number} treshold The required distance from the bottom required.
|
||||
*/
|
||||
const scrollToBottom = (treshold = 0) => (WrappedComponent) =>
|
||||
{
|
||||
return class AutoScroller extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.ref = React.createRef();
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate()
|
||||
{
|
||||
// Check if the user has scrolled close enough to the bottom for
|
||||
// us to scroll to the bottom or not.
|
||||
return this.elem.scrollHeight - this.elem.scrollTop <=
|
||||
this.elem.clientHeight - treshold;
|
||||
}
|
||||
|
||||
scrollToBottom = () =>
|
||||
{
|
||||
// Scroll the user to the bottom of the wrapped element.
|
||||
this.elem.scrollTop = this.elem.scrollHeight;
|
||||
};
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
this.elem = findDOMNode(this.ref.current);
|
||||
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, atBottom)
|
||||
{
|
||||
if (atBottom)
|
||||
{
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<WrappedComponent
|
||||
ref={this.ref}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default scrollToBottom;
|
||||
Reference in New Issue
Block a user