Merge pull request #76 from darkwire/refactor

Refactor actions and reducers
This commit is contained in:
Alan Friedman 2019-05-28 08:29:11 -04:00 committed by GitHub
commit a70efc5674
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 238 additions and 433 deletions

View File

@ -21,7 +21,6 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"query-string": "^6.5.0",
"randomcolor": "^0.5.4", "randomcolor": "^0.5.4",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",

View File

@ -18,3 +18,11 @@ export const toggleSoundEnabled = payload => async (dispatch) => {
export const toggleSocketConnected = payload => async (dispatch) => { export const toggleSocketConnected = payload => async (dispatch) => {
dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload }) dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload })
} }
export const createUser = payload => async (dispatch) => {
dispatch({ type: 'CREATE_USER', payload })
}
export const clearActivities = () => async (dispatch) => {
dispatch({ type: 'CLEAR_ACTIVITIES' })
}

View File

@ -0,0 +1,19 @@
import { getSocket } from 'utils/socket'
import {
prepare as prepareMessage,
process as processMessage,
} from 'utils/message'
export const sendEncryptedMessage = payload => async (dispatch, getState) => {
const state = getState()
const msg = await prepareMessage(payload, state)
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload })
getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend)
}
export const receiveEncryptedMessage = payload => async (dispatch, getState) => {
const state = getState()
const message = await processMessage(payload, state)
// Pass current state to all RECEIVE_ENCRYPTED_MESSAGE reducers for convenience, since each may have different needs
dispatch({ type: `RECEIVE_ENCRYPTED_MESSAGE_${message.type}`, payload: { payload: message.payload, state } })
}

View File

@ -1,12 +0,0 @@
const methodMap = {
GET: '',
POST: 'CREATE_',
PUT: 'UPDATE_',
DELETE: 'DELETE_',
}
export const fetchStart = (name, method, resourceId, meta) => ({ type: `FETCH_${methodMap[method]}${name.toUpperCase()}_START`, payload: { resourceId, meta } })
export const fetchSuccess = (name, method, response) => ({ type: `FETCH_${methodMap[method]}${name.toUpperCase()}_SUCCESS`, payload: response })
export const fetchFailure = (name, method, response) => ({ type: `FETCH_${methodMap[method]}${name.toUpperCase()}_FAILURE`, payload: response })

View File

@ -1,4 +1,4 @@
export * from './fetch'
export * from './room'
export * from './app' export * from './app'
export * from './unencrypted_messages'
export * from './encrypted_messages'

View File

@ -1,104 +0,0 @@
import fetch from 'api'
import isEqual from 'lodash/isEqual'
import {
process as processMessage,
prepare as prepareMessage,
} from 'utils/message'
import { getSocket } from 'utils/socket'
export const receiveSocketMessage = payload => async (dispatch, getState) => {
const state = getState()
const message = await processMessage(payload, state)
// Pass current state to all HANDLE_SOCKET_MESSAGE reducers for convenience, since each may have different needs
dispatch({ type: `HANDLE_SOCKET_MESSAGE_${message.type}`, payload: { payload: message.payload, state } })
}
export const createUser = payload => async (dispatch) => {
dispatch({ type: 'CREATE_USER', payload })
}
export const sendUserEnter = payload => async () => {
getSocket().emit('USER_ENTER', {
publicKey: payload.publicKey,
})
}
export const receiveUserExit = payload => async (dispatch, getState) => {
const state = getState()
const exitingUser = state.room.members.find(m => !payload.map(p => JSON.stringify(p.publicKey)).includes(JSON.stringify(m.publicKey)))
if (!exitingUser) {
return;
}
const exitingUserId = exitingUser.id
const exitingUsername = exitingUser.username
dispatch({
type: 'USER_EXIT',
payload: {
members: payload,
id: exitingUserId,
username: exitingUsername,
},
})
}
export const receiveUserEnter = payload => async (dispatch) => {
dispatch({ type: 'USER_ENTER', payload })
}
export const onFileTransfer = payload => async (dispatch) => {
dispatch({ type: 'PREFLIGHT_FILE_TRANSFER', payload })
}
export const sendSocketMessage = payload => async (dispatch, getState) => {
const state = getState()
const msg = await prepareMessage(payload, state)
dispatch({ type: `SEND_SOCKET_MESSAGE_${msg.original.type}`, payload: msg.original.payload })
getSocket().emit('PAYLOAD', msg.toSend)
}
export const toggleLockRoom = () => async (dispatch, getState) => {
const state = getState()
getSocket().emit('TOGGLE_LOCK_ROOM', null, (res) => {
dispatch({
type: 'TOGGLE_LOCK_ROOM',
payload: {
locked: res.isLocked,
username: state.user.username,
sender: state.user.id,
},
})
})
}
export const receiveToggleLockRoom = payload => async (dispatch, getState) => {
const state = getState()
const lockedByUser = state.room.members.find(m => isEqual(m.publicKey, payload.publicKey))
const lockedByUsername = lockedByUser.username
const lockedByUserId = lockedByUser.id
dispatch({
type: 'RECEIVE_TOGGLE_LOCK_ROOM',
payload: {
username: lockedByUsername,
locked: payload.locked,
id: lockedByUserId,
},
})
}
export const clearActivities = () => async (dispatch) => {
dispatch({ type: 'CLEAR_ACTIVITIES' })
}
export const onConnected = payload => async (dispatch) => {
dispatch({ type: 'CONNECTED', payload })
}
export const sendUserDisconnect = () => async () => {
getSocket().emit('USER_DISCONNECT')
}

