diff --git a/client/src/components/Home/Activity.js b/client/src/components/Home/Activity.js deleted file mode 100644 index a531c51..0000000 --- a/client/src/components/Home/Activity.js +++ /dev/null @@ -1,156 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import Message from 'components/Message' -import Username from 'components/Username' -import Notice from 'components/Notice' -import Zoom from 'utils/ImageZoom' -import { getObjectUrl } from 'utils/file' - -import T from 'components/T' - -class Activity extends Component { - constructor(props) { - super(props) - - this.state = { - zoomableImages: [], - } - } - - getFileDisplay(activity) { - const type = activity.fileType - if (type.match('image.*')) { - return ( - this._zoomableImage = c} - className="image-transfer zoomable" - src={`data:${activity.fileType};base64,${activity.encodedFile}`} - alt={`${activity.fileName} from ${activity.username}`} - onLoad={this.handleImageDisplay.bind(this)} - /> - ) - } - return null - } - - handleImageDisplay() { - Zoom(this._zoomableImage) - this.props.scrollToBottom() - } - - getActivityComponent(activity) { - switch (activity.type) { - case 'TEXT_MESSAGE': - return ( - - ) - case 'USER_ENTER': - return ( - -
- - }} path='userJoined'/> -
-
- ) - case 'USER_EXIT': - return ( - -
- - }} path='userLeft'/> -
-
- ) - case 'TOGGLE_LOCK_ROOM': - if (activity.locked) { - return ( - -
- }} path='lockedRoom'/>
-
- ) - } else { - return ( - -
- }} path='unlockedRoom'/>
-
- ) - } - case 'NOTICE': - return ( - -
{activity.message}
-
- ) - case 'CHANGE_USERNAME': - return ( - -
, - newUsername: - }} path='nameChange'/> -
-
- ) - case 'USER_ACTION': - return ( - -
* {activity.action}
-
- ) - case 'RECEIVE_FILE': - const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType) - return ( -
- , - }} path='userSentFile'/>  - - - - - {this.getFileDisplay(activity)} -
- ) - case 'SEND_FILE': - const url = getObjectUrl(activity.encodedFile, activity.fileType) - return ( - -
- {activity.fileName}, - }} path='sentFile'/>  -
- {this.getFileDisplay(activity)} -
- ) - default: - return false - } - } - - render() { - return ( - this.getActivityComponent(this.props.activity) - ) - } -} - -Activity.propTypes = { - activity: PropTypes.object.isRequired, - scrollToBottom: PropTypes.func.isRequired, -} - -export default Activity; diff --git a/client/src/components/Home/ActivityList.js b/client/src/components/Home/ActivityList.js deleted file mode 100644 index 3c05a71..0000000 --- a/client/src/components/Home/ActivityList.js +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ChatInput from 'components/Chat' -import { defer } from 'lodash' -import Activity from './Activity' - -import T from 'components/T' - -import styles from './styles.module.scss' - -class ActivityList extends Component { - constructor(props) { - super(props) - - this.state = { - zoomableImages: [], - focusChat: false, - } - } - - componentDidMount() { - this.bindEvents() - } - - componentDidUpdate(prevProps) { - if (prevProps.activities.length < this.props.activities.length) { - this.scrollToBottomIfShould() - } - } - - onScroll() { - const messageStreamHeight = this.messageStream.clientHeight - const activitiesListHeight = this.activitiesList.clientHeight - - const bodyRect = document.body.getBoundingClientRect() - const elemRect = this.activitiesList.getBoundingClientRect() - const offset = elemRect.top - bodyRect.top - const activitiesListYPos = offset - - const scrolledToBottom = (activitiesListHeight + (activitiesListYPos - 60)) <= messageStreamHeight - if (scrolledToBottom) { - if (!this.props.scrolledToBottom) { - this.props.setScrolledToBottom(true) - } - } else if (this.props.scrolledToBottom) { - this.props.setScrolledToBottom(false) - } - } - - scrollToBottomIfShould() { - if (this.props.scrolledToBottom) { - setTimeout(() => { - this.messageStream.scrollTop = this.messageStream.scrollHeight - }, 0) - } - } - - scrollToBottom() { - this.messageStream.scrollTop = this.messageStream.scrollHeight - this.props.setScrolledToBottom(true) - } - - bindEvents() { - this.messageStream.addEventListener('scroll', this.onScroll.bind(this)) - } - - handleChatClick() { - this.setState({ focusChat: true }) - defer(() => this.setState({ focusChat: false })) - } - - render() { - return ( -
-
this.messageStream = el}> -
    this.activitiesList = el}> -
  • - {this.props.activities.map((activity, index) => ( -
  • - -
  • - ))} -
-
-
- -
-
- ) - } -} - -ActivityList.propTypes = { - activities: PropTypes.array.isRequired, - openModal: PropTypes.func.isRequired, - setScrolledToBottom: PropTypes.func.isRequired, - scrolledToBottom: PropTypes.bool.isRequired, -} - -export default ActivityList; diff --git a/client/src/components/Home/Home.js b/client/src/components/Home/Home.js deleted file mode 100644 index 32e717c..0000000 --- a/client/src/components/Home/Home.js +++ /dev/null @@ -1,277 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import Crypto from 'utils/crypto' -import { connect as connectSocket } from 'utils/socket' -import Nav from 'components/Nav' -import shortId from 'shortid' -import Connecting from 'components/Connecting' -import Modal from 'react-modal' -import About from 'components/About' -import Settings from 'components/Settings' -import Welcome from 'components/Welcome' -import RoomLocked from 'components/RoomLocked' -import { X, AlertCircle } from 'react-feather' -import { defer } from 'lodash' -import Tinycon from 'tinycon' -import beepFile from 'audio/beep.mp3' -import classNames from 'classnames' -import ActivityList from './ActivityList' - -import styles from './styles.module.scss' - -const crypto = new Crypto() - -Modal.setAppElement('#root'); - -class Home extends Component { - constructor(props) { - super(props) - - this.state = { - zoomableImages: [] - } - } - - async componentWillMount() { - const roomId = encodeURI(this.props.match.params.roomId) - - const user = await this.createUser() - - const socket = connectSocket(roomId) - - this.socket = socket; - - socket.on('disconnect', () => { - this.props.toggleSocketConnected(false) - }) - - socket.on('connect', () => { - this.initApp(user) - this.props.toggleSocketConnected(true) - }) - - socket.on('USER_ENTER', (payload) => { - this.props.receiveUnencryptedMessage('USER_ENTER', payload) - this.props.sendEncryptedMessage({ - type: 'ADD_USER', - payload: { - username: this.props.username, - publicKey: this.props.publicKey, - isOwner: this.props.iAmOwner, - id: this.props.userId, - }, - }) - if (payload.users.length === 1) { - this.props.openModal('Welcome'); - } - }) - - socket.on('USER_EXIT', (payload) => { - this.props.receiveUnencryptedMessage('USER_EXIT', payload) - }) - - socket.on('ENCRYPTED_MESSAGE', (payload) => { - this.props.receiveEncryptedMessage(payload) - }) - - socket.on('TOGGLE_LOCK_ROOM', (payload) => { - this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload) - }) - - socket.on('ROOM_LOCKED', (payload) => { - this.props.openModal('Room Locked') - }); - - window.addEventListener('beforeunload', (evt) => { - socket.emit('USER_DISCONNECT') - }); - } - - componentDidMount() { - this.bindEvents() - - this.beep = window.Audio && new window.Audio(beepFile) - } - - componentWillReceiveProps(nextProps) { - Tinycon.setBubble(nextProps.faviconCount) - - if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) { - this.beep.play() - } - } - - getModal() { - switch (this.props.modalComponent) { - case 'Connecting': - return { - component: , - title: 'Connecting...', - preventClose: true, - } - case 'About': - return { - component: , - title: this.props.translations.aboutHeader, - } - case 'Settings': - return { - component: , - title: this.props.translations.settingsHeader, - } - case 'Welcome': - return { - component: , - title: this.props.translations.welcomeHeader, - } - case 'Room Locked': - return { - component: , - title: this.props.translations.lockedRoomHeader, - preventClose: true, - } - default: - return { - component: null, - title: null, - } - } - } - - initApp(user) { - this.socket.emit('USER_ENTER', { - publicKey: user.publicKey, - }) - } - - bindEvents() { - - window.onfocus = () => { - this.props.toggleWindowFocus(true) - } - - window.onblur = () => { - this.props.toggleWindowFocus(false) - } - } - - createUser() { - return new Promise(async (resolve) => { - const username = shortId.generate() - - const encryptDecryptKeys = await crypto.createEncryptDecryptKeys() - const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey) - const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey) - - this.props.createUser({ - username, - publicKey: exportedEncryptDecryptPublicKey, - privateKey: exportedEncryptDecryptPrivateKey, - }) - - resolve({ - publicKey: exportedEncryptDecryptPublicKey, - }) - }) - } - - handleChatClick() { - this.setState({ focusChat: true }) - defer(() => this.setState({ focusChat: false })) - } - - render() { - const modalOpts = this.getModal() - return ( -
-
- {!this.props.socketConnected && -
- Disconnected -
- } -
- - -
- {!modalOpts.preventClose && - - } -

- {modalOpts.title} -

-
-
- {modalOpts.component} -
-
-
- ) - } -} - -Home.defaultProps = { - modalComponent: null, -} - -Home.propTypes = { - receiveEncryptedMessage: PropTypes.func.isRequired, - createUser: PropTypes.func.isRequired, - activities: PropTypes.array.isRequired, - username: PropTypes.string.isRequired, - publicKey: PropTypes.object.isRequired, - members: PropTypes.array.isRequired, - match: PropTypes.object.isRequired, - roomId: PropTypes.string.isRequired, - roomLocked: PropTypes.bool.isRequired, - modalComponent: PropTypes.string, - openModal: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired, - setScrolledToBottom: PropTypes.func.isRequired, - scrolledToBottom: PropTypes.bool.isRequired, - iAmOwner: PropTypes.bool.isRequired, - userId: PropTypes.string.isRequired, - toggleWindowFocus: PropTypes.func.isRequired, - faviconCount: PropTypes.number.isRequired, - soundIsEnabled: PropTypes.bool.isRequired, - toggleSoundEnabled: PropTypes.func.isRequired, - toggleSocketConnected: PropTypes.func.isRequired, - socketConnected: PropTypes.bool.isRequired, - sendUnencryptedMessage: PropTypes.func.isRequired, - sendEncryptedMessage: PropTypes.func.isRequired -} - -export default Home; diff --git a/client/src/components/Home/index.js b/client/src/components/Home/index.js index b90450c..b427a14 100644 --- a/client/src/components/Home/index.js +++ b/client/src/components/Home/index.js @@ -1,4 +1,26 @@ -import Home from './Home' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Crypto from 'utils/crypto' +import { connect as connectSocket } from 'utils/socket' +import Nav from 'components/Nav' +import shortId from 'shortid' +import ChatInput from 'components/Chat' +import Connecting from 'components/Connecting' +import Message from 'components/Message' +import Username from 'components/Username' +import Notice from 'components/Notice' +import Modal from 'react-modal' +import About from 'components/About' +import Settings from 'components/Settings' +import Welcome from 'components/Welcome' +import RoomLocked from 'components/RoomLocked' +import { X, AlertCircle } from 'react-feather' +import { defer } from 'lodash' +import Tinycon from 'tinycon' +import beepFile from 'audio/beep.mp3' +import Zoom from 'utils/ImageZoom' +import classNames from 'classnames' +import { getObjectUrl } from 'utils/file' import { connect } from 'react-redux' import { receiveEncryptedMessage, @@ -14,6 +36,439 @@ import { sendEncryptedMessage, setLanguage } from 'actions' +import T from 'components/T' + +import styles from './styles.module.scss' + +const crypto = new Crypto() + +Modal.setAppElement('#root'); + +class Home extends Component { + constructor(props) { + super(props) + + this.state = { + zoomableImages: [], + focusChat: false, + } + + this.hasConnected = false + } + + async componentWillMount() { + const roomId = encodeURI(this.props.match.params.roomId) + + const user = await this.createUser() + + const socket = connectSocket(roomId) + + this.socket = socket; + + socket.on('disconnect', () => { + this.props.toggleSocketConnected(false) + }) + + socket.on('connect', () => { + this.initApp(user) + this.props.toggleSocketConnected(true) + }) + + socket.on('USER_ENTER', (payload) => { + this.props.receiveUnencryptedMessage('USER_ENTER', payload) + this.props.sendEncryptedMessage({ + type: 'ADD_USER', + payload: { + username: this.props.username, + publicKey: this.props.publicKey, + isOwner: this.props.iAmOwner, + id: this.props.userId, + }, + }) + if (payload.users.length === 1) { + this.props.openModal('Welcome'); + } + }) + + socket.on('USER_EXIT', (payload) => { + this.props.receiveUnencryptedMessage('USER_EXIT', payload) + }) + + socket.on('ENCRYPTED_MESSAGE', (payload) => { + this.props.receiveEncryptedMessage(payload) + }) + + socket.on('TOGGLE_LOCK_ROOM', (payload) => { + this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload) + }) + + socket.on('ROOM_LOCKED', (payload) => { + this.props.openModal('Room Locked') + }); + + window.addEventListener('beforeunload', (evt) => { + socket.emit('USER_DISCONNECT') + }); + } + + componentDidMount() { + this.bindEvents() + + this.beep = window.Audio && new window.Audio(beepFile) + } + + componentWillReceiveProps(nextProps) { + Tinycon.setBubble(nextProps.faviconCount) + + if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) { + this.beep.play() + } + } + + componentDidUpdate(prevProps) { + if (prevProps.activities.length < this.props.activities.length) { + this.scrollToBottomIfShould() + } + } + + onScroll() { + const messageStreamHeight = this.messageStream.clientHeight + const activitiesListHeight = this.activitiesList.clientHeight + + const bodyRect = document.body.getBoundingClientRect() + const elemRect = this.activitiesList.getBoundingClientRect() + const offset = elemRect.top - bodyRect.top + const activitiesListYPos = offset + + const scrolledToBottom = (activitiesListHeight + (activitiesListYPos - 60)) <= messageStreamHeight + if (scrolledToBottom) { + if (!this.props.scrolledToBottom) { + this.props.setScrolledToBottom(true) + } + } else if (this.props.scrolledToBottom) { + this.props.setScrolledToBottom(false) + } + } + + getFileDisplay(activity) { + const type = activity.fileType + if (type.match('image.*')) { + return ( + this._zoomableImage = c} + className="image-transfer zoomable" + src={`data:${activity.fileType};base64,${activity.encodedFile}`} + alt={`${activity.fileName} from ${activity.username}`} + onLoad={this.handleImageDisplay.bind(this)} + /> + ) + } + return null + } + + getActivityComponent(activity) { + switch (activity.type) { + case 'TEXT_MESSAGE': + return ( + + ) + case 'USER_ENTER': + return ( + +
+ + }} path='userJoined'/> +
+
+ ) + case 'USER_EXIT': + return ( + +
+ + }} path='userLeft'/> +
+
+ ) + case 'TOGGLE_LOCK_ROOM': + if (activity.locked) { + return ( + +
+ }} path='lockedRoom'/>
+
+ ) + } else { + return ( + +
+ }} path='unlockedRoom'/>
+
+ ) + } + case 'NOTICE': + return ( + +
{activity.message}
+
+ ) + case 'CHANGE_USERNAME': + return ( + +
, + newUsername: + }} path='nameChange'/> +
+
+ ) + case 'USER_ACTION': + return ( + +
* {activity.action}
+
+ ) + case 'RECEIVE_FILE': + const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType) + return ( +
+ , + }} path='userSentFile'/>  + + + + + {this.getFileDisplay(activity)} +
+ ) + case 'SEND_FILE': + const url = getObjectUrl(activity.encodedFile, activity.fileType) + return ( + +
+ {activity.fileName}, + }} path='sentFile'/>  +
+ {this.getFileDisplay(activity)} +
+ ) + default: + return false + } + } + + getModal() { + switch (this.props.modalComponent) { + case 'Connecting': + return { + component: , + title: 'Connecting...', + preventClose: true, + } + case 'About': + return { + component: , + title: this.props.translations.aboutHeader, + } + case 'Settings': + return { + component: , + title: this.props.translations.settingsHeader, + } + case 'Welcome': + return { + component: , + title: this.props.translations.welcomeHeader, + } + case 'Room Locked': + return { + component: , + title: this.props.translations.lockedRoomHeader, + preventClose: true, + } + default: + return { + component: null, + title: null, + } + } + } + + initApp(user) { + this.socket.emit('USER_ENTER', { + publicKey: user.publicKey, + }) + } + + handleImageDisplay() { + Zoom(this._zoomableImage) + this.scrollToBottomIfShould() + } + + scrollToBottomIfShould() { + if (this.props.scrolledToBottom) { + setTimeout(() => { + this.messageStream.scrollTop = this.messageStream.scrollHeight + }, 0) + } + } + + scrollToBottom() { + this.messageStream.scrollTop = this.messageStream.scrollHeight + this.props.setScrolledToBottom(true) + } + + bindEvents() { + this.messageStream.addEventListener('scroll', this.onScroll.bind(this)) + + window.onfocus = () => { + this.props.toggleWindowFocus(true) + } + + window.onblur = () => { + this.props.toggleWindowFocus(false) + } + } + + createUser() { + return new Promise(async (resolve) => { + const username = shortId.generate() + + const encryptDecryptKeys = await crypto.createEncryptDecryptKeys() + const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey) + const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey) + + this.props.createUser({ + username, + publicKey: exportedEncryptDecryptPublicKey, + privateKey: exportedEncryptDecryptPrivateKey, + }) + + resolve({ + publicKey: exportedEncryptDecryptPublicKey, + }) + }) + } + + handleChatClick() { + this.setState({ focusChat: true }) + defer(() => this.setState({ focusChat: false })) + } + + render() { + const modalOpts = this.getModal() + return ( +
+
+ {!this.props.socketConnected && +
+ Disconnected +
+ } +
+
+
this.messageStream = el}> +
    this.activitiesList = el}> +
  • + {this.props.activities.map((activity, index) => ( +
  • + {this.getActivityComponent(activity)} +
  • + ))} +
