mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 02:44:01 +00:00
Revert "First step of spliting home component (#145)"
This reverts commit 141da51f74ff50a35399d7d6b7552007b464b441.
This commit is contained in:
parent
141da51f74
commit
2fe16ce0c7
@ -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 (
|
|
||||||
<img
|
|
||||||
ref={c => 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 (
|
|
||||||
<Message
|
|
||||||
sender={activity.username}
|
|
||||||
message={activity.text}
|
|
||||||
timestamp={activity.timestamp}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'USER_ENTER':
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div>
|
|
||||||
<T data={{
|
|
||||||
username: <Username key={0} username={activity.username} />
|
|
||||||
}} path='userJoined'/>
|
|
||||||
</div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
case 'USER_EXIT':
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div>
|
|
||||||
<T data={{
|
|
||||||
username: <Username key={0} username={activity.username} />
|
|
||||||
}} path='userLeft'/>
|
|
||||||
</div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
case 'TOGGLE_LOCK_ROOM':
|
|
||||||
if (activity.locked) {
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div><T data={{
|
|
||||||
username: <Username key={0} username={activity.username} />
|
|
||||||
}} path='lockedRoom'/></div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div><T data={{
|
|
||||||
username: <Username key={0} username={activity.username} />
|
|
||||||
}} path='unlockedRoom'/></div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case 'NOTICE':
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div>{activity.message}</div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
case 'CHANGE_USERNAME':
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div><T data={{
|
|
||||||
oldUsername: <Username key={0} username={activity.currentUsername} />,
|
|
||||||
newUsername: <Username key={1} username={activity.newUsername} />
|
|
||||||
}} path='nameChange'/>
|
|
||||||
</div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
case 'USER_ACTION':
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div>* <Username username={activity.username} /> {activity.action}</div>
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
case 'RECEIVE_FILE':
|
|
||||||
const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<T data={{
|
|
||||||
username: <Username key={0} username={activity.username} />,
|
|
||||||
}} path='userSentFile'/>
|
|
||||||
|
|
||||||
<a target="_blank" href={downloadUrl} rel="noopener noreferrer" download={activity.fileName}>
|
|
||||||
<T data={{
|
|
||||||
filename: activity.fileName,
|
|
||||||
}} path='downloadFile'/>
|
|
||||||
</a>
|
|
||||||
{this.getFileDisplay(activity)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case 'SEND_FILE':
|
|
||||||
const url = getObjectUrl(activity.encodedFile, activity.fileType)
|
|
||||||
return (
|
|
||||||
<Notice>
|
|
||||||
<div>
|
|
||||||
<T data={{
|
|
||||||
filename: <a key={0} target="_blank" href={url} rel="noopener noreferrer" download={activity.fileName}>{activity.fileName}</a>,
|
|
||||||
}} path='sentFile'/>
|
|
||||||
</div>
|
|
||||||
{this.getFileDisplay(activity)}
|
|
||||||
</Notice>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
this.getActivityComponent(this.props.activity)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Activity.propTypes = {
|
|
||||||
activity: PropTypes.object.isRequired,
|
|
||||||
scrollToBottom: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Activity;
|
|
@ -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 (
|
|
||||||
<div className="main-chat">
|
|
||||||
<div onClick={this.handleChatClick.bind(this)} className="message-stream h-100" ref={el => this.messageStream = el}>
|
|
||||||
<ul className="plain" ref={el => this.activitiesList = el}>
|
|
||||||
<li><p className={styles.tos}><button className='btn btn-link' onClick={this.props.openModal.bind(this, 'About')}> <T path='agreement'/></button></p></li>
|
|
||||||
{this.props.activities.map((activity, index) => (
|
|
||||||
<li key={index} className={`activity-item ${activity.type}`}>
|
|
||||||
<Activity activity={activity} scrollToBottom={this.scrollToBottomIfShould.bind(this)} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="chat-container">
|
|
||||||
<ChatInput scrollToBottom={this.scrollToBottom.bind(this)} focusChat={this.state.focusChat} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityList.propTypes = {
|
|
||||||
activities: PropTypes.array.isRequired,
|
|
||||||
openModal: PropTypes.func.isRequired,
|
|
||||||
setScrolledToBottom: PropTypes.func.isRequired,
|
|
||||||
scrolledToBottom: PropTypes.bool.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActivityList;
|
|
@ -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: <Connecting />,
|
|
||||||
title: 'Connecting...',
|
|
||||||
preventClose: true,
|
|
||||||
}
|
|
||||||
case 'About':
|
|
||||||
return {
|
|
||||||
component: <About roomId={this.props.roomId} />,
|
|
||||||
title: this.props.translations.aboutHeader,
|
|
||||||
}
|
|
||||||
case 'Settings':
|
|
||||||
return {
|
|
||||||
component: <Settings roomId={this.props.roomId} toggleSoundEnabled={this.props.toggleSoundEnabled} soundIsEnabled={this.props.soundIsEnabled} setLanguage={this.props.setLanguage} language={this.props.language} translations={this.props.translations} />,
|
|
||||||
title: this.props.translations.settingsHeader,
|
|
||||||
}
|
|
||||||
case 'Welcome':
|
|
||||||
return {
|
|
||||||
component: <Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />,
|
|
||||||
title: this.props.translations.welcomeHeader,
|
|
||||||
}
|
|
||||||
case 'Room Locked':
|
|
||||||
return {
|
|
||||||
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
|
|
||||||
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 (
|
|
||||||
<div className={classNames(styles.styles, 'h-100')}>
|
|
||||||
<div className="nav-container">
|
|
||||||
{!this.props.socketConnected &&
|
|
||||||
<div className="alert-banner">
|
|
||||||
<span className="icon"><AlertCircle size="15" /></span> <span>Disconnected</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Nav
|
|
||||||
members={this.props.members}
|
|
||||||
roomId={this.props.roomId}
|
|
||||||
roomLocked={this.props.roomLocked}
|
|
||||||
toggleLockRoom={() => this.props.sendUnencryptedMessage('TOGGLE_LOCK_ROOM')}
|
|
||||||
openModal={this.props.openModal}
|
|
||||||
iAmOwner={this.props.iAmOwner}
|
|
||||||
userId={this.props.userId}
|
|
||||||
translations={this.props.translations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ActivityList
|
|
||||||
openModal={this.props.openModal}
|
|
||||||
activities={this.props.activities}
|
|
||||||
setScrolledToBottom={this.props.setScrolledToBottom}
|
|
||||||
scrolledToBottom={this.props.scrolledToBottom}
|
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
isOpen={Boolean(this.props.modalComponent)}
|
|
||||||
contentLabel="Modal"
|
|
||||||
style={{ overlay: { zIndex: 10 } }}
|
|
||||||
className={{
|
|
||||||
base: 'react-modal-content',
|
|
||||||
afterOpen: 'react-modal-content_after-open',
|
|
||||||
beforeClose: 'react-modal-content_before-close',
|
|
||||||
}}
|
|
||||||
overlayClassName={{
|
|
||||||
base: 'react-modal-overlay',
|
|
||||||
afterOpen: 'react-modal-overlay_after-open',
|
|
||||||
beforeClose: 'react-modal-overlay_before-close',
|
|
||||||
}}
|
|
||||||
shouldCloseOnOverlayClick={!modalOpts.preventClose}
|
|
||||||
onRequestClose={this.props.closeModal}
|
|
||||||
>
|
|
||||||
<div className="react-modal-header">
|
|
||||||
{!modalOpts.preventClose &&
|
|
||||||
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal">
|
|
||||||
<X />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<h3 className="react-modal-title">
|
|
||||||
{modalOpts.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="react-modal-component">
|
|
||||||
{modalOpts.component}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
@ -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 { connect } from 'react-redux'
|
||||||
import {
|
import {
|
||||||
receiveEncryptedMessage,
|
receiveEncryptedMessage,
|
||||||
@ -14,6 +36,439 @@ import {
|
|||||||
sendEncryptedMessage,
|
sendEncryptedMessage,
|
||||||
setLanguage
|
setLanguage
|
||||||
} from 'actions'
|
} 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 (
|
||||||
|
<img
|
||||||
|
ref={c => 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 (
|
||||||
|
<Message
|
||||||
|
sender={activity.username}
|
||||||
|
message={activity.text}
|
||||||
|
timestamp={activity.timestamp}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'USER_ENTER':
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div>
|
||||||
|
<T data={{
|
||||||
|
username: <Username key={0} username={activity.username} />
|
||||||
|
}} path='userJoined'/>
|
||||||
|
</div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
case 'USER_EXIT':
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div>
|
||||||
|
<T data={{
|
||||||
|
username: <Username key={0} username={activity.username} />
|
||||||
|
}} path='userLeft'/>
|
||||||
|
</div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
case 'TOGGLE_LOCK_ROOM':
|
||||||
|
if (activity.locked) {
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div><T data={{
|
||||||
|
username: <Username key={0} username={activity.username} />
|
||||||
|
}} path='lockedRoom'/></div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div><T data={{
|
||||||
|
username: <Username key={0} username={activity.username} />
|
||||||
|
}} path='unlockedRoom'/></div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'NOTICE':
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div>{activity.message}</div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
case 'CHANGE_USERNAME':
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div><T data={{
|
||||||
|
oldUsername: <Username key={0} username={activity.currentUsername} />,
|
||||||
|
newUsername: <Username key={1} username={activity.newUsername} />
|
||||||
|
}} path='nameChange'/>
|
||||||
|
</div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
case 'USER_ACTION':
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div>* <Username username={activity.username} /> {activity.action}</div>
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
case 'RECEIVE_FILE':
|
||||||
|
const downloadUrl = getObjectUrl(activity.encodedFile, activity.fileType)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<T data={{
|
||||||
|
username: <Username key={0} username={activity.username} />,
|
||||||
|
}} path='userSentFile'/>
|
||||||
|
|
||||||
|
<a target="_blank" href={downloadUrl} rel="noopener noreferrer" download={activity.fileName}>
|
||||||
|
<T data={{
|
||||||
|
filename: activity.fileName,
|
||||||
|
}} path='downloadFile'/>
|
||||||
|
</a>
|
||||||
|
{this.getFileDisplay(activity)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case 'SEND_FILE':
|
||||||
|
const url = getObjectUrl(activity.encodedFile, activity.fileType)
|
||||||
|
return (
|
||||||
|
<Notice>
|
||||||
|
<div>
|
||||||
|
<T data={{
|
||||||
|
filename: <a key={0} target="_blank" href={url} rel="noopener noreferrer" download={activity.fileName}>{activity.fileName}</a>,
|
||||||
|
}} path='sentFile'/>
|
||||||
|
</div>
|
||||||
|
{this.getFileDisplay(activity)}
|
||||||
|
</Notice>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getModal() {
|
||||||
|
switch (this.props.modalComponent) {
|
||||||
|
case 'Connecting':
|
||||||
|
return {
|
||||||
|
component: <Connecting />,
|
||||||
|
title: 'Connecting...',
|
||||||
|
preventClose: true,
|
||||||
|
}
|
||||||
|
case 'About':
|
||||||
|
return {
|
||||||
|
component: <About roomId={this.props.roomId} />,
|
||||||
|
title: this.props.translations.aboutHeader,
|
||||||
|
}
|
||||||
|
case 'Settings':
|
||||||
|
return {
|
||||||
|
component: <Settings roomId={this.props.roomId} toggleSoundEnabled={this.props.toggleSoundEnabled} soundIsEnabled={this.props.soundIsEnabled} setLanguage={this.props.setLanguage} language={this.props.language} translations={this.props.translations} />,
|
||||||
|
title: this.props.translations.settingsHeader,
|
||||||
|
}
|
||||||
|
case 'Welcome':
|
||||||
|
return {
|
||||||
|
component: <Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />,
|
||||||
|
title: this.props.translations.welcomeHeader,
|
||||||
|
}
|
||||||
|
case 'Room Locked':
|
||||||
|
return {
|
||||||
|
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
|
||||||
|
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 (
|
||||||
|
<div className={classNames(styles.styles, 'h-100')}>
|
||||||
|
<div className="nav-container">
|
||||||
|
{!this.props.socketConnected &&
|
||||||
|
<div className="alert-banner">
|
||||||
|
<span className="icon"><AlertCircle size="15" /></span> <span>Disconnected</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<Nav
|
||||||
|
members={this.props.members}
|
||||||
|
roomId={this.props.roomId}
|
||||||
|
roomLocked={this.props.roomLocked}
|
||||||
|
toggleLockRoom={() => this.props.sendUnencryptedMessage('TOGGLE_LOCK_ROOM')}
|
||||||
|
openModal={this.props.openModal}
|
||||||
|
iAmOwner={this.props.iAmOwner}
|
||||||
|
userId={this.props.userId}
|
||||||
|
translations={this.props.translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="main-chat">
|
||||||
|
<div onClick={this.handleChatClick.bind(this)} className="message-stream h-100" ref={el => this.messageStream = el}>
|
||||||
|
<ul className="plain" ref={el => this.activitiesList = el}>
|
||||||
|
<li><p className={styles.tos}><button className='btn btn-link' onClick={this.props.openModal.bind(this, 'About')}> <T path='agreement'/></button></p></li>
|
||||||
|
{this.props.activities.map((activity, index) => (
|
||||||
|
<li key={index} className={`activity-item ${activity.type}`}>
|
||||||
|
{this.getActivityComponent(activity)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="chat-container">
|
||||||
|
<ChatInput scrollToBottom={this.scrollToBottom.bind(this)} focusChat={this.state.focusChat} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={Boolean(this.props.modalComponent)}
|
||||||
|
contentLabel="Modal"
|
||||||
|
style={{ overlay: { zIndex: 10 } }}
|
||||||
|
className={{
|
||||||
|
base: 'react-modal-content',
|
||||||
|
afterOpen: 'react-modal-content_after-open',
|
||||||
|
beforeClose: 'react-modal-content_before-close',
|
||||||
|
}}
|
||||||
|
overlayClassName={{
|
||||||
|
base: 'react-modal-overlay',
|
||||||
|
afterOpen: 'react-modal-overlay_after-open',
|
||||||
|
beforeClose: 'react-modal-overlay_before-close',
|
||||||
|
}}
|
||||||
|
shouldCloseOnOverlayClick={!modalOpts.preventClose}
|
||||||
|
onRequestClose={this.props.closeModal}
|
||||||
|
>
|
||||||
|
<div className="react-modal-header">
|
||||||
|
{!modalOpts.preventClose &&
|
||||||
|
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal">
|
||||||
|
<X />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<h3 className="react-modal-title">
|
||||||
|
{modalOpts.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="react-modal-component">
|
||||||
|
{modalOpts.component}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 mapStateToProps = (state) => {
|
||||||
const me = state.room.members.find(m => m.id === state.user.id)
|
const me = state.room.members.find(m => m.id === state.user.id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user