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,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;
|
||||
Reference in New Issue
Block a user