View File

@ -0,0 +1,80 @@
import { getSocket } from 'utils/socket'
const receiveUserEnter = (payload, dispatch) => {
dispatch({ type: 'USER_ENTER', payload })
}
const receiveToggleLockRoom = (payload, dispatch, getState) => {
const state = getState()
const lockedByUser = state.room.members.find(m => m.publicKey.n === payload.publicKey.n)
const lockedByUsername = lockedByUser.username
const lockedByUserId = lockedByUser.id
dispatch({
type: 'RECEIVE_TOGGLE_LOCK_ROOM',
payload: {
username: lockedByUsername,
locked: payload.locked,
id: lockedByUserId,
},
})
}
const receiveUserExit = (payload, dispatch, getState) => {
const state = getState()
const payloadPublicKeys = payload.map(member => member.publicKey.n);
const exitingUser = state.room.members.find(m => !payloadPublicKeys.includes(m.publicKey.n))
if (!exitingUser) {
return;
}
const exitingUserId = exitingUser.id
const exitingUsername = exitingUser.username
dispatch({
type: 'USER_EXIT',
payload: {
members: payload,
id: exitingUserId,
username: exitingUsername,
},
})
}
export const receiveUnencryptedMessage = (type, payload) => async (dispatch, getState) => {
switch(type) {
case 'USER_ENTER':
return receiveUserEnter(payload, dispatch);
case 'USER_EXIT':
return receiveUserExit(payload, dispatch, getState);
case 'TOGGLE_LOCK_ROOM':
return receiveToggleLockRoom(payload, dispatch, getState);
default:
return;
}
}
const sendToggleLockRoom = (dispatch, getState) => {
const state = getState()
getSocket().emit('TOGGLE_LOCK_ROOM', null, (res) => {
dispatch({
type: 'TOGGLE_LOCK_ROOM',
payload: {
locked: res.isLocked,
username: state.user.username,
sender: state.user.id,
},
})
})
}
export const sendUnencryptedMessage = (type, payload) => async (dispatch, getState) => {
switch(type) {
case 'TOGGLE_LOCK_ROOM':
return sendToggleLockRoom(dispatch, getState);
default:
return;
}
}

View File

@ -1,59 +0,0 @@
import {
fetchStart,
fetchSuccess,
fetchFailure,
} from 'actions'
import queryString from 'querystring'
import generateUrl from './generator'
export default (opts, dispatch, name, metaOpts = {}) => {
const method = opts.method || 'GET'
const resourceId = opts.resourceId
let url = generateUrl(opts.resourceName, resourceId)
const config = {
method,
headers: {},
type: 'cors',
}
if (opts.body) {
config.body = JSON.stringify(opts.body)
config.headers['Content-Type'] = 'application/json'
}
if (opts.query) {
url = `${url}?${queryString.stringify(opts.query)}`
}
return new Promise((resolve, reject) => {
const meta = { ...metaOpts, timestamp: Date.now() }
dispatch(fetchStart(name, method, resourceId, meta))
return window.fetch(url, config)
.then(async (response) => {
let json = {}
try {
json = await response.json()
} catch (e) {
throw new Error(e)
}
const dispatchOps = {
response,
json,
resourceId,
meta,
}
if (response.ok) {
dispatch(fetchSuccess(name, method, dispatchOps))
return resolve(dispatchOps)
}
dispatch(fetchFailure(name, method, dispatchOps))
return reject(dispatchOps)
})
})
}

View File

@ -71,7 +71,7 @@ class About extends Component {
<div className="input-group-append"> <div className="input-group-append">
<button <button
className="btn btn-secondary" className="btn btn-secondary"
type="button" type="submit"
> >
Submit Submit
</button> </button>

View File

@ -3,11 +3,11 @@ import { mount } from 'enzyme'
import toJson from 'enzyme-to-json' import toJson from 'enzyme-to-json'
import { Chat } from './index.js' import { Chat } from './index.js'
const sendSocketMessage = jest.fn() const sendEncryptedMessage = jest.fn()
test('Chat Component', () => { test('Chat Component', () => {
const component = mount( const component = mount(
<Chat scrollToBottom={() => {}} focusChat={false} userId="foo" username="user" showNotice={() => {}} clearActivities={() => {}} sendSocketMessage={sendSocketMessage} /> <Chat scrollToBottom={() => {}} focusChat={false} userId="foo" username="user" showNotice={() => {}} clearActivities={() => {}} sendEncryptedMessage={sendEncryptedMessage} />
) )
const componentJSON = toJson(component) const componentJSON = toJson(component)

View File

@ -4,8 +4,9 @@ import sanitizeHtml from 'sanitize-html'
import FileTransfer from 'components/FileTransfer' import FileTransfer from 'components/FileTransfer'
import { CornerDownRight } from 'react-feather' import { CornerDownRight } from 'react-feather'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { clearActivities, showNotice } from '../../actions' import { clearActivities, showNotice, sendEncryptedMessage } from '../../actions'
import { getSelectedText, hasTouchSupport } from '../../utils/dom' import { getSelectedText, hasTouchSupport } from '../../utils/dom'
// Disable for now // Disable for now
// import autosize from 'autosize' // import autosize from 'autosize'
@ -51,7 +52,7 @@ export class Chat extends Component {
}) })
} }
this.props.sendSocketMessage({ this.props.sendEncryptedMessage({
type: 'CHANGE_USERNAME', type: 'CHANGE_USERNAME',
payload: { payload: {
id: this.props.userId, id: this.props.userId,
@ -85,7 +86,7 @@ export class Chat extends Component {
return false return false
} }
this.props.sendSocketMessage({ this.props.sendEncryptedMessage({
type: 'USER_ACTION', type: 'USER_ACTION',
payload: { payload: {
action: actionMessage, action: actionMessage,
@ -216,8 +217,8 @@ export class Chat extends Component {
return return
} }
} else { } else {
this.props.sendSocketMessage({ this.props.sendEncryptedMessage({
type: 'SEND_MESSAGE', type: 'TEXT_MESSAGE',
payload: { payload: {
text: message, text: message,
timestamp: Date.now(), timestamp: Date.now(),
@ -256,7 +257,7 @@ export class Chat extends Component {
placeholder="Type here" placeholder="Type here"
onChange={this.handleInputChange.bind(this)} /> onChange={this.handleInputChange.bind(this)} />
<div className="input-controls"> <div className="input-controls">
<FileTransfer sendSocketMessage={this.props.sendSocketMessage} /> <FileTransfer sendEncryptedMessage={this.props.sendEncryptedMessage} />
{touchSupport && {touchSupport &&
<button onClick={this.handleSendClick.bind(this)} className={`icon is-right send btn btn-link ${this.canSend() ? 'active' : ''}`}> <button onClick={this.handleSendClick.bind(this)} className={`icon is-right send btn btn-link ${this.canSend() ? 'active' : ''}`}>
<CornerDownRight className={this.canSend() ? '' : 'disabled'} /> <CornerDownRight className={this.canSend() ? '' : 'disabled'} />
@ -269,7 +270,7 @@ export class Chat extends Component {
} }
Chat.propTypes = { Chat.propTypes = {
sendSocketMessage: PropTypes.func.isRequired, sendEncryptedMessage: PropTypes.func.isRequired,
showNotice: PropTypes.func.isRequired, showNotice: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
@ -286,6 +287,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = { const mapDispatchToProps = {
clearActivities, clearActivities,
showNotice, showNotice,
sendEncryptedMessage
} }
export default connect( export default connect(

View File

@ -81,7 +81,7 @@ export default class FileTransfer extends Component {
this.setState({ this.setState({
localFileQueue, localFileQueue,
}, async () => { }, async () => {
this.props.sendSocketMessage({ this.props.sendEncryptedMessage({
type: 'SEND_FILE', type: 'SEND_FILE',
payload: { payload: {
fileName: fileData.fileName, fileName: fileData.fileName,
@ -112,5 +112,5 @@ export default class FileTransfer extends Component {
} }
FileTransfer.propTypes = { FileTransfer.propTypes = {
sendSocketMessage: PropTypes.func.isRequired, sendEncryptedMessage: PropTypes.func.isRequired,
} }

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Crypto from 'utils/crypto' import Crypto from 'utils/crypto'
import { connect } from 'utils/socket' import { connect as connectSocket } from 'utils/socket'
import Nav from 'components/Nav' import Nav from 'components/Nav'
import shortId from 'shortid' import shortId from 'shortid'
import ChatInput from 'containers/Chat' import ChatInput from 'components/Chat'
import Connecting from 'components/Connecting' import Connecting from 'components/Connecting'
import Message from 'components/Message' import Message from 'components/Message'
import Username from 'components/Username' import Username from 'components/Username'
@ -21,6 +21,20 @@ import beepFile from 'audio/beep.mp3'
import Zoom from 'utils/ImageZoom' import Zoom from 'utils/ImageZoom'
import classNames from 'classnames' import classNames from 'classnames'
import { getObjectUrl } from 'utils/file' import { getObjectUrl } from 'utils/file'
import { connect } from 'react-redux'
import {
receiveEncryptedMessage,
createUser,
openModal,
closeModal,
setScrolledToBottom,
toggleWindowFocus,
toggleSoundEnabled,
toggleSocketConnected,
receiveUnencryptedMessage,
sendUnencryptedMessage,
sendEncryptedMessage
} from 'actions'
import styles from './styles.module.scss' import styles from './styles.module.scss'
@ -28,7 +42,7 @@ const crypto = new Crypto()
Modal.setAppElement('#root'); Modal.setAppElement('#root');
export default class Home extends Component { class Home extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -45,32 +59,22 @@ export default class Home extends Component {
const user = await this.createUser() const user = await this.createUser()
const socket = connect(roomId) const socket = connectSocket(roomId)
const disconnectEvents = [ this.socket = socket;
'disconnect',
]
disconnectEvents.forEach((evt) => { socket.on('disconnect', () => {
socket.on(evt, () => {
this.props.toggleSocketConnected(false) this.props.toggleSocketConnected(false)
}) })
})
const connectEvents = [ socket.on('connect', () => {
'connect',
]
connectEvents.forEach((evt) => {
socket.on(evt, () => {
this.initApp(user) this.initApp(user)
this.props.toggleSocketConnected(true) this.props.toggleSocketConnected(true)
}) })
})
socket.on('USER_ENTER', (payload) => { socket.on('USER_ENTER', (payload) => {
this.props.receiveUserEnter(payload) this.props.receiveUnencryptedMessage('USER_ENTER', payload)
this.props.sendSocketMessage({ this.props.sendEncryptedMessage({
type: 'ADD_USER', type: 'ADD_USER',
payload: { payload: {
username: this.props.username, username: this.props.username,
@ -82,45 +86,33 @@ export default class Home extends Component {
}) })
socket.on('USER_EXIT', (payload) => { socket.on('USER_EXIT', (payload) => {
this.props.receiveUserExit(payload) this.props.receiveUnencryptedMessage('USER_EXIT', payload)
}) })
socket.on('PAYLOAD', (payload) => { socket.on('ENCRYPTED_MESSAGE', (payload) => {
this.props.receiveSocketMessage(payload) this.props.receiveEncryptedMessage(payload)
}) })
socket.on('TOGGLE_LOCK_ROOM', (payload) => { socket.on('TOGGLE_LOCK_ROOM', (payload) => {
this.props.receiveToggleLockRoom(payload) this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload)
}) })
socket.on('CONNECTED', (payload) => {
this.props.onConnected(payload);
});
socket.on('ROOM_LOCKED', (payload) => { socket.on('ROOM_LOCKED', (payload) => {
this.props.openModal('Room Locked') this.props.openModal('Room Locked')
}); });
window.addEventListener('beforeunload', (evt) => { window.addEventListener('beforeunload', (evt) => {
this.props.sendUserDisconnect(); socket.emit('USER_DISCONNECT')
}); });
} }
componentDidMount() { componentDidMount() {
this.bindEvents() this.bindEvents()
if (this.props.joining) {
this.props.openModal('Connecting')
}
this.beep = window.Audio && new window.Audio(beepFile) this.beep = window.Audio && new window.Audio(beepFile)
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.joining && !nextProps.joining) {
this.props.closeModal()
}
Tinycon.setBubble(nextProps.faviconCount) Tinycon.setBubble(nextProps.faviconCount)
if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) { if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) {
@ -171,7 +163,7 @@ export default class Home extends Component {
getActivityComponent(activity) { getActivityComponent(activity) {
switch (activity.type) { switch (activity.type) {
case 'SEND_MESSAGE': case 'TEXT_MESSAGE':
return ( return (
<Message <Message
sender={activity.username} sender={activity.username}
@ -275,7 +267,7 @@ export default class Home extends Component {
} }
initApp(user) { initApp(user) {
this.props.sendUserEnter({ this.socket.emit('USER_ENTER', {
publicKey: user.publicKey, publicKey: user.publicKey,
}) })
} }
@ -349,7 +341,7 @@ export default class Home extends Component {
members={this.props.members} members={this.props.members}
roomId={this.props.roomId} roomId={this.props.roomId}
roomLocked={this.props.roomLocked} roomLocked={this.props.roomLocked}
toggleLockRoom={this.props.toggleLockRoom} toggleLockRoom={() => this.props.sendUnencryptedMessage('TOGGLE_LOCK_ROOM')}
openModal={this.props.openModal} openModal={this.props.openModal}
iAmOwner={this.props.iAmOwner} iAmOwner={this.props.iAmOwner}
userId={this.props.userId} userId={this.props.userId}
@ -411,11 +403,8 @@ Home.defaultProps = {
} }
Home.propTypes = { Home.propTypes = {
receiveSocketMessage: PropTypes.func.isRequired, receiveEncryptedMessage: PropTypes.func.isRequired,
sendSocketMessage: PropTypes.func.isRequired,
createUser: PropTypes.func.isRequired, createUser: PropTypes.func.isRequired,
receiveUserExit: PropTypes.func.isRequired,
receiveUserEnter: PropTypes.func.isRequired,
activities: PropTypes.array.isRequired, activities: PropTypes.array.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
publicKey: PropTypes.object.isRequired, publicKey: PropTypes.object.isRequired,
@ -423,23 +412,59 @@ Home.propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
roomLocked: PropTypes.bool.isRequired, roomLocked: PropTypes.bool.isRequired,
toggleLockRoom: PropTypes.func.isRequired,
receiveToggleLockRoom: PropTypes.func.isRequired,
modalComponent: PropTypes.string, modalComponent: PropTypes.string,
openModal: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
setScrolledToBottom: PropTypes.func.isRequired, setScrolledToBottom: PropTypes.func.isRequired,
scrolledToBottom: PropTypes.bool.isRequired, scrolledToBottom: PropTypes.bool.isRequired,
iAmOwner: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired,
sendUserEnter: PropTypes.func.isRequired,
sendUserDisconnect: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
joining: PropTypes.bool.isRequired,
toggleWindowFocus: PropTypes.func.isRequired, toggleWindowFocus: PropTypes.func.isRequired,
faviconCount: PropTypes.number.isRequired, faviconCount: PropTypes.number.isRequired,
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
toggleSocketConnected: PropTypes.func.isRequired, toggleSocketConnected: PropTypes.func.isRequired,
socketConnected: PropTypes.bool.isRequired, socketConnected: PropTypes.bool.isRequired,
onConnected: PropTypes.func.isRequired sendUnencryptedMessage: PropTypes.func.isRequired,
sendEncryptedMessage: PropTypes.func.isRequired
} }
const mapStateToProps = (state) => {
const me = state.room.members.find(m => m.id === state.user.id)
return {
activities: state.activities.items,
userId: state.user.id,
username: state.user.username,
publicKey: state.user.publicKey,
privateKey: state.user.privateKey,
members: state.room.members.filter(m => m.username && m.publicKey),
roomId: state.room.id,
roomLocked: state.room.isLocked,
modalComponent: state.app.modalComponent,
scrolledToBottom: state.app.scrolledToBottom,
iAmOwner: Boolean(me && me.isOwner),
faviconCount: state.app.unreadMessageCount,
soundIsEnabled: state.app.soundIsEnabled,
socketConnected: state.app.socketConnected,
}
}
const mapDispatchToProps = {
receiveEncryptedMessage,
createUser,
openModal,
closeModal,
setScrolledToBottom,
toggleWindowFocus,
toggleSoundEnabled,
toggleSocketConnected,
receiveUnencryptedMessage,
sendUnencryptedMessage,
sendEncryptedMessage
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home)

View File

@ -1,15 +0,0 @@
import { connect } from 'react-redux'
import { sendSocketMessage } from 'actions'
import ChatInput from 'components/Chat'
const mapStateToProps = () => ({
})
const mapDispatchToProps = {
sendSocketMessage,
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatInput)

View File

@ -1,67 +0,0 @@
import { connect } from 'react-redux'
import Home from 'components/Home'
import {
receiveSocketMessage,
sendSocketMessage,
createUser,
receiveUserExit,
receiveUserEnter,
toggleLockRoom,
receiveToggleLockRoom,
openModal,
closeModal,
setScrolledToBottom,
sendUserEnter,
toggleWindowFocus,
toggleSoundEnabled,
toggleSocketConnected,
onConnected,
sendUserDisconnect
} from 'actions'
const mapStateToProps = (state) => {
const me = state.room.members.find(m => m.id === state.user.id)
return {
activities: state.activities.items,
userId: state.user.id,
username: state.user.username,
publicKey: state.user.publicKey,
privateKey: state.user.privateKey,
members: state.room.members.filter(m => m.username && m.publicKey),
roomId: state.room.id,
roomLocked: state.room.isLocked,
modalComponent: state.app.modalComponent,
scrolledToBottom: state.app.scrolledToBottom,
iAmOwner: Boolean(me && me.isOwner),
joining: state.room.joining,
faviconCount: state.app.unreadMessageCount,
soundIsEnabled: state.app.soundIsEnabled,
socketConnected: state.app.socketConnected,
}
}
const mapDispatchToProps = {
receiveSocketMessage,
sendSocketMessage,
receiveUserExit,
receiveUserEnter,
createUser,
toggleLockRoom,
receiveToggleLockRoom,
openModal,
closeModal,
setScrolledToBottom,
sendUserEnter,
toggleWindowFocus,
toggleSoundEnabled,
toggleSocketConnected,
onConnected,
sendUserDisconnect
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home)

View File

@ -1,12 +1,5 @@
const initialState = { const initialState = {
items: [ items: [],
// {
// type: 'message | file | isTyping | usernameChange | slashCommand',
// data,
// username,
// timestamp
// }
],
} }
const activities = (state = initialState, action) => { const activities = (state = initialState, action) => {
@ -16,7 +9,7 @@ const activities = (state = initialState, action) => {
...state, ...state,
items: [], items: [],
} }
case 'SEND_SOCKET_MESSAGE_SLASH_COMMAND': case 'SEND_ENCRYPTED_MESSAGE_SLASH_COMMAND':
return { return {
...state, ...state,
items: [ items: [
@ -27,7 +20,7 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'SEND_SOCKET_MESSAGE_FILE_TRANSFER': case 'SEND_ENCRYPTED_MESSAGE_FILE_TRANSFER':
return { return {
...state, ...state,
items: [ items: [
@ -38,29 +31,29 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'SEND_SOCKET_MESSAGE_SEND_MESSAGE': case 'SEND_ENCRYPTED_MESSAGE_TEXT_MESSAGE':
return { return {
...state, ...state,
items: [ items: [
...state.items, ...state.items,
{ {
...action.payload, ...action.payload,
type: 'SEND_MESSAGE', type: 'TEXT_MESSAGE',
}, },
], ],
} }
case 'HANDLE_SOCKET_MESSAGE_SEND_MESSAGE': case 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE':
return { return {
...state, ...state,
items: [ items: [
...state.items, ...state.items,
{ {
...action.payload.payload, ...action.payload.payload,
type: 'SEND_MESSAGE', type: 'TEXT_MESSAGE',
}, },
], ],
} }
case 'SEND_SOCKET_MESSAGE_SEND_FILE': case 'SEND_ENCRYPTED_MESSAGE_SEND_FILE':
return { return {
...state, ...state,
items: [ items: [
@ -71,7 +64,7 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'HANDLE_SOCKET_MESSAGE_SEND_FILE': case 'RECEIVE_ENCRYPTED_MESSAGE_SEND_FILE':
return { return {
...state, ...state,
items: [ items: [
@ -82,7 +75,7 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'HANDLE_SOCKET_MESSAGE_ADD_USER': case 'RECEIVE_ENCRYPTED_MESSAGE_ADD_USER':
const newUserId = action.payload.payload.id const newUserId = action.payload.payload.id
const haveUser = action.payload.state.room.members.find(m => m.id === newUserId) const haveUser = action.payload.state.room.members.find(m => m.id === newUserId)
@ -90,10 +83,6 @@ const activities = (state = initialState, action) => {
return state return state
} }
if (action.payload.state.room.joining) {
return state
}
return { return {
...state, ...state,
items: [ items: [
@ -159,7 +148,7 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'SEND_SOCKET_MESSAGE_CHANGE_USERNAME': case 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME':
return { return {
...state, ...state,
items: [ items: [
@ -179,7 +168,7 @@ const activities = (state = initialState, action) => {
return item return item
}), }),
} }
case 'HANDLE_SOCKET_MESSAGE_CHANGE_USERNAME': case 'RECEIVE_ENCRYPTED_MESSAGE_CHANGE_USERNAME':
return { return {
...state, ...state,
items: [ items: [
@ -190,7 +179,7 @@ const activities = (state = initialState, action) => {
newUsername: action.payload.payload.newUsername, newUsername: action.payload.payload.newUsername,
}, },
].map((item) => { ].map((item) => {
if (['SEND_MESSAGE', 'USER_ACTION'].includes(item.type) && item.sender === action.payload.payload.sender) { if (['TEXT_MESSAGE', 'USER_ACTION'].includes(item.type) && item.sender === action.payload.payload.sender) {
return { return {
...item, ...item,
username: action.payload.payload.newUsername, username: action.payload.payload.newUsername,
@ -199,7 +188,7 @@ const activities = (state = initialState, action) => {
return item return item
}), }),
} }
case 'SEND_SOCKET_MESSAGE_USER_ACTION': case 'SEND_ENCRYPTED_MESSAGE_USER_ACTION':
return { return {
...state, ...state,
items: [ items: [
@ -210,7 +199,7 @@ const activities = (state = initialState, action) => {
}, },
], ],
} }
case 'HANDLE_SOCKET_MESSAGE_USER_ACTION': case 'RECEIVE_ENCRYPTED_MESSAGE_USER_ACTION':
return { return {
...state, ...state,
items: [ items: [

View File

@ -30,7 +30,7 @@ const app = (state = initialState, action) => {
windowIsFocused: action.payload, windowIsFocused: action.payload,
unreadMessageCount: 0, unreadMessageCount: 0,
} }
case 'HANDLE_SOCKET_MESSAGE_SEND_MESSAGE': case 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE':
return { return {
...state, ...state,
unreadMessageCount: state.windowIsFocused ? 0 : state.unreadMessageCount + 1, unreadMessageCount: state.windowIsFocused ? 0 : state.unreadMessageCount + 1,

View File

@ -9,43 +9,22 @@ const initialState = {
], ],
id: '', id: '',
isLocked: false, isLocked: false,
joining: true,
size: 0,
} }
const room = (state = initialState, action) => { const room = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'CONNECTED':
const size = action.payload.users ? action.payload.users.length : 1;
return {
...state,
id: action.payload.id,
isLocked: Boolean(action.payload.isLocked),
size,
joining: false
}
case 'USER_EXIT': case 'USER_EXIT':
const memberPubKeys = action.payload.members.map(m => JSON.stringify(m.publicKey)) const memberPubKeys = action.payload.members.map(m => m.publicKey.n)
return { return {
...state, ...state,
members: state.members members: state.members
.filter(m => memberPubKeys.includes(JSON.stringify(m.publicKey))) .filter(member => memberPubKeys.includes(member.publicKey.n))
.map((m) => {
const payloadMember = action.payload.members.find(member => _.isEqual(m.publicKey, member.publicKey))
return {
...m,
...payloadMember,
} }
}), case 'RECEIVE_ENCRYPTED_MESSAGE_ADD_USER':
}
case 'HANDLE_SOCKET_MESSAGE_ADD_USER':
const membersWithId = state.members.filter(m => m.id)
const joining = false
return { return {
...state, ...state,
members: state.members.map((member) => { members: state.members.map((member) => {
if (_.isEqual(member.publicKey, action.payload.payload.publicKey)) { if (member.publicKey.n === action.payload.payload.publicKey.n) {
return { return {
...member, ...member,
username: action.payload.payload.username, username: action.payload.payload.username,
@ -55,7 +34,6 @@ const room = (state = initialState, action) => {
} }
return member return member
}), }),
joining,
} }
case 'CREATE_USER': case 'CREATE_USER':
return { return {
@ -70,17 +48,12 @@ const room = (state = initialState, action) => {
], ],
} }
case 'USER_ENTER': case 'USER_ENTER':
/* const members = _.uniqBy(action.payload.users, member => member.publicKey.n);
In this payload the server sends all users' public keys. Normally the server
will have all the users the client does, but in some cases - such as when
new users join before this client has registered with the server (this can
happen when lots of users join in quick succession) - the client
will receive a USER_ENTER event that doesn't contain itself. In that case we
want to prepend "me" to the members payload
*/
const members = _.uniqBy(action.payload, member => member.publicKey.n);
return { return {
...state, ...state,
id: action.payload.id,
isLocked: Boolean(action.payload.isLocked),
members: members.reduce((acc, user) => { members: members.reduce((acc, user) => {
const exists = state.members.find(m => m.publicKey.n === user.publicKey.n) const exists = state.members.find(m => m.publicKey.n === user.publicKey.n)
if (exists) { if (exists) {
@ -112,7 +85,7 @@ const room = (state = initialState, action) => {
...state, ...state,
isLocked: action.payload.locked, isLocked: action.payload.locked,
} }
case 'SEND_SOCKET_MESSAGE_CHANGE_USERNAME': case 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME':
const newUsername = action.payload.newUsername const newUsername = action.payload.newUsername
const userId = action.payload.id const userId = action.payload.id
return { return {
@ -125,7 +98,7 @@ const room = (state = initialState, action) => {
} : member } : member
)), )),
} }
case 'HANDLE_SOCKET_MESSAGE_CHANGE_USERNAME': case 'RECEIVE_ENCRYPTED_MESSAGE_CHANGE_USERNAME':
const newUsername2 = action.payload.payload.newUsername const newUsername2 = action.payload.payload.newUsername
const userId2 = action.payload.payload.id const userId2 = action.payload.payload.id
return { return {

View File

@ -12,7 +12,7 @@ const user = (state = initialState, action) => {
...action.payload, ...action.payload,
id: action.payload.publicKey.n, id: action.payload.publicKey.n,
} }
case 'SEND_SOCKET_MESSAGE_CHANGE_USERNAME': case 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME':
return { return {
...state, ...state,
username: action.payload.newUsername, username: action.payload.newUsername,

View File

@ -8,7 +8,7 @@ import { Provider } from 'react-redux'
import configureStore from 'store' import configureStore from 'store'
import { BrowserRouter, Route, Switch } from 'react-router-dom' import { BrowserRouter, Route, Switch } from 'react-router-dom'
import shortId from 'shortid' import shortId from 'shortid'
import Home from 'containers/Home' import Home from 'components/Home'
import { hasTouchSupport } from './utils/dom' import { hasTouchSupport } from './utils/dom'
const store = configureStore() const store = configureStore()

View File

@ -142,7 +142,7 @@ textarea
.activity-item .activity-item
margin-bottom: 15px margin-bottom: 15px
&.SEND_MESSAGE &.TEXT_MESSAGE
.chat-meta .chat-meta
font-size: 13px font-size: 13px
.timestamp .timestamp

View File

@ -8513,15 +8513,6 @@ qs@6.5.2, qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
query-string@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.5.0.tgz#2e1a70125af01f6f04573692d02c09302a1d8bfc"
integrity sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
querystring-es3@^0.2.0: querystring-es3@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -9751,11 +9742,6 @@ spdy@^4.0.0:
select-hose "^2.0.0" select-hose "^2.0.0"
spdy-transport "^3.0.0" spdy-transport "^3.0.0"
split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
split-string@^3.0.1, split-string@^3.0.2: split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@ -9870,11 +9856,6 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-length@^2.0.0: string-length@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"

View File

@ -103,7 +103,7 @@ const protocol = (process.env.PROTOCOL || 'http') === 'http' ? http : https;
const server = protocol.createServer(app.callback()); const server = protocol.createServer(app.callback());
const io = Io(server, { const io = Io(server, {
pingInterval: 5000, pingInterval: 5000,
pingTimeout: 3000 pingTimeout: 5000
}); });
io.adapter(socketRedis(process.env.REDIS_URL)); io.adapter(socketRedis(process.env.REDIS_URL));

View File

@ -21,24 +21,9 @@ export default class Socket {
async init(opts) { async init(opts) {
const { roomId, socket, room } = opts const { roomId, socket, room } = opts
await this.joinRoom(roomId, socket.id) await this.joinRoom(roomId, socket.id)
this.sendRoomInfo();
this.handleSocket(socket) this.handleSocket(socket)
} }
sendRoomInfo() {
let room;
if (_.isEmpty(this.room)) {
room = {
id: this.roomIdOriginal,
users: [],
isLocked: false,
}
} else {
room = this.room;
}
this.socket.emit('CONNECTED', room);
}
sendRoomLocked() { sendRoomLocked() {
this.socket.emit('ROOM_LOCKED'); this.socket.emit('ROOM_LOCKED');
} }
@ -75,11 +60,11 @@ export default class Socket {
} }
async handleSocket(socket) { async handleSocket(socket) {
socket.on('PAYLOAD', (payload) => { socket.on('ENCRYPTED_MESSAGE', (payload) => {
socket.to(this._roomId).emit('PAYLOAD', payload); socket.to(this._roomId).emit('ENCRYPTED_MESSAGE', payload);
}); });
socket.on('USER_ENTER', async payload => { socket.on('USER_ENTER', async (payload) => {
let room = await this.fetchRoom() let room = await this.fetchRoom()
if (_.isEmpty(room)) { if (_.isEmpty(room)) {
room = { room = {
@ -99,10 +84,11 @@ export default class Socket {
}] }]
} }
await this.saveRoom(newRoom) await this.saveRoom(newRoom)
getIO().to(this._roomId).emit('USER_ENTER', newRoom.users.map(u => ({
publicKey: u.publicKey, getIO().to(this._roomId).emit('USER_ENTER', {
isOwner: u.isOwner, ...newRoom,
}))); id: this.roomIdOriginal
});
}) })
socket.on('TOGGLE_LOCK_ROOM', async (data, callback) => { socket.on('TOGGLE_LOCK_ROOM', async (data, callback) => {