Major cleanup. Moved a lot of CSS variables out too root of CSS for easier styling.

This commit is contained in:
Håvar Aambø Fosstveit
2018-12-18 12:13:47 +01:00
parent db9d423feb
commit 651cd6f737
44 changed files with 296 additions and 517 deletions
+101
View File
@@ -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: &lsquo;a&lsquo;'
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;
+4 -4
View File
@@ -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;