| @ -1,7 +0,0 @@ | |||||
| import { LOGIN_GET_STATUS } from "../constants/ActionTypes"; | |||||
| export default { | |||||
| getStatus: () => ({ | |||||
| type: LOGIN_GET_STATUS, | |||||
| }), | |||||
| }; | |||||
| @ -1,48 +0,0 @@ | |||||
| import { | |||||
| ROOM_GET_LIST, | |||||
| ROOM_JOIN, | |||||
| ROOM_LEAVE, | |||||
| ROOM_MESSAGE, | |||||
| ROOM_SELECT, | |||||
| ROOM_SHOW_USERS, | |||||
| ROOM_HIDE_USERS, | |||||
| } from "../constants/ActionTypes"; | |||||
| export default { | |||||
| getList: () => ({ | |||||
| type: ROOM_GET_LIST, | |||||
| }), | |||||
| join: (room_name) => ({ | |||||
| type: ROOM_JOIN, | |||||
| payload: room_name, | |||||
| }), | |||||
| leave: (room_name) => ({ | |||||
| type: ROOM_LEAVE, | |||||
| payload: room_name, | |||||
| }), | |||||
| select: (room_name) => ({ | |||||
| type: ROOM_SELECT, | |||||
| payload: room_name, | |||||
| }), | |||||
| sendMessage: (room_name, message) => ({ | |||||
| type: ROOM_MESSAGE, | |||||
| payload: { | |||||
| room_name, | |||||
| message, | |||||
| }, | |||||
| }), | |||||
| showUsers: (room_name) => ({ | |||||
| type: ROOM_SHOW_USERS, | |||||
| payload: room_name, | |||||
| }), | |||||
| hideUsers: (room_name) => ({ | |||||
| type: ROOM_HIDE_USERS, | |||||
| payload: room_name, | |||||
| }), | |||||
| }; | |||||
| @ -1,29 +0,0 @@ | |||||
| import { | |||||
| SOCKET_RECEIVE_MESSAGE, | |||||
| SOCKET_SEND_MESSAGE, | |||||
| SOCKET_SET_CLOSED, | |||||
| SOCKET_SET_CLOSING, | |||||
| SOCKET_SET_ERROR, | |||||
| SOCKET_SET_OPEN, | |||||
| SOCKET_SET_OPENING, | |||||
| } from "../constants/ActionTypes"; | |||||
| export default { | |||||
| open: (url, handlers) => ({ | |||||
| type: SOCKET_SET_OPENING, | |||||
| payload: { | |||||
| url, | |||||
| onclose: handlers.onclose, | |||||
| onerror: handlers.onerror, | |||||
| onopen: handlers.onopen, | |||||
| onmessage: handlers.onmessage, | |||||
| }, | |||||
| }), | |||||
| close: () => ({ type: SOCKET_SET_CLOSING }), | |||||
| send: (message) => ({ | |||||
| type: SOCKET_SEND_MESSAGE, | |||||
| payload: message, | |||||
| }), | |||||
| }; | |||||
| @ -1,36 +0,0 @@ | |||||
| import { | |||||
| SOCKET_SET_CLOSED, | |||||
| SOCKET_SET_ERROR, | |||||
| SOCKET_SET_OPEN, | |||||
| SOCKET_RECEIVE_MESSAGE, | |||||
| } from "../constants/ActionTypes"; | |||||
| export default { | |||||
| onclose: (event) => ({ | |||||
| type: SOCKET_SET_CLOSED, | |||||
| payload: event.code, | |||||
| }), | |||||
| onerror: (event) => ({ type: SOCKET_SET_ERROR }), | |||||
| onopen: (event) => ({ type: SOCKET_SET_OPEN }), | |||||
| onmessage: (event) => { | |||||
| console.log(`Received message: ${event.data}`); | |||||
| const action = { type: SOCKET_RECEIVE_MESSAGE }; | |||||
| try { | |||||
| const { | |||||
| variant, | |||||
| fields: [data], | |||||
| } = JSON.parse(event.data); | |||||
| if (typeof variant === "undefined") { | |||||
| throw new Error('Missing "variant" field in control response'); | |||||
| } | |||||
| action.payload = { variant, data }; | |||||
| } catch (err) { | |||||
| action.error = true; | |||||
| action.payload = err; | |||||
| } | |||||
| return action; | |||||
| }, | |||||
| }; | |||||
| @ -1,9 +0,0 @@ | |||||
| import { USER_GET_LIST } from "../constants/ActionTypes"; | |||||
| const UserActions = { | |||||
| getList: () => ({ | |||||
| type: USER_GET_LIST, | |||||
| }), | |||||
| }; | |||||
| export default UserActions; | |||||
| @ -1,23 +0,0 @@ | |||||
| // Socket actions | |||||
| export const SOCKET_SET_OPEN = "SOCKET_SET_OPEN"; | |||||
| export const SOCKET_SET_OPENING = "SOCKET_SET_OPENING"; | |||||
| export const SOCKET_SET_CLOSED = "SOCKET_SET_CLOSED"; | |||||
| export const SOCKET_SET_CLOSING = "SOCKET_SET_CLOSING"; | |||||
| export const SOCKET_SET_ERROR = "SOCKET_SET_ERROR"; | |||||
| export const SOCKET_RECEIVE_MESSAGE = "SOCKET_RECEIVE_MESSAGE"; | |||||
| export const SOCKET_SEND_MESSAGE = "SOCKET_SEND_MESSAGE"; | |||||
| // Login actions | |||||
| export const LOGIN_GET_STATUS = "LOGIN_GET_STATUS"; | |||||
| // Room actions | |||||
| export const ROOM_GET_LIST = "ROOM_GET_LIST"; | |||||
| export const ROOM_SELECT = "ROOM_SELECT"; | |||||
| export const ROOM_JOIN = "ROOM_JOIN"; | |||||
| export const ROOM_LEAVE = "ROOM_LEAVE"; | |||||
| export const ROOM_MESSAGE = "ROOM_MESSAGE"; | |||||
| export const ROOM_SHOW_USERS = "ROOM_SHOW_USERS"; | |||||
| export const ROOM_HIDE_USERS = "ROOM_HIDE_USERS"; | |||||
| // User actions | |||||
| export const USER_GET_LIST = "USER_GET_LIST"; | |||||
| @ -1,5 +0,0 @@ | |||||
| export const LOGIN_STATUS_UNKNOWN = Symbol("LOGIN_STATUS_UNKNOWN"); | |||||
| export const LOGIN_STATUS_GETTING = Symbol("LOGIN_STATUS_GETTING"); | |||||
| export const LOGIN_STATUS_PENDING = Symbol("LOGIN_STATUS_PENDING"); | |||||
| export const LOGIN_STATUS_SUCCESS = Symbol("LOGIN_STATUS_SUCCESS"); | |||||
| export const LOGIN_STATUS_FAILURE = Symbol("LOGIN_STATUS_FAILURE"); | |||||
| @ -1,4 +0,0 @@ | |||||
| export const STATE_OPENING = 0; | |||||
| export const STATE_OPEN = 1; | |||||
| export const STATE_CLOSING = 2; | |||||
| export const STATE_CLOSED = 3; | |||||
| @ -1,17 +0,0 @@ | |||||
| import { combineReducers } from "redux"; | |||||
| import { reducer as form } from "redux-form"; | |||||
| import login from "./login"; | |||||
| import rooms from "./rooms"; | |||||
| import socket from "./socket"; | |||||
| import users from "./users"; | |||||
| const rootReducer = combineReducers({ | |||||
| login, | |||||
| rooms, | |||||
| socket, | |||||
| users, | |||||
| form, | |||||
| }); | |||||
| export default rootReducer; | |||||
| @ -1,75 +0,0 @@ | |||||
| import Immutable from "immutable"; | |||||
| import { | |||||
| LOGIN_GET_STATUS, | |||||
| SOCKET_RECEIVE_MESSAGE, | |||||
| } from "../constants/ActionTypes"; | |||||
| import { | |||||
| LOGIN_STATUS_UNKNOWN, | |||||
| LOGIN_STATUS_GETTING, | |||||
| LOGIN_STATUS_PENDING, | |||||
| LOGIN_STATUS_SUCCESS, | |||||
| LOGIN_STATUS_FAILURE, | |||||
| } from "../constants/login"; | |||||
| const LoginRecord = Immutable.Record({ | |||||
| status: LOGIN_STATUS_UNKNOWN, | |||||
| username: undefined, | |||||
| motd: undefined, | |||||
| reason: undefined, | |||||
| }); | |||||
| const initialState = new LoginRecord(); | |||||
| const reduceReceiveMessage = (state, message) => { | |||||
| const { variant, data } = message; | |||||
| if (variant !== "LoginStatusResponse") { | |||||
| return state; | |||||
| } | |||||
| switch (data.variant) { | |||||
| case "Pending": { | |||||
| // sub-block required otherwise const username declarations clash | |||||
| const [username] = data.fields; | |||||
| return state | |||||
| .set("status", LOGIN_STATUS_PENDING) | |||||
| .set("username", username); | |||||
| } | |||||
| case "Success": { | |||||
| // sub-block required otherwise const username declarations clash | |||||
| const [username, motd] = data.fields; | |||||
| return state | |||||
| .set("status", LOGIN_STATUS_SUCCESS) | |||||
| .set("username", username) | |||||
| .set("motd", motd); | |||||
| } | |||||
| case "Failure": { | |||||
| // sub-block required otherwise const username declarations clash | |||||
| const [username, reason] = data.fields; | |||||
| return state | |||||
| .set("status", LOGIN_STATUS_FAILURE) | |||||
| .set("username", username) | |||||
| .set("reason", reason); | |||||
| } | |||||
| default: | |||||
| return state; | |||||
| } | |||||
| }; | |||||
| export default (state = initialState, action) => { | |||||
| const { type, payload } = action; | |||||
| switch (type) { | |||||
| case LOGIN_GET_STATUS: | |||||
| return state.set("status", LOGIN_STATUS_GETTING); | |||||
| case SOCKET_RECEIVE_MESSAGE: | |||||
| return reduceReceiveMessage(state, payload); | |||||
| default: | |||||
| return state; | |||||
| } | |||||
| }; | |||||
| @ -1,123 +0,0 @@ | |||||
| import Immutable from "immutable"; | |||||
| import OrderedMap from "../utils/OrderedMap"; | |||||
| import { | |||||
| ROOM_JOIN, | |||||
| ROOM_LEAVE, | |||||
| ROOM_MESSAGE, | |||||
| ROOM_SHOW_USERS, | |||||
| ROOM_HIDE_USERS, | |||||
| SOCKET_RECEIVE_MESSAGE, | |||||
| } from "../constants/ActionTypes"; | |||||
| const RoomRecord = Immutable.Record({ | |||||
| membership: "", | |||||
| visibility: "", | |||||
| operated: false, | |||||
| userCount: 0, | |||||
| owner: "", | |||||
| operators: Immutable.Map(), | |||||
| members: Immutable.Map(), | |||||
| messages: Immutable.List(), | |||||
| tickers: Immutable.List(), | |||||
| }); | |||||
| const initialState = OrderedMap(); | |||||
| const reduceReceiveMessageRoom = (roomData, { variant, data }) => { | |||||
| switch (variant) { | |||||
| case "RoomJoinResponse": | |||||
| return roomData.set("membership", "Member"); | |||||
| case "RoomLeaveResponse": | |||||
| return roomData.set("membership", "NonMember"); | |||||
| case "RoomMessageResponse": { | |||||
| const { user_name, message } = data; | |||||
| const messages = roomData.messages.push({ user_name, message }); | |||||
| return roomData.set("messages", messages); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const reduceReceiveMessage = (state, message) => { | |||||
| const { variant, data } = message; | |||||
| switch (variant) { | |||||
| case "RoomJoinResponse": | |||||
| case "RoomLeaveResponse": | |||||
| case "RoomMessageResponse": { | |||||
| const { room_name } = data; | |||||
| return state.updateByName(data.room_name, (roomData) => { | |||||
| if (roomData) { | |||||
| return reduceReceiveMessageRoom(roomData, message); | |||||
| } else { | |||||
| console.log(`Error: unknown room ${data.room_name}`); | |||||
| return roomData; | |||||
| } | |||||
| }); | |||||
| } | |||||
| case "RoomListResponse": | |||||
| return state.updateAll(data.rooms, (newData, oldData) => { | |||||
| if (!oldData) { | |||||
| oldData = RoomRecord(); | |||||
| } | |||||
| return oldData | |||||
| .set("membership", newData.membership) | |||||
| .set("visibility", newData.visibility) | |||||
| .set("operated", newData.operated) | |||||
| .set("userCount", newData.user_count) | |||||
| .set("owner", newData.owner) | |||||
| .set("operators", newData.operators) | |||||
| .set("members", newData.members) | |||||
| .set("tickers", newData.tickers); | |||||
| }); | |||||
| default: | |||||
| return state; | |||||
| } | |||||
| }; | |||||
| const reduceRoom = (roomData, { type, payload }) => { | |||||
| switch (type) { | |||||
| case ROOM_JOIN: | |||||
| return roomData.set("membership", "Joining"); | |||||
| case ROOM_LEAVE: | |||||
| return roomData.set("membership", "Leaving"); | |||||
| case ROOM_SHOW_USERS: | |||||
| return roomData.set("showUsers", true); | |||||
| case ROOM_HIDE_USERS: | |||||
| return roomData.set("showUsers", false); | |||||
| } | |||||
| }; | |||||
| export default (state = initialState, action) => { | |||||
| const { type, payload } = action; | |||||
| switch (type) { | |||||
| case SOCKET_RECEIVE_MESSAGE: | |||||
| return reduceReceiveMessage(state, payload); | |||||
| case ROOM_JOIN: | |||||
| case ROOM_LEAVE: | |||||
| case ROOM_SHOW_USERS: | |||||
| case ROOM_HIDE_USERS: { | |||||
| return state.updateByName(payload, (roomData) => { | |||||
| if (roomData) { | |||||
| return reduceRoom(roomData, action); | |||||
| } else { | |||||
| console.log(`Error: unknown room ${payload}`); | |||||
| return roomData; | |||||
| } | |||||
| }); | |||||
| } | |||||
| case ROOM_MESSAGE: | |||||
| default: | |||||
| return state; | |||||
| } | |||||
| }; | |||||
| @ -1,94 +0,0 @@ | |||||
| import Immutable from "immutable"; | |||||
| import * as types from "../constants/ActionTypes"; | |||||
| import { | |||||
| STATE_OPENING, | |||||
| STATE_OPEN, | |||||
| STATE_CLOSING, | |||||
| STATE_CLOSED, | |||||
| } from "../constants/socket"; | |||||
| import ControlRequest from "../utils/ControlRequest"; | |||||
| export const SocketRecord = Immutable.Record({ | |||||
| state: STATE_CLOSED, | |||||
| socket: undefined, | |||||
| url: undefined, | |||||
| }); | |||||
| const initialState = new SocketRecord(); | |||||
| export default (state = initialState, { type, payload }) => { | |||||
| const sendRequest = (controlRequest) => { | |||||
| try { | |||||
| state.socket.send(JSON.stringify(controlRequest)); | |||||
| } catch (err) { | |||||
| console.log(`Socket error: failed to send ${controlRequest}`); | |||||
| } | |||||
| }; | |||||
| switch (type) { | |||||
| case types.SOCKET_SET_OPENING: { | |||||
| if (state.state !== STATE_CLOSED) { | |||||
| console.log("Cannot open socket, already open"); | |||||
| return state; | |||||
| } | |||||
| const { url, onopen, onclose, onerror, onmessage } = payload; | |||||
| const socket = new WebSocket(url); | |||||
| socket.onopen = onopen; | |||||
| socket.onclose = onclose; | |||||
| socket.onerror = onerror; | |||||
| socket.onmessage = onmessage; | |||||
| return state | |||||
| .set("state", STATE_OPENING) | |||||
| .set("socket", socket) | |||||
| .set("url", url); | |||||
| } | |||||
| case types.SOCKET_SET_OPEN: | |||||
| return state.set("state", STATE_OPEN); | |||||
| case types.SOCKET_SET_CLOSING: | |||||
| // Ooh bad stateful reducing... | |||||
| state.socket.close(); | |||||
| return state.set("state", STATE_CLOSING); | |||||
| case types.SOCKET_SET_CLOSED: | |||||
| return state.set("state", STATE_CLOSED); | |||||
| case types.SOCKET_SET_ERROR: | |||||
| console.log("Socket error"); | |||||
| return state.set("state", state.socket.readyState); | |||||
| case types.LOGIN_GET_STATUS: | |||||
| sendRequest(ControlRequest.loginStatus()); | |||||
| return state; | |||||
| case types.ROOM_GET_LIST: | |||||
| sendRequest(ControlRequest.roomList()); | |||||
| return state; | |||||
| case types.ROOM_JOIN: | |||||
| sendRequest(ControlRequest.roomJoin(payload)); | |||||
| return state; | |||||
| case types.ROOM_LEAVE: | |||||
| sendRequest(ControlRequest.roomLeave(payload)); | |||||
| return state; | |||||
| case types.ROOM_MESSAGE: { | |||||
| const { room_name, message } = payload; | |||||
| sendRequest(ControlRequest.roomMessage(room_name, message)); | |||||
| return state; | |||||
| } | |||||
| case types.USER_GET_LIST: | |||||
| sendRequest(ControlRequest.userList()); | |||||
| return state; | |||||
| default: | |||||
| return state; | |||||
| } | |||||
| }; | |||||
| @ -1,84 +0,0 @@ | |||||
| import Immutable from "immutable"; | |||||
| import md5 from "md5"; | |||||
| // Updates should be requested every 5 minutes at most. | |||||
| const UPDATE_INTERVAL_MS = 5 * 60 * 1000; | |||||
| const MapRecord = Immutable.Record({ | |||||
| byName: Immutable.OrderedMap(), | |||||
| byHash: Immutable.Map(), | |||||
| lastUpdated: 0, | |||||
| }); | |||||
| // TODO: use a regular map and a reversible name -> hash encoding (e.g. base64). | |||||
| // This would entirely remove the need for this complicated logic. | |||||
| class OrderedMap extends MapRecord { | |||||
| constructor(arg) { | |||||
| super(arg); | |||||
| } | |||||
| getByName(name) { | |||||
| return this.getIn(["byName", name]); | |||||
| } | |||||
| setByName(name, data) { | |||||
| let thisModified = this; | |||||
| if (!this.getByName(name)) { | |||||
| // That key was not there yet, add hash -> name mapping. | |||||
| thisModified = this.setIn(["byHash", md5(name)], name); | |||||
| } | |||||
| // Add data to map. | |||||
| return thisModified.setIn(["byName", name], data); | |||||
| } | |||||
| updateByName(name, updater) { | |||||
| return this.updateIn(["byName", name], updater); | |||||
| } | |||||
| getNameByHash(hash) { | |||||
| return this.getIn(["byHash", hash]); | |||||
| } | |||||
| updateAll(nameAndDataList, merger) { | |||||
| const { byName } = this; | |||||
| let { byHash } = this; | |||||
| // First sort the room list by room name | |||||
| nameAndDataList.sort(([name1], [name2]) => { | |||||
| if (name1 < name2) { | |||||
| return -1; | |||||
| } else if (name1 > name2) { | |||||
| return 1; | |||||
| } | |||||
| return 0; | |||||
| }); | |||||
| // Then build the new map. | |||||
| let newByName = Immutable.OrderedMap(); | |||||
| for (const [name, newData] of nameAndDataList) { | |||||
| // Get the old room data. | |||||
| let data = byName.get(name); | |||||
| if (!data) { | |||||
| // Add the hash -> name mapping. | |||||
| byHash = byHash.set(md5(name), name); | |||||
| } | |||||
| // Merge the old data and the new data using the provided function. | |||||
| const mergedData = merger(newData, data); | |||||
| // Insert that in the new room map. | |||||
| newByName = newByName.set(name, mergedData); | |||||
| } | |||||
| return new OrderedMap({ | |||||
| byName: newByName, | |||||
| byHash, | |||||
| lastUpdated: Date.now(), | |||||
| }); | |||||
| } | |||||
| shouldUpdate() { | |||||
| return Date.now() - this.lastUpdated > UPDATE_INTERVAL_MS; | |||||
| } | |||||
| } | |||||
| export default () => new OrderedMap(); | |||||
| @ -1,16 +0,0 @@ | |||||
| const checkRequiredThenValidate = | |||||
| (validator) => (props, propName, componentName, location) => { | |||||
| if (props[propName] != null) { | |||||
| return validator(props, propName, componentName, location); | |||||
| } | |||||
| return new Error( | |||||
| `Required prop \`${propName}\` was not specified in ` + | |||||
| `\`${componentName}\`.` | |||||
| ); | |||||
| }; | |||||
| export default (validator) => { | |||||
| validator.isRequired = checkRequiredThenValidate(validator); | |||||
| return validator; | |||||
| }; | |||||
| @ -1,19 +0,0 @@ | |||||
| import propTypeRequiredWrapper from "./propTypeRequiredWrapper"; | |||||
| // Simple validator for Symbols (instanceof does not work on symbols). | |||||
| function propTypeSymbol(props, propName, componentName) { | |||||
| const prop = props[propName]; | |||||
| if (prop === null) { | |||||
| return; | |||||
| } | |||||
| const type = typeof prop; | |||||
| if (type === "symbol") { | |||||
| return; | |||||
| } | |||||
| return new Error( | |||||
| `Invalid prop \`${propName}\` of type \`${type}\` ` + | |||||
| `supplied to \`${componentName}\`, expected \`symbol\`` | |||||
| ); | |||||
| } | |||||
| export default propTypeRequiredWrapper(propTypeSymbol); | |||||