+
+
+ +
+
+ +
+ {!modalOpts.preventClose && + + } +

+ {modalOpts.title} +

+
+
+ {modalOpts.component} +
+
+
+ ) + } +} + +Home.defaultProps = { + modalComponent: null, +} + +Home.propTypes = { + receiveEncryptedMessage: PropTypes.func.isRequired, + createUser: PropTypes.func.isRequired, + activities: PropTypes.array.isRequired, + username: PropTypes.string.isRequired, + publicKey: PropTypes.object.isRequired, + members: PropTypes.array.isRequired, + match: PropTypes.object.isRequired, + roomId: PropTypes.string.isRequired, + roomLocked: PropTypes.bool.isRequired, + modalComponent: PropTypes.string, + openModal: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, + setScrolledToBottom: PropTypes.func.isRequired, + scrolledToBottom: PropTypes.bool.isRequired, + iAmOwner: PropTypes.bool.isRequired, + userId: PropTypes.string.isRequired, + toggleWindowFocus: PropTypes.func.isRequired, + faviconCount: PropTypes.number.isRequired, + soundIsEnabled: PropTypes.bool.isRequired, + toggleSoundEnabled: PropTypes.func.isRequired, + toggleSocketConnected: PropTypes.func.isRequired, + socketConnected: PropTypes.bool.isRequired, + sendUnencryptedMessage: PropTypes.func.isRequired, + sendEncryptedMessage: PropTypes.func.isRequired +} const mapStateToProps = (state) => { const me = state.room.members.find(m => m.id === state.user.id)