diff --git a/client/src/actions/room.js b/client/src/actions/room.js index 37b9eff..83b0d96 100644 --- a/client/src/actions/room.js +++ b/client/src/actions/room.js @@ -4,15 +4,7 @@ import { process as processMessage, prepare as prepareMessage, } from 'utils/message' -import { getIO } from 'utils/socket' - -export const createRoom = id => async dispatch => fetch({ - resourceName: 'handshake', - method: 'POST', - body: { - roomId: id, - }, -}, dispatch, 'handshake') +import { getSocket } from 'utils/socket' export const receiveSocketMessage = payload => async (dispatch, getState) => { const state = getState() @@ -26,7 +18,7 @@ export const createUser = payload => async (dispatch) => { } export const sendUserEnter = payload => async () => { - getIO().emit('USER_ENTER', { + getSocket().emit('USER_ENTER', { publicKey: payload.publicKey, }) } @@ -34,6 +26,11 @@ export const sendUserEnter = payload => async () => { 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 @@ -59,12 +56,12 @@ 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 }) - getIO().emit('PAYLOAD', msg.toSend) + getSocket().emit('PAYLOAD', msg.toSend) } export const toggleLockRoom = () => async (dispatch, getState) => { const state = getState() - getIO().emit('TOGGLE_LOCK_ROOM', null, (res) => { + getSocket().emit('TOGGLE_LOCK_ROOM', null, (res) => { dispatch({ type: 'TOGGLE_LOCK_ROOM', payload: { @@ -96,3 +93,12 @@ export const receiveToggleLockRoom = payload => async (dispatch, getState) => { 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') +} + diff --git a/client/src/components/Home/index.js b/client/src/components/Home/index.js index bcb498d..1cd145d 100644 --- a/client/src/components/Home/index.js +++ b/client/src/components/Home/index.js @@ -43,56 +43,32 @@ export default class Home extends Component { async componentWillMount() { const roomId = encodeURI(this.props.match.params.roomId) - const res = await this.props.createRoom(roomId) - - if (res.json.isLocked) { - this.props.openModal('Room Locked') - return - } - - if (res.json.size === 1) { - this.props.openModal('Welcome') - } - const user = await this.createUser() - const io = connect(roomId) + const socket = connect(roomId) const disconnectEvents = [ - 'reconnect_failed', - 'connect_timeout', - 'connect_error', 'disconnect', - 'reconnect', - 'reconnect_error', - 'reconnecting', - 'reconnect_attempt', ] disconnectEvents.forEach((evt) => { - io.on(evt, () => { + socket.on(evt, () => { this.props.toggleSocketConnected(false) }) }) const connectEvents = [ 'connect', - 'reconnect', ] connectEvents.forEach((evt) => { - io.on(evt, () => { - if (evt === 'connect') { - if (!this.hasConnected) { - this.initApp(user) - this.hasConnected = true - } - } + socket.on(evt, () => { + this.initApp(user) this.props.toggleSocketConnected(true) }) }) - io.on('USER_ENTER', (payload) => { + socket.on('USER_ENTER', (payload) => { this.props.receiveUserEnter(payload) this.props.sendSocketMessage({ type: 'ADD_USER', @@ -105,17 +81,29 @@ export default class Home extends Component { }) }) - io.on('USER_EXIT', (payload) => { + socket.on('USER_EXIT', (payload) => { this.props.receiveUserExit(payload) }) - io.on('PAYLOAD', (payload) => { + socket.on('PAYLOAD', (payload) => { this.props.receiveSocketMessage(payload) }) - io.on('TOGGLE_LOCK_ROOM', (payload) => { + socket.on('TOGGLE_LOCK_ROOM', (payload) => { this.props.receiveToggleLockRoom(payload) }) + + socket.on('CONNECTED', (payload) => { + this.props.onConnected(payload); + }); + + socket.on('ROOM_LOCKED', (payload) => { + this.props.openModal('Room Locked') + }); + + window.addEventListener('beforeunload', (evt) => { + this.props.sendUserDisconnect(); + }); } componentDidMount() { @@ -135,7 +123,7 @@ export default class Home extends Component { Tinycon.setBubble(nextProps.faviconCount) - if (nextProps.faviconCount !== 0 && this.props.soundIsEnabled) { + if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) { this.beep.play() } } @@ -423,7 +411,6 @@ Home.defaultProps = { } Home.propTypes = { - createRoom: PropTypes.func.isRequired, receiveSocketMessage: PropTypes.func.isRequired, sendSocketMessage: PropTypes.func.isRequired, createUser: PropTypes.func.isRequired, @@ -445,6 +432,7 @@ Home.propTypes = { scrolledToBottom: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired, sendUserEnter: PropTypes.func.isRequired, + sendUserDisconnect: PropTypes.func.isRequired, userId: PropTypes.string.isRequired, joining: PropTypes.bool.isRequired, toggleWindowFocus: PropTypes.func.isRequired, @@ -453,4 +441,5 @@ Home.propTypes = { toggleSoundEnabled: PropTypes.func.isRequired, toggleSocketConnected: PropTypes.func.isRequired, socketConnected: PropTypes.bool.isRequired, + onConnected: PropTypes.func.isRequired } diff --git a/client/src/containers/Home/index.js b/client/src/containers/Home/index.js index ed65e95..3d426a3 100644 --- a/client/src/containers/Home/index.js +++ b/client/src/containers/Home/index.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux' import Home from 'components/Home' import { - createRoom, receiveSocketMessage, sendSocketMessage, createUser, @@ -16,6 +15,8 @@ import { toggleWindowFocus, toggleSoundEnabled, toggleSocketConnected, + onConnected, + sendUserDisconnect } from 'actions' const mapStateToProps = (state) => { @@ -41,7 +42,6 @@ const mapStateToProps = (state) => { } const mapDispatchToProps = { - createRoom, receiveSocketMessage, sendSocketMessage, receiveUserExit, @@ -56,6 +56,8 @@ const mapDispatchToProps = { toggleWindowFocus, toggleSoundEnabled, toggleSocketConnected, + onConnected, + sendUserDisconnect } export default connect( diff --git a/client/src/reducers/activities.js b/client/src/reducers/activities.js index d8ae231..35a1bc3 100644 --- a/client/src/reducers/activities.js +++ b/client/src/reducers/activities.js @@ -90,13 +90,6 @@ const activities = (state = initialState, action) => { return state } - // Duplicate "user entered" can happen when >2 users join - // in quick succession - const alreadyEntered = state.items.find(act => act.type === 'USER_ENTER' && act.userId === newUserId) - if (alreadyEntered) { - return state - } - if (action.payload.state.room.joining) { return state } diff --git a/client/src/reducers/app.js b/client/src/reducers/app.js index 7787621..265d26c 100644 --- a/client/src/reducers/app.js +++ b/client/src/reducers/app.js @@ -9,8 +9,6 @@ const initialState = { const app = (state = initialState, action) => { switch (action.type) { - case 'FETCH_CREATE_HANDSHAKE_SUCCESS': - return state; case 'OPEN_MODAL': return { ...state, diff --git a/client/src/reducers/room.js b/client/src/reducers/room.js index 81f4321..6f3a396 100644 --- a/client/src/reducers/room.js +++ b/client/src/reducers/room.js @@ -15,15 +15,14 @@ const initialState = { const room = (state = initialState, action) => { switch (action.type) { - case 'FETCH_CREATE_HANDSHAKE_SUCCESS': - const isLocked = action.payload.json.isLocked - // Handle "room is locked" message for new members here + case 'CONNECTED': + const size = action.payload.users ? action.payload.users.length : 1; return { ...state, - id: action.payload.json.id, - isLocked, - size: action.payload.json.size, - joining: !(action.payload.json.size === 1), + id: action.payload.id, + isLocked: Boolean(action.payload.isLocked), + size, + joining: false } case 'USER_EXIT': const memberPubKeys = action.payload.members.map(m => JSON.stringify(m.publicKey)) @@ -41,7 +40,7 @@ const room = (state = initialState, action) => { } case 'HANDLE_SOCKET_MESSAGE_ADD_USER': const membersWithId = state.members.filter(m => m.id) - const joining = state.joining ? membersWithId.length + 1 < state.size : false + const joining = false return { ...state, @@ -79,24 +78,29 @@ const room = (state = initialState, action) => { will receive a USER_ENTER event that doesn't contain itself. In that case we want to prepend "me" to the members payload */ - const diff = _.differenceBy(state.members, action.payload, m => m.publicKey.n) - const members = diff.length ? state.members.concat(action.payload) : action.payload + const members = _.uniqBy(action.payload, member => member.publicKey.n); return { ...state, - members: members.map((user) => { - const exists = state.members.find(m => _.isEqual(m.publicKey, user.publicKey)) + members: members.reduce((acc, user) => { + const exists = state.members.find(m => m.publicKey.n === user.publicKey.n) if (exists) { - return { - ...user, - ...exists, + return [ + ...acc, + { + ...user, + ...exists, + } + ] + } + return [ + ...acc, + { + publicKey: user.publicKey, + isOwner: user.isOwner, + id: user.id, } - } - return { - publicKey: user.publicKey, - isOwner: user.isOwner, - id: user.id, - } - }), + ] + }, []), } case 'TOGGLE_LOCK_ROOM': return { diff --git a/client/src/utils/socket.js b/client/src/utils/socket.js index f161c45..cfc028d 100644 --- a/client/src/utils/socket.js +++ b/client/src/utils/socket.js @@ -13,4 +13,4 @@ export const connect = (roomId) => { return socket } -export const getIO = () => socket +export const getSocket = () => socket diff --git a/server/src/index.js b/server/src/index.js index c8eeffe..9df77e5 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -42,25 +42,6 @@ if ((siteURL || env === 'development') && !isReviewApp) { })); } -router.post('/handshake', koaBody, async (ctx) => { - const { body } = ctx.request; - const { roomId } = body; - - const roomIdHash = getRoomIdHash(roomId) - - let roomExists = await redis.hgetAsync('rooms', roomIdHash) - if (roomExists) { - roomExists = JSON.parse(roomExists) - } - - ctx.body = { - id: roomId, - ready: true, - isLocked: Boolean(roomExists && roomExists.isLocked), - size: ((roomExists && roomExists.users.length) || 0) + 1, - }; -}); - router.post('/abuse/:roomId', koaBody, async (ctx) => { let { roomId } = ctx.params; @@ -120,7 +101,10 @@ if (clientDistDirectory) { const protocol = (process.env.PROTOCOL || 'http') === 'http' ? http : https; const server = protocol.createServer(app.callback()); -const io = Io(server); +const io = Io(server, { + pingInterval: 5000, + pingTimeout: 3000 +}); io.adapter(socketRedis(process.env.REDIS_URL)); const roomHashSecret = process.env.ROOM_HASH_SECRET; @@ -151,6 +135,7 @@ io.on('connection', async (socket) => { room = JSON.parse(room || '{}') new Socket({ + roomIdOriginal: roomId, roomId: roomIdHash, socket, room, diff --git a/server/src/socket.js b/server/src/socket.js index a75df55..4fc9c3c 100644 --- a/server/src/socket.js +++ b/server/src/socket.js @@ -4,10 +4,14 @@ import { getIO, getRedis } from './index' export default class Socket { constructor(opts) { - const { roomId, socket, room } = opts + const { roomId, socket, room, roomIdOriginal } = opts this._roomId = roomId + this.socket = socket; + this.roomIdOriginal = roomIdOriginal; + this.room = room; if (room.isLocked) { + this.sendRoomLocked(); return } @@ -17,9 +21,28 @@ export default class Socket { async init(opts) { const { roomId, socket, room } = opts await this.joinRoom(roomId, socket.id) + this.sendRoomInfo(); 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() { + this.socket.emit('ROOM_LOCKED'); + } + async saveRoom(room) { const json = { ...room, @@ -109,6 +132,8 @@ export default class Socket { }); socket.on('disconnect', () => this.handleDisconnect(socket)); + + socket.on('USER_DISCONNECT', () => this.handleDisconnect(socket)); } async handleDisconnect(socket) { @@ -129,6 +154,8 @@ export default class Socket { if (newRoom.users && newRoom.users.length === 0) { await this.destroyRoom() } + + socket.disconnect(true); } }