Browse Source

Install and run prettier formatter.

main
Titouan Rigoudy 4 years ago
parent
commit
e82639f7df
55 changed files with 1114 additions and 1164 deletions
  1. +4
    -0
      .prettierignore
  2. +1
    -0
      .prettierrc.json
  3. +2
    -3
      TODO.md
  4. +6
    -6
      src/App.test.tsx
  5. +4
    -4
      src/App.tsx
  6. +3
    -3
      src/actions/LoginActions.js
  7. +39
    -39
      src/actions/RoomActions.js
  8. +24
    -24
      src/actions/SocketActions.js
  9. +27
    -24
      src/actions/SocketHandlerActions.js
  10. +3
    -3
      src/actions/UserActions.js
  11. +2
    -2
      src/app/hooks.ts
  12. +5
    -5
      src/app/store.ts
  13. +6
    -8
      src/components/ConnectForm.js
  14. +11
    -11
      src/components/Header.js
  15. +33
    -43
      src/components/LoginStatusPane.js
  16. +18
    -17
      src/components/Room.js
  17. +60
    -69
      src/components/RoomChat.js
  18. +30
    -32
      src/components/RoomChatForm.js
  19. +43
    -48
      src/components/RoomChatHeader.js
  20. +27
    -28
      src/components/RoomChatMessageList.js
  21. +9
    -9
      src/components/RoomList.js
  22. +11
    -11
      src/components/RoomListHeader.js
  23. +13
    -15
      src/components/RoomUserList.js
  24. +45
    -53
      src/components/SearchableList.js
  25. +2
    -2
      src/components/SocketStatusPane.tsx
  26. +2
    -6
      src/components/SolsticeApp.js
  27. +7
    -10
      src/components/User.js
  28. +7
    -7
      src/components/UserList.js
  29. +11
    -11
      src/constants/ActionTypes.js
  30. +11
    -9
      src/containers/ConnectPage.js
  31. +1
    -1
      src/containers/Footer.js
  32. +37
    -45
      src/containers/RoomsPane.js
  33. +9
    -12
      src/containers/UsersPane.js
  34. +2
    -2
      src/features/counter/Counter.module.css
  35. +5
    -5
      src/features/counter/Counter.tsx
  36. +9
    -9
      src/features/counter/counterSlice.spec.ts
  37. +17
    -18
      src/features/counter/counterSlice.ts
  38. +5
    -5
      src/index.tsx
  39. +5
    -7
      src/modules/login/message.ts
  40. +15
    -13
      src/modules/login/slice.ts
  41. +5
    -5
      src/modules/websocket/actions.ts
  42. +7
    -7
      src/modules/websocket/message.ts
  43. +30
    -30
      src/modules/websocket/middleware.ts
  44. +7
    -7
      src/modules/websocket/slice.ts
  45. +5
    -5
      src/reducers/index.js
  46. +51
    -51
      src/reducers/login.js
  47. +92
    -95
      src/reducers/rooms.js
  48. +78
    -77
      src/reducers/socket.js
  49. +32
    -34
      src/reducers/users.js
  50. +1
    -1
      src/setupTests.ts
  51. +129
    -127
      src/styles/styles.scss
  52. +29
    -27
      src/utils/ControlRequest.js
  53. +58
    -58
      src/utils/OrderedMap.js
  54. +7
    -8
      src/utils/propTypeRequiredWrapper.js
  55. +12
    -13
      src/utils/propTypeSymbol.js

+ 4
- 0
.prettierignore View File

@ -0,0 +1,4 @@
build
node_modules
package.json
package-lock.json

+ 1
- 0
.prettierrc.json View File

@ -0,0 +1 @@
{}

+ 2
- 3
TODO.md View File

@ -1,4 +1,3 @@
Things to do:
-------------
## Things to do:
- actually join rooms, display and send messages
- actually join rooms, display and send messages

+ 6
- 6
src/App.test.tsx View File

@ -1,10 +1,10 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import React from "react";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
import { store } from "./app/store";
import App from "./App";
test('renders learn react link', () => {
test("renders learn react link", () => {
const { getByText } = render(
<Provider store={store}>
<App />


+ 4
- 4
src/App.tsx View File

@ -1,7 +1,7 @@
import React from 'react';
import logo from './logo.svg';
import { Counter } from './features/counter/Counter';
import './App.css';
import React from "react";
import logo from "./logo.svg";
import { Counter } from "./features/counter/Counter";
import "./App.css";
function App() {
return (


+ 3
- 3
src/actions/LoginActions.js View File

@ -1,7 +1,7 @@
import { LOGIN_GET_STATUS } from "../constants/ActionTypes";
export default {
getStatus: () => ({
type: LOGIN_GET_STATUS
})
getStatus: () => ({
type: LOGIN_GET_STATUS,
}),
};

+ 39
- 39
src/actions/RoomActions.js View File

@ -1,48 +1,48 @@
import {
ROOM_GET_LIST,
ROOM_JOIN,
ROOM_LEAVE,
ROOM_MESSAGE,
ROOM_SELECT,
ROOM_SHOW_USERS,
ROOM_HIDE_USERS
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
}),
export default {
getList: () => ({
type: ROOM_GET_LIST,
}),
join: (room_name) => ({
type: ROOM_JOIN,
payload: room_name
}),
join: (room_name) => ({
type: ROOM_JOIN,
payload: room_name,
}),
leave: (room_name) => ({
type: ROOM_LEAVE,
payload: room_name
}),
leave: (room_name) => ({
type: ROOM_LEAVE,
payload: room_name,
}),
select: (room_name) => ({
type: ROOM_SELECT,
payload: room_name
}),
select: (room_name) => ({
type: ROOM_SELECT,
payload: room_name,
}),
sendMessage: (room_name, message) => ({
type: ROOM_MESSAGE,
payload: {
room_name,
message
}
}),
sendMessage: (room_name, message) => ({
type: ROOM_MESSAGE,
payload: {
room_name,
message,
},
}),
showUsers: (room_name) => ({
type: ROOM_SHOW_USERS,
payload: room_name
}),
showUsers: (room_name) => ({
type: ROOM_SHOW_USERS,
payload: room_name,
}),
hideUsers: (room_name) => ({
type: ROOM_HIDE_USERS,
payload: room_name
})
});
hideUsers: (room_name) => ({
type: ROOM_HIDE_USERS,
payload: room_name,
}),
};

+ 24
- 24
src/actions/SocketActions.js View File

@ -1,29 +1,29 @@
import {
SOCKET_RECEIVE_MESSAGE,
SOCKET_SEND_MESSAGE,
SOCKET_SET_CLOSED,
SOCKET_SET_CLOSING,
SOCKET_SET_ERROR,
SOCKET_SET_OPEN,
SOCKET_SET_OPENING
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
}
}),
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 }),
close: () => ({ type: SOCKET_SET_CLOSING }),
send: (message) => ({
type: SOCKET_SEND_MESSAGE,
payload: message
})
});
send: (message) => ({
type: SOCKET_SEND_MESSAGE,
payload: message,
}),
};

+ 27
- 24
src/actions/SocketHandlerActions.js View File

@ -1,33 +1,36 @@
import {
SOCKET_SET_CLOSED,
SOCKET_SET_ERROR,
SOCKET_SET_OPEN,
SOCKET_RECEIVE_MESSAGE
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
}),
onclose: (event) => ({
type: SOCKET_SET_CLOSED,
payload: event.code,
}),
onerror: event => ({ type: SOCKET_SET_ERROR }),
onerror: (event) => ({ type: SOCKET_SET_ERROR }),
onopen: event => ({ type: SOCKET_SET_OPEN }),
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;
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;
},
};

+ 3
- 3
src/actions/UserActions.js View File

@ -1,9 +1,9 @@
import { USER_GET_LIST } from "../constants/ActionTypes";
const UserActions = {
getList: () => ({
type: USER_GET_LIST
})
getList: () => ({
type: USER_GET_LIST,
}),
};
export default UserActions;

+ 2
- 2
src/app/hooks.ts View File

@ -1,5 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();


+ 5
- 5
src/app/store.ts View File

@ -1,4 +1,4 @@
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
// TODO: Rework these to use a slice, otherwise redux complains about
@ -20,10 +20,10 @@ export const store = configureStore({
socket: socketReducer,
//users,
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(makeSocketMiddleware(
loginSocketMessageHandlers))
)
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
makeSocketMiddleware(loginSocketMessageHandlers)
),
});
export type AppDispatch = typeof store.dispatch;


+ 6
- 8
src/components/ConnectForm.js View File

@ -5,12 +5,12 @@ import SocketStatusPane from "./SocketStatusPane";
import {
SocketSliceState,
SocketState,
socketOpen
socketOpen,
} from "../modules/websocket/slice";
interface Props {
socket: SocketSliceState,
};
socket: SocketSliceState;
}
const ConnectForm: React.FC<Props> = ({ socket }) => {
const dispatch = useDispatch();
@ -24,7 +24,7 @@ const ConnectForm: React.FC<Props> = ({ socket }) => {
return (
<div id="connect-form">
<h2>Connect to a solstice client</h2>
<Form
<Form
onSubmit={onSubmit}
render={({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
@ -36,10 +36,8 @@ const ConnectForm: React.FC<Props> = ({ socket }) => {
required
pattern="wss?://.+"
/>
<button
type="submit"
disabled={submitting || !isSocketClosed}>
Connect
<button type="submit" disabled={submitting || !isSocketClosed}>
Connect
</button>
</form>
)}


+ 11
- 11
src/components/Header.js View File

@ -2,17 +2,17 @@ import React from "react";
import { Link } from "react-router-dom";
const Header = () => {
return (
<header>
<h1>Solstice web UI</h1>
<Link to="/app/rooms" activeClassName="active">
Rooms
</Link>
<Link to="/app/users" activeClassName="active">
Users
</Link>
</header>
);
return (
<header>
<h1>Solstice web UI</h1>
<Link to="/app/rooms" activeClassName="active">
Rooms
</Link>
<Link to="/app/users" activeClassName="active">
Users
</Link>
</header>
);
};
export default Header;

+ 33
- 43
src/components/LoginStatusPane.js View File

@ -1,8 +1,8 @@
import { LoginStatus, LoginSliceState } from "../modules/login/slice";
interface Props {
login: LoginSliceState,
};
login: LoginSliceState;
}
const LoginStatusPane: React.FC<Props> = ({ login }) => {
let statusText;
@ -10,50 +10,40 @@ const LoginStatusPane: React.FC<Props> = ({ login }) => {
let reason;
switch (login.status) {
case LoginStatus.Unknown:
statusText = "unknown";
break;
case LoginStatus.Fetching:
statusText = "fetching";
break;
case LoginStatus.Pending:
statusText = `logging in as ${login.username}`;
break;
case LoginStatus.Success:
statusText = `logged in as ${login.username}`;
motd = (
<span id="login-status-motd">
MOTD: {login.motd}
</span>
);
break;
case LoginStatus.Failure:
statusText = `failed to log in as ${login.username}`;
reason = (
<span id="login-status-reason">
Reason: {login.reason}
</span>
);
break;
default:
statusText = `invalid status ${login.status}`;
break;
case LoginStatus.Unknown:
statusText = "unknown";
break;
case LoginStatus.Fetching:
statusText = "fetching";
break;
case LoginStatus.Pending:
statusText = `logging in as ${login.username}`;
break;
case LoginStatus.Success:
statusText = `logged in as ${login.username}`;
motd = <span id="login-status-motd">MOTD: {login.motd}</span>;
break;
case LoginStatus.Failure:
statusText = `failed to log in as ${login.username}`;
reason = <span id="login-status-reason">Reason: {login.reason}</span>;
break;
default:
statusText = `invalid status ${login.status}`;
break;
}
return (
<div id="login-status-pane">
<span id="login-status-text">
Login status: {statusText}
</span>
{motd}
{reason}
</div>
<div id="login-status-pane">
<span id="login-status-text">Login status: {statusText}</span>
{motd}
{reason}
</div>
);
}
};
export default LoginStatusPane;

+ 18
- 17
src/components/Room.js View File

@ -5,29 +5,30 @@ import ImmutablePropTypes from "react-immutable-proptypes";
import md5 from "md5";
const Room = ({ name, data }) => {
const { membership, userCount } = data;
const { membership, userCount } = data;
const classes = ["room"];
if (membership == "Member") {
classes.push("room-joined");
}
const classes = ["room"];
if (membership == "Member") {
classes.push("room-joined");
}
const path = `/app/rooms/${md5(name)}`;
const path = `/app/rooms/${md5(name)}`;
return (
<Link to={path}
activeClassName="room-selected"
className={classes.join(" ")}
>
<span className="room-name">{name}</span>
<span className="room-user-count">({userCount})</span>
</Link>
);
return (
<Link
to={path}
activeClassName="room-selected"
className={classes.join(" ")}
>
<span className="room-name">{name}</span>
<span className="room-user-count">({userCount})</span>
</Link>
);
};
Room.propTypes = {
name: PropTypes.string.isRequired,
data: ImmutablePropTypes.map.isRequired
name: PropTypes.string.isRequired,
data: ImmutablePropTypes.map.isRequired,
};
export default Room;

+ 60
- 69
src/components/RoomChat.js View File

@ -12,88 +12,79 @@ import RoomChatMessageList from "../components/RoomChatMessageList";
const ID = "room-chat";
class RoomChat extends React.Component {
constructor(props) {
super(props);
}
constructor(props) {
super(props);
}
componentDidMount() {
this.join_if_non_member(this.props);
}
componentDidMount() {
this.join_if_non_member(this.props);
}
componentWillReceiveProps(props) {
this.join_if_non_member(props);
}
componentWillReceiveProps(props) {
this.join_if_non_member(props);
}
join_if_non_member(props) {
const { room, roomActions } = props;
if (room && room.membership == "NonMember") {
roomActions.join(room.name);
}
join_if_non_member(props) {
const { room, roomActions } = props;
if (room && room.membership == "NonMember") {
roomActions.join(room.name);
}
}
render() {
const { loginUserName, room, roomActions } = this.props;
render() {
const { loginUserName, room, roomActions } = this.props;
if (!room) {
return (
<div id={ID}>
<RoomChatHeader
roomActions={roomActions}
/>
</div>
);
}
const { name, membership, messages, showUsers } = room;
if (!room) {
return (
<div id={ID}>
<RoomChatHeader roomActions={roomActions} />
</div>
);
}
const header = (
<RoomChatHeader
room={{
membership,
name,
showUsers
}}
roomActions={roomActions}
/>
);
const { name, membership, messages, showUsers } = room;
if (membership != "Member") {
return (
<div id={ID}>
{header}
</div>
);
}
const header = (
<RoomChatHeader
room={{
membership,
name,
showUsers,
}}
roomActions={roomActions}
/>
);
// room.membership == "Member"
return (
<div id={ID}>
{header}
<RoomChatMessageList
loginUserName={loginUserName}
messages={messages}
/>
<RoomChatForm
roomName={name}
sendMessage={roomActions.sendMessage}
/>
</div>
);
if (membership != "Member") {
return <div id={ID}>{header}</div>;
}
// room.membership == "Member"
return (
<div id={ID}>
{header}
<RoomChatMessageList
loginUserName={loginUserName}
messages={messages}
/>
<RoomChatForm roomName={name} sendMessage={roomActions.sendMessage} />
</div>
);
}
}
RoomChat.propTypes = {
loginUserName: PropTypes.string,
room: PropTypes.shape({
name: PropTypes.string.isRequired,
membership: PropTypes.string.isRequired,
messages: ImmutablePropTypes.list.isRequired,
showUsers: PropTypes.bool
}),
roomActions: PropTypes.shape({
join: PropTypes.func.isRequired,
sendMessage: PropTypes.func.isRequired
}).isRequired
loginUserName: PropTypes.string,
room: PropTypes.shape({
name: PropTypes.string.isRequired,
membership: PropTypes.string.isRequired,
messages: ImmutablePropTypes.list.isRequired,
showUsers: PropTypes.bool,
}),
roomActions: PropTypes.shape({
join: PropTypes.func.isRequired,
sendMessage: PropTypes.func.isRequired,
}).isRequired,
};
export default RoomChat;

+ 30
- 32
src/components/RoomChatForm.js View File

@ -1,43 +1,41 @@
import React, {PropTypes} from "react";
import {reduxForm} from "redux-form";
import React, { PropTypes } from "react";
import { reduxForm } from "redux-form";
const RoomChatForm = (props) => {
const {
fields: { message },
handleSubmit,
resetForm,
roomName,
sendMessage
} = props;
const {
fields: { message },
handleSubmit,
resetForm,
roomName,
sendMessage,
} = props;
const onSubmit = handleSubmit((values) => {
sendMessage(roomName, values.message);
resetForm();
});
const onSubmit = handleSubmit((values) => {
sendMessage(roomName, values.message);
resetForm();
});
return (
<div id="room-chat-form">
<form onSubmit={onSubmit}>
<input type="text" placeholder="Type a message..."
{...message} />
<button type="submit">Send</button>
</form>
</div>
);
return (
<div id="room-chat-form">
<form onSubmit={onSubmit}>
<input type="text" placeholder="Type a message..." {...message} />
<button type="submit">Send</button>
</form>
</div>
);
};
RoomChatForm.propTypes = {
fields: PropTypes.shape({
message: PropTypes.object.isRequired
}).isRequired,
handleSubmit: PropTypes.func.isRequired,
resetForm: PropTypes.func.isRequired,
roomName: PropTypes.string,
sendMessage: PropTypes.func.isRequired
fields: PropTypes.shape({
message: PropTypes.object.isRequired,
}).isRequired,
handleSubmit: PropTypes.func.isRequired,
resetForm: PropTypes.func.isRequired,
roomName: PropTypes.string,
sendMessage: PropTypes.func.isRequired,
};
export default reduxForm({
form: "chat",
fields: ["message"]
form: "chat",
fields: ["message"],
})(RoomChatForm);

+ 43
- 48
src/components/RoomChatHeader.js View File

@ -3,64 +3,59 @@ import { withRouter } from "react-router";
import ImmutablePropTypes from "react-immutable-proptypes";
const make_header = (title, showUsersButton, leaveButton) => (
<div id="room-chat-header">
<div id="room-chat-header-title">{title}</div>
{showUsersButton}
{leaveButton}
</div>
<div id="room-chat-header">
<div id="room-chat-header-title">{title}</div>
{showUsersButton}
{leaveButton}
</div>
);
const RoomChatHeader = ({ room, roomActions, router }) => {
if (!room) {
return make_header("Select a room");
if (!room) {
return make_header("Select a room");
}
switch (room.membership) {
case "Member": {
const onClickLeave = (event) => {
router.push("/app/rooms");
roomActions.leave(room.name);
};
const leaveButton = <button onClick={onClickLeave}>Leave</button>;
let toggleUsersButton;
if (room.showUsers) {
const onClick = (event) => roomActions.hideUsers(room.name);
toggleUsersButton = <button onClick={onClick}>Hide users</button>;
} else {
const onClick = (event) => roomActions.showUsers(room.name);
toggleUsersButton = <button onClick={onClick}>Show users</button>;
}
return make_header(room.name, toggleUsersButton, leaveButton);
}
switch (room.membership) {
case "Member":
{
const onClickLeave = (event) => {
router.push("/app/rooms");
roomActions.leave(room.name);
};
const leaveButton = <button onClick={onClickLeave}>Leave</button>;
case "NonMember":
return make_header(`Not a member of ${room.name}`);
let toggleUsersButton;
if (room.showUsers) {
const onClick = (event) => roomActions.hideUsers(room.name);
toggleUsersButton = (
<button onClick={onClick}>Hide users</button>
);
} else {
const onClick = (event) => roomActions.showUsers(room.name);
toggleUsersButton = (
<button onClick={onClick}>Show users</button>
);
}
case "Joining":
return make_header(`Joining ${room.name}`);
return make_header(room.name, toggleUsersButton, leaveButton);
}
case "NonMember":
return make_header(`Not a member of ${room.name}`);
case "Joining":
return make_header(`Joining ${room.name}`);
case "Leaving":
return make_header(`Leaving ${room.name}`);
}
case "Leaving":
return make_header(`Leaving ${room.name}`);
}
};
RoomChatHeader.propTypes = {
room: PropTypes.shape({
membership: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
showUsers: PropTypes.bool
}),
roomActions: PropTypes.shape({
leave: PropTypes.func.isRequired
}).isRequired,
router: PropTypes.object.isRequired
room: PropTypes.shape({
membership: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
showUsers: PropTypes.bool,
}),
roomActions: PropTypes.shape({
leave: PropTypes.func.isRequired,
}).isRequired,
router: PropTypes.object.isRequired,
};
export default withRouter(RoomChatHeader);

+ 27
- 28
src/components/RoomChatMessageList.js View File

@ -2,39 +2,38 @@ import React, { PropTypes } from "react";
import ImmutablePropTypes from "react-immutable-proptypes";
const RoomChatMessageList = ({ loginUserName, messages }) => {
// Append all messages in the chat room.
const children = [];
let i = 0;
for (const { user_name, message } of messages) {
if (user_name == loginUserName) {
children.push(
<li key={i} className="room-chat-message room-chat-message-me">
<div className="room-chat-message-text">{message}</div>
</li>
);
} else {
children.push(
<li key={i} className="room-chat-message">
<div className="room-chat-message-user">{user_name}</div>
<div className="room-chat-message-text">{message}</div>
</li>
);
}
i++;
// Append all messages in the chat room.
const children = [];
let i = 0;
for (const { user_name, message } of messages) {
if (user_name == loginUserName) {
children.push(
<li key={i} className="room-chat-message room-chat-message-me">
<div className="room-chat-message-text">{message}</div>
</li>
);
} else {
children.push(
<li key={i} className="room-chat-message">
<div className="room-chat-message-user">{user_name}</div>
<div className="room-chat-message-text">{message}</div>
</li>
);
}
i++;
}
return <ul id="room-chat-message-list">{children}</ul>;
return <ul id="room-chat-message-list">{children}</ul>;
};
RoomChatMessageList.propTypes = {
loginUserName: PropTypes.string.isRequired,
messages: ImmutablePropTypes.listOf(
PropTypes.shape({
user_name: PropTypes.string.isRequired,
message: PropTypes.string.isRequired
}).isRequired
).isRequired
loginUserName: PropTypes.string.isRequired,
messages: ImmutablePropTypes.listOf(
PropTypes.shape({
user_name: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
}).isRequired
).isRequired,
};
export default RoomChatMessageList;

+ 9
- 9
src/components/RoomList.js View File

@ -7,18 +7,18 @@ import SearchableList from "./SearchableList";
const ComposedSearchableList = SearchableList(Room);
const RoomList = ({ rooms, roomActions }) => (
<ComposedSearchableList
id="room-list"
itemMap={rooms}
refresh={roomActions.getList}
/>
<ComposedSearchableList
id="room-list"
itemMap={rooms}
refresh={roomActions.getList}
/>
);
RoomList.propTypes = {
rooms: ImmutablePropTypes.record.isRequired,
roomActions: PropTypes.shape({
getList: PropTypes.func.isRequired
}).isRequired
rooms: ImmutablePropTypes.record.isRequired,
roomActions: PropTypes.shape({
getList: PropTypes.func.isRequired,
}).isRequired,
};
export default RoomList;

+ 11
- 11
src/components/RoomListHeader.js View File

@ -1,20 +1,20 @@
import React, { PropTypes } from "react";
const RoomListHeader = ({ refresh }) => {
return (
<div id="room-list-header">
<div>
<h2>Room List</h2>
</div>
<div>
<button onClick={refresh}>Refresh</button>
</div>
</div>
);
return (
<div id="room-list-header">
<div>
<h2>Room List</h2>
</div>
<div>
<button onClick={refresh}>Refresh</button>
</div>
</div>
);
};
RoomListHeader.propTypes = {
refresh: PropTypes.func.isRequired
refresh: PropTypes.func.isRequired,
};
export default RoomListHeader;

+ 13
- 15
src/components/RoomUserList.js View File

@ -1,25 +1,23 @@
import React, { PropTypes } from "react";
const RoomUserList = ({ users }) => {
// Append all users
const children = [];
let i = 0;
for (const user of users) {
children.push(
<li key={i} className="room-user">
{user}
</li>
);
i++;
}
// Append all users
const children = [];
let i = 0;
for (const user of users) {
children.push(
<li key={i} className="room-user">
{user}
</li>
);
i++;
}
return <ul id="room-user-list">{children}</ul>;
return <ul id="room-user-list">{children}</ul>;
};
RoomUserList.propTypes = {
users: PropTypes.arrayOf(
PropTypes.string.isRequired
).isRequired
users: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
};
export default RoomUserList;

+ 45
- 53
src/components/SearchableList.js View File

@ -2,64 +2,56 @@ import React, { PropTypes } from "react";
import ImmutablePropTypes from "react-immutable-proptypes";
const SearchableList = (ItemComponent) => {
class ComposedSearchableList extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
const { itemMap, refresh } = this.props;
if (itemMap.shouldUpdate()) {
refresh();
}
}
render() {
const { id, itemMap, refresh } = this.props;
const children = [];
for (const [itemName, itemData] of itemMap.byName) {
children.push(
<li key={itemName}>
<ItemComponent
name={itemName}
data={itemData}
/>
</li>
);
}
class ComposedSearchableList extends React.Component {
constructor(props) {
super(props);
}
const onClick = (event) => {
event.preventDefault();
refresh();
};
componentDidMount() {
const { itemMap, refresh } = this.props;
if (itemMap.shouldUpdate()) {
refresh();
}
}
return (
<div id={id}>
<div id={`${id}-header`}>
<div>
<button onClick={onClick}>
Refresh
</button>
</div>
</div>
<ul>
{children}
</ul>
</div>
);
}
render() {
const { id, itemMap, refresh } = this.props;
const children = [];
for (const [itemName, itemData] of itemMap.byName) {
children.push(
<li key={itemName}>
<ItemComponent name={itemName} data={itemData} />
</li>
);
}
const onClick = (event) => {
event.preventDefault();
refresh();
};
return (
<div id={id}>
<div id={`${id}-header`}>
<div>
<button onClick={onClick}>Refresh</button>
</div>
</div>
<ul>{children}</ul>
</div>
);
}
}
ComposedSearchableList.propTypes = {
id: PropTypes.string.isRequired,
itemMap: ImmutablePropTypes.record.isRequired,
refresh: PropTypes.func.isRequired
};
ComposedSearchableList.propTypes = {
id: PropTypes.string.isRequired,
itemMap: ImmutablePropTypes.record.isRequired,
refresh: PropTypes.func.isRequired,
};
return ComposedSearchableList;
return ComposedSearchableList;
};
export default SearchableList;

+ 2
- 2
src/components/SocketStatusPane.tsx View File

@ -17,8 +17,8 @@ function socketStateToString(socket: SocketSliceState): string {
}
interface Props {
socket: SocketSliceState,
};
socket: SocketSliceState;
}
const SocketStatusPane: React.FC<Props> = ({ socket }: Props) => (
<div id="socket-status-pane">


+ 2
- 6
src/components/SolsticeApp.js View File

@ -25,9 +25,7 @@ const ConnectedApp: React.FC<SocketRecord> = ({ socket, children }) => {
return (
<div id="solstice-app">
<Header />
<main>
{children}
</main>
<main>{children}</main>
<Footer />
</div>
);
@ -39,9 +37,7 @@ const SolsticeApp = ({ socket, children }) => (
<ConnectPage />
</Route>
<Route path="/">
<ConnectedApp socket={socket}>
{children}
</ConnectedApp>
<ConnectedApp socket={socket}>{children}</ConnectedApp>
</Route>
</Switch>
);


+ 7
- 10
src/components/User.js View File

@ -3,20 +3,17 @@ import { Link } from "react-router";
import md5 from "md5";
const User = ({ name }) => {
const path = `/app/users/${md5(name)}`;
const path = `/app/users/${md5(name)}`;
return (
<Link to={path}
className="user"
activeClassName="user-selected"
>
{name}
</Link>
);
return (
<Link to={path} className="user" activeClassName="user-selected">
{name}
</Link>
);
};
User.propTypes = {
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
};
export default User;

+ 7
- 7
src/components/UserList.js View File

@ -7,16 +7,16 @@ import User from "./User";
const ComposedSearchableList = SearchableList(User);
const UserList = ({ users, userActions }) => (
<ComposedSearchableList
id="user-list"
itemMap={users}
refresh={userActions.getList}
/>
<ComposedSearchableList
id="user-list"
itemMap={users}
refresh={userActions.getList}
/>
);
UserList.propTypes = {
users: ImmutablePropTypes.record.isRequired,
userActions: PropTypes.object.isRequired
users: ImmutablePropTypes.record.isRequired,
userActions: PropTypes.object.isRequired,
};
export default UserList;

+ 11
- 11
src/constants/ActionTypes.js View File

@ -1,21 +1,21 @@
// 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_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";
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_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";


+ 11
- 9
src/containers/ConnectPage.js View File

@ -2,7 +2,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router";
import { Redirect } from "react-router-dom";
import ConnectForm from "../components/ConnectForm";
import ConnectForm from "../components/ConnectForm";
import { LoginStatus, selectLogin } from "../modules/login/slice";
import { loginStatusRequest } from "../modules/login/message";
import {
@ -27,18 +27,20 @@ const ConnectPage: React.FC = () => {
// not seem to respond to our request for the login status, however. Until
// that is the case, we cannot block on receiving a response...
return (
<Redirect to={{
pathname: "/app/rooms",
state: { from: location },
}} />
<Redirect
to={{
pathname: "/app/rooms",
state: { from: location },
}}
/>
);
}
return (
<div id="connect-page">
<ConnectForm socket={socket} />
</div>
<div id="connect-page">
<ConnectForm socket={socket} />
</div>
);
}
};
export default ConnectPage;

+ 1
- 1
src/containers/Footer.js View File

@ -16,6 +16,6 @@ const Footer: FC = () => {
<LoginStatusPane login={login} />
</footer>
);
}
};
export default Footer;

+ 37
- 45
src/containers/RoomsPane.js View File

@ -10,66 +10,58 @@ import RoomList from "../components/RoomList";
import RoomUserList from "../components/RoomUserList";
const RoomsPane = (props) => {
const { loginUserName, params, rooms, roomActions } = props;
const { loginUserName, params, rooms, roomActions } = props;
let roomName;
let roomChat;
let roomName;
let roomChat;
if (params && params.roomNameHash) {
roomName = rooms.getNameByHash(params.roomNameHash);
if (params && params.roomNameHash) {
roomName = rooms.getNameByHash(params.roomNameHash);
const roomData = rooms.getByName(roomName);
const roomData = rooms.getByName(roomName);
if (roomData) {
const room = {
name: roomName,
membership: roomData.membership,
messages: roomData.messages,
showUsers: roomData.showUsers
};
if (roomData) {
const room = {
name: roomName,
membership: roomData.membership,
messages: roomData.messages,
showUsers: roomData.showUsers,
};
roomChat = (
<RoomChat
loginUserName={loginUserName}
room={room}
roomActions={roomActions}
/>
);
}
roomChat = (
<RoomChat
loginUserName={loginUserName}
room={room}
roomActions={roomActions}
/>
);
}
}
return (
<div id="rooms-pane">
<RoomList
rooms={rooms}
roomActions={roomActions}
/>
<div id="room-selected-pane">
{roomChat}
</div>
</div>
);
return (
<div id="rooms-pane">
<RoomList rooms={rooms} roomActions={roomActions} />
<div id="room-selected-pane">{roomChat}</div>
</div>
);
};
RoomsPane.propTypes = {
loginUserName: PropTypes.string.isRequired,
params: PropTypes.shape({
roomNameHash: PropTypes.string
}),
rooms: ImmutablePropTypes.record.isRequired,
roomActions: PropTypes.object.isRequired
loginUserName: PropTypes.string.isRequired,
params: PropTypes.shape({
roomNameHash: PropTypes.string,
}),
rooms: ImmutablePropTypes.record.isRequired,
roomActions: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
loginUserName: state.login.username,
rooms: state.rooms
loginUserName: state.login.username,
rooms: state.rooms,
});
const mapDispatchToProps = (dispatch) => ({
roomActions: bindActionCreators(RoomActions, dispatch)
roomActions: bindActionCreators(RoomActions, dispatch),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(RoomsPane);
export default connect(mapStateToProps, mapDispatchToProps)(RoomsPane);

+ 9
- 12
src/containers/UsersPane.js View File

@ -8,25 +8,22 @@ import UserActions from "../actions/UserActions";
import UserList from "../components/UserList";
const UsersPane = ({ users, userActions }) => {
return (
<div id="users-pane">
<UserList users={users} userActions={userActions} />
</div>
);
return (
<div id="users-pane">
<UserList users={users} userActions={userActions} />
</div>
);
};
UsersPane.propTypes = {
users: ImmutablePropTypes.record.isRequired,
userActions: PropTypes.object.isRequired
users: ImmutablePropTypes.record.isRequired,
userActions: PropTypes.object.isRequired,
};
const mapStateToProps = ({ users }) => ({ users });
const mapDispatchToProps = (dispatch) => ({
userActions: bindActionCreators(UserActions, dispatch)
userActions: bindActionCreators(UserActions, dispatch),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UsersPane);
export default connect(mapStateToProps, mapDispatchToProps)(UsersPane);

+ 2
- 2
src/features/counter/Counter.module.css View File

@ -18,7 +18,7 @@
padding-left: 16px;
padding-right: 16px;
margin-top: 2px;
font-family: 'Courier New', Courier, monospace;
font-family: "Courier New", Courier, monospace;
}
.button {
@ -60,7 +60,7 @@
}
.asyncButton:after {
content: '';
content: "";
background-color: rgba(112, 76, 182, 0.15);
display: block;
position: absolute;


+ 5
- 5
src/features/counter/Counter.tsx View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import { useAppSelector, useAppDispatch } from "../../app/hooks";
import {
decrement,
increment,
@ -8,13 +8,13 @@ import {
incrementAsync,
incrementIfOdd,
selectCount,
} from './counterSlice';
import styles from './Counter.module.css';
} from "./counterSlice";
import styles from "./Counter.module.css";
export function Counter() {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
const [incrementAmount, setIncrementAmount] = useState("2");
const incrementValue = Number(incrementAmount) || 0;


+ 9
- 9
src/features/counter/counterSlice.spec.ts View File

@ -3,31 +3,31 @@ import counterReducer, {
increment,
decrement,
incrementByAmount,
} from './counterSlice';
} from "./counterSlice";
describe('counter reducer', () => {
describe("counter reducer", () => {
const initialState: CounterState = {
value: 3,
status: 'idle',
status: "idle",
};
it('should handle initial state', () => {
expect(counterReducer(undefined, { type: 'unknown' })).toEqual({
it("should handle initial state", () => {
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
value: 0,
status: 'idle',
status: "idle",
});
});
it('should handle increment', () => {
it("should handle increment", () => {
const actual = counterReducer(initialState, increment());
expect(actual.value).toEqual(4);
});
it('should handle decrement', () => {
it("should handle decrement", () => {
const actual = counterReducer(initialState, decrement());
expect(actual.value).toEqual(2);
});
it('should handle incrementByAmount', () => {
it("should handle incrementByAmount", () => {
const actual = counterReducer(initialState, incrementByAmount(2));
expect(actual.value).toEqual(5);
});


+ 17
- 18
src/features/counter/counterSlice.ts View File

@ -1,15 +1,15 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState, AppThunk } from "../../app/store";
import { fetchCount } from "./counterAPI";
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
status: "idle" | "loading" | "failed";
}
const initialState: CounterState = {
value: 0,
status: 'idle',
status: "idle",
};
// The function below is called a thunk and allows us to perform async logic. It
@ -18,7 +18,7 @@ const initialState: CounterState = {
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
"counter/fetchCount",
async (amount: number) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
@ -27,7 +27,7 @@ export const incrementAsync = createAsyncThunk(
);
export const counterSlice = createSlice({
name: 'counter',
name: "counter",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
@ -51,10 +51,10 @@ export const counterSlice = createSlice({
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
state.status = "loading";
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.status = "idle";
state.value += action.payload;
});
},
@ -69,14 +69,13 @@ export const selectCount = (state: RootState) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount: number): AppThunk => (
dispatch,
getState
) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;

+ 5
- 5
src/index.tsx View File

@ -1,9 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import './styles/styles.scss';
import "./styles/styles.scss";
import { store } from "./app/store";
@ -17,5 +17,5 @@ ReactDOM.render(
</Router>
</Provider>
</React.StrictMode>,
document.getElementById('root')
document.getElementById("root")
);

+ 5
- 7
src/modules/login/message.ts View File

@ -5,7 +5,7 @@ export function loginStatusRequest(): SocketMessage {
return {
variant: "LoginStatusRequest",
fields: [],
}
};
}
const loginStatusResponseHandler: SocketMessageHandler = {
@ -14,14 +14,14 @@ const loginStatusResponseHandler: SocketMessageHandler = {
handleData: ({ variant, fields }) => {
switch (variant) {
case "Pending": {
const [ username ] = fields;
const [username] = fields;
return loginSetState({
status: LoginStatus.Pending,
username,
});
}
case "Success": {
const [ username, motd ] = data.fields;
const [username, motd] = data.fields;
return loginSetState({
status: LoginStatus.Success,
username,
@ -30,7 +30,7 @@ const loginStatusResponseHandler: SocketMessageHandler = {
}
case "Failure": {
const [ username, reason ] = data.fields;
const [username, reason] = data.fields;
return loginSetState({
status: LoginStatus.Failure,
username,
@ -41,6 +41,4 @@ const loginStatusResponseHandler: SocketMessageHandler = {
},
};
export const loginSocketMessageHandlers = [
loginStatusResponseHandler,
];
export const loginSocketMessageHandlers = [loginStatusResponseHandler];

+ 15
- 13
src/modules/login/slice.ts View File

@ -1,6 +1,6 @@
import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from '../../app/store';
import { RootState } from "../../app/store";
export enum LoginStatus {
Unknown,
@ -8,14 +8,14 @@ export enum LoginStatus {
Pending,
Success,
Failure,
};
}
export interface LoginSliceState {
status: LoginStatus,
username?: string,
motd?: string,
reason?: string,
};
status: LoginStatus;
username?: string;
motd?: string;
reason?: string;
}
const initialState: LoginSliceState = {
status: LoginStatus.Unknown,
@ -25,13 +25,15 @@ const initialState: LoginSliceState = {
};
export const loginSlice = createSlice({
name: 'login',
name: "login",
initialState,
reducers: {
loginSetState:
(state: RootState, action: PayloadAction<LoginSliceState>) => {
state = action.payload;
},
loginSetState: (
state: RootState,
action: PayloadAction<LoginSliceState>
) => {
state = action.payload;
},
},
});


+ 5
- 5
src/modules/websocket/actions.ts View File

@ -1,5 +1,5 @@
export const wsConnect = host => ({ type: 'WS_CONNECT', host });
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });
export const wsConnected = host => ({ type: 'WS_CONNECTED', host });
export const wsDisconnect = () => ({ type: 'WS_DISCONNECT' });
export const wsDisconnected = () => ({ type: 'WS_DISCONNECTED' });
export const wsConnect = (host) => ({ type: "WS_CONNECT", host });
export const wsConnecting = (host) => ({ type: "WS_CONNECTING", host });
export const wsConnected = (host) => ({ type: "WS_CONNECTED", host });
export const wsDisconnect = () => ({ type: "WS_DISCONNECT" });
export const wsDisconnected = () => ({ type: "WS_DISCONNECTED" });

+ 7
- 7
src/modules/websocket/message.ts View File

@ -4,9 +4,9 @@ import { PayloadAction } from "@reduxjs/toolkit";
// The type of a message exchanged over a websocket.
export interface SocketMessage {
variant: string,
fields?: List<any>,
};
variant: string;
fields?: List<any>;
}
// Serializes the given message for sending over the wire.
export function serializeMessage(message: SocketMessage): string {
@ -22,7 +22,7 @@ export function parseMessage(serialized: string): SocketMessage {
}
return { variant, fields };
};
}
// Handles the `data` field of a `SocketMessage` and returns an optional action.
export type SocketMessageDataHandler = (any) => PayloadAction<any> | undefined;
@ -34,6 +34,6 @@ export type SocketMessageDataHandler = (any) => PayloadAction<any> | undefined;
// their actions only. Otherwise, most reducers would have to handle all
// incoming messages.
export interface SocketMessageHandler {
variant: string,
handleData: SocketMessageDataHandler,
};
variant: string;
handleData: SocketMessageDataHandler;
}

+ 30
- 30
src/modules/websocket/middleware.ts View File

@ -23,8 +23,8 @@ import { AppDispatch, RootState } from "../app/store";
// TODO: use undefined instead, it is more idiomatic.
let socket: WebSocket | null = null;
const onOpen = (dispatch: AppDispatch) => event => {
console.log('Websocket open', event.target.url);
const onOpen = (dispatch: AppDispatch) => (event) => {
console.log("Websocket open", event.target.url);
dispatch(socketOpened(event.target.url));
};
@ -32,8 +32,9 @@ const onClose = (dispatch: AppDispatch) => () => {
dispatch(socketClosed());
};
function prepareHandlers(handlers: SocketMessageHandler[])
: Map<string, SocketMessageDataHandler[]> {
function prepareHandlers(
handlers: SocketMessageHandler[]
): Map<string, SocketMessageDataHandler[]> {
const map = new Map();
for (const handler of handlers) {
@ -54,35 +55,36 @@ function prepareHandlers(handlers: SocketMessageHandler[])
const onMessage =
(dispatch: AppDispatch, handlers: Map<string, SocketMessageDataHandler[]>) =>
(event: MessageEvent) => {
console.log("Websocket received message", event.data);
let message;
try {
message = parseMessage(event.data);
} catch (err) {
console.log("Websocket received invalid message:", err);
return;
}
console.log("Websocket received message", event.data);
let message;
try {
message = parseMessage(event.data);
} catch (err) {
console.log("Websocket received invalid message:", err);
return;
}
const list = handlers.get(message.variant);
if (list === undefined) {
return;
}
const list = handlers.get(message.variant);
if (list === undefined) {
return;
}
for (handler of list) {
const action = handler(message);
if (action !== undefined) {
dispatch(action);
for (handler of list) {
const action = handler(message);
if (action !== undefined) {
dispatch(action);
}
}
}
};
};
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
function makeMiddleware(handlers: SocketMessageHandler[])
: Middleware<{}, RootState> {
function makeMiddleware(
handlers: SocketMessageHandler[]
): Middleware<{}, RootState> {
const handlerMap = prepareHandlers(handlers);
return storeApi => next => action => {
return (storeApi) => (next) => (action) => {
if (socketOpen.match(action)) {
if (socket !== null) {
socket.close();
@ -95,17 +97,15 @@ function makeMiddleware(handlers: SocketMessageHandler[])
socket.onmessage = onMessage(storeApi.dispatch, handlerMap);
socket.onclose = onClose(storeApi.dispatch);
socket.onopen = onOpen(storeApi.dispatch);
} else if (socketClose.match(action)) {
if (socket !== null) {
socket.close();
}
socket = null;
console.log('Websocket closed.');
console.log("Websocket closed.");
} else if (socketSendMessage.match(action)) {
console.log('Websocket sending message', action.payload);
console.log("Websocket sending message", action.payload);
socket.send(JSON.stringify(action.payload));
}


+ 7
- 7
src/modules/websocket/slice.ts View File

@ -1,26 +1,26 @@
import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SocketMessage } from "./message";
import { RootState } from '../../app/store';
import { RootState } from "../../app/store";
export enum SocketState {
Opening,
Open,
Closing,
Closed,
};
}
export interface SocketSliceState {
state: SocketState,
url?: string,
};
state: SocketState;
url?: string;
}
const initialState: SocketSliceState = {
state: SocketState.Closed,
};
export const socketSlice = createSlice({
name: 'socket',
name: "socket",
initialState,
reducers: {
socketOpen: (state, action: PayloadAction<string>) => {


+ 5
- 5
src/reducers/index.js View File

@ -7,11 +7,11 @@ import socket from "./socket";
import users from "./users";
const rootReducer = combineReducers({
login,
rooms,
socket,
users,
form
login,
rooms,
socket,
users,
form,
});
export default rootReducer;

+ 51
- 51
src/reducers/login.js View File

@ -1,75 +1,75 @@
import Immutable from "immutable";
import {
LOGIN_GET_STATUS,
SOCKET_RECEIVE_MESSAGE
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
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
status: LOGIN_STATUS_UNKNOWN,
username: undefined,
motd: undefined,
reason: undefined,
});
const initialState = new LoginRecord();
const reduceReceiveMessage = (state, message) => {
const { variant, data } = 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);
}
if (variant !== "LoginStatusResponse") {
return state;
}
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);
}
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);
}
default:
return state;
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);
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);
case SOCKET_RECEIVE_MESSAGE:
return reduceReceiveMessage(state, payload);
default:
return state;
}
default:
return state;
}
};

+ 92
- 95
src/reducers/rooms.js View File

@ -3,124 +3,121 @@ 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
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()
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);
}
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;
}
});
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;
});
}
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");
switch (type) {
case ROOM_JOIN:
return roomData.set("membership", "Joining");
case ROOM_LEAVE:
return roomData.set("membership", "Leaving");
case ROOM_LEAVE:
return roomData.set("membership", "Leaving");
case ROOM_SHOW_USERS:
return roomData.set("showUsers", true);
case ROOM_SHOW_USERS:
return roomData.set("showUsers", true);
case ROOM_HIDE_USERS:
return roomData.set("showUsers", false);
}
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;
}
});
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;
});
}
case ROOM_MESSAGE:
default:
return state;
}
};

+ 78
- 77
src/reducers/socket.js View File

@ -2,92 +2,93 @@ import Immutable from "immutable";
import * as types from "../constants/ActionTypes";
import {
STATE_OPENING, STATE_OPEN, STATE_CLOSING, STATE_CLOSED
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
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;
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;
}
};

+ 32
- 34
src/reducers/users.js View File

@ -5,49 +5,47 @@ import OrderedMap from "../utils/OrderedMap";
import { SOCKET_RECEIVE_MESSAGE } from "../constants/ActionTypes";
const UserRecord = Immutable.Record({
status: "",
averageSpeed: 0,
numDownloads: 0,
numFiles: 0,
numFolders: 0,
numFreeSlots: 0,
country: ""
status: "",
averageSpeed: 0,
numDownloads: 0,
numFiles: 0,
numFolders: 0,
numFreeSlots: 0,
country: "",
});
const initialState = OrderedMap();
const reduceUsersReceiveMessage = (users, message) => {
switch (message.variant) {
case "UserListResponse":
return users.updateAll(message.data.user_list,
(newUser, oldUser) => {
if (!oldUser) {
oldUser = UserRecord();
}
return oldUser
.set("status", newUser.status)
.set("averageSpeed", newUser.average_speed)
.set("numDownloads", newUser.num_downloads)
.set("numFiles", newUser.num_files)
.set("numFolders", newUser.num_folders)
.set("numFreeSlots", newUser.num_free_slots)
.set("country", newUser.country);
}
);
default:
return users;
}
switch (message.variant) {
case "UserListResponse":
return users.updateAll(message.data.user_list, (newUser, oldUser) => {
if (!oldUser) {
oldUser = UserRecord();
}
return oldUser
.set("status", newUser.status)
.set("averageSpeed", newUser.average_speed)
.set("numDownloads", newUser.num_downloads)
.set("numFiles", newUser.num_files)
.set("numFolders", newUser.num_folders)
.set("numFreeSlots", newUser.num_free_slots)
.set("country", newUser.country);
});
default:
return users;
}
};
const reduceUsers = (users = initialState, action) => {
switch (action.type) {
case SOCKET_RECEIVE_MESSAGE:
return reduceUsersReceiveMessage(users, action.payload);
switch (action.type) {
case SOCKET_RECEIVE_MESSAGE:
return reduceUsersReceiveMessage(users, action.payload);
default:
return users;
}
default:
return users;
}
};
export default reduceUsers;

+ 1
- 1
src/setupTests.ts View File

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import "@testing-library/jest-dom/extend-expect";

+ 129
- 127
src/styles/styles.scss View File

@ -1,236 +1,238 @@
/* Styles */
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
padding: 0 !important;
margin: 0 !important;
height: 100%;
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
padding: 0 !important;
margin: 0 !important;
height: 100%;
}
html {
padding: 0;
margin: 0;
height: 100vh;
padding: 0;
margin: 0;
height: 100vh;
}
#app, #solstice-app {
height: 100%;
margin: 0;
padding: 0;
#app,
#solstice-app {
height: 100%;
margin: 0;
padding: 0;
}
#solstice-app {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
header {
margin: 0;
padding: 1.5em 2em;
display: flex;
flex-flow: row;
justify-content: space-around;
width: 100%;
margin: 0;
padding: 1.5em 2em;
display: flex;
flex-flow: row;
justify-content: space-around;
width: 100%;
}
header h1 {
margin: 0;
margin: 0;
}
footer {
display: flex;
padding: 0.5em;
width: 100%;
box-sizing: border-box;
display: flex;
padding: 0.5em;
width: 100%;
box-sizing: border-box;
}
main {
width: 100%;
height: 85%;
margin: 0;
padding: 0;
width: 100%;
height: 85%;
margin: 0;
padding: 0;
}
#rooms-pane {
display: flex;
border: solid grey 0.1em;
height: 100%;
display: flex;
border: solid grey 0.1em;
height: 100%;
}
#room-chat {
border: solid grey 0.1em;
border: solid grey 0.1em;
}
#room-list {
flex: 1;
display: flex;
flex-flow: column;
border: solid grey 0.1em;
height: 100%;
flex: 1;
display: flex;
flex-flow: column;
border: solid grey 0.1em;
height: 100%;
}
#room-selected-pane {
height: 100%;
flex: 3;
display: flex;
flex-flow: row;
height: 100%;
flex: 3;
display: flex;
flex-flow: row;
}
#room-chat {
height: 100%;
max-width: 100%;
flex: 2;
display: flex;
flex-flow: column;
justify-content: space-between;
height: 100%;
max-width: 100%;
flex: 2;
display: flex;
flex-flow: column;
justify-content: space-between;
}
#room-user-list {
flex: 1;
display: block;
list-style: none;
margin: 0;
padding: 1em;
flex: 1;
display: block;
list-style: none;
margin: 0;
padding: 1em;
}
#room-chat-header {
flex: 1;
padding: 0.8em;
border: solid grey 0.1em;
flex: 1;
padding: 0.8em;
border: solid grey 0.1em;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
display: flex;
flex-flow: row;
justify-content: space-between;
align-items: center;
}
#room-chat-header-title {
font-size: 1.3em;
text-align: center;
flex: 1;
font-size: 1.3em;
text-align: center;
flex: 1;
}
#room-chat-header > button {
padding: 0.5em 1em;
padding: 0.5em 1em;
}
#room-chat-message-list {
display: block;
height: 83%;
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 1em;
display: block;
height: 83%;
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 1em;
overflow: auto;
overflow: auto;
list-style: none;
list-style: none;
}
.room-chat-message {
display: flex;
flex-flow: column;
align-items: flex-start;
margin-right: 3em;
display: flex;
flex-flow: column;
align-items: flex-start;
margin-right: 3em;
}
.room-chat-message-me {
align-items: flex-end;
margin-left: 3em;
margin-right: 0;
align-items: flex-end;
margin-left: 3em;
margin-right: 0;
}
.room-chat-message-user {
font-weight: bold;
color: blue;
font-weight: bold;
color: blue;
}
.room-chat-message-text {
padding: 0.5em 0.7em;
margin: 0.2em 0.5em;
border-radius: 0.8em;
background-color: lightgrey;
padding: 0.5em 0.7em;
margin: 0.2em 0.5em;
border-radius: 0.8em;
background-color: lightgrey;
}
.room-chat-message-me > .room-chat-message-text {
background-color: blue;
color: white;
background-color: blue;
color: white;
}
#room-chat-form {
width: 100%;
border: solid grey 0.1em;
width: 100%;
border: solid grey 0.1em;
}
#room-chat-form form {
width: 100%;
display: flex;
flex-flow: row;
width: 100%;
display: flex;
flex-flow: row;
}
#room-chat-form input {
flex: 1;
padding: 0.8em;
flex: 1;
padding: 0.8em;
}
#room-list-header {
flex: 1;
display: flex;
flex-flow: row;
border: solid grey 0.1em;
flex: 1;
display: flex;
flex-flow: row;
border: solid grey 0.1em;
}
#room-list-header > div {
flex: 1;
border: solid grey 0.1em;
flex: 1;
border: solid grey 0.1em;
}
#room-list ul {
display: block;
list-style: none;
padding: 0;
margin: 0;
height: 84%;
overflow-y: auto;
display: block;
list-style: none;
padding: 0;
margin: 0;
height: 84%;
overflow-y: auto;
}
.room {
display: flex;
justify-content: space-between;
color: inherit;
text-decoration: inherit;
padding: .5em 1em;
border: solid grey 0.1em;
display: flex;
justify-content: space-between;
color: inherit;
text-decoration: inherit;
padding: 0.5em 1em;
border: solid grey 0.1em;
}
.room:hover {
background: lightgrey;
background: lightgrey;
}
.room:active {
background: grey;
background: grey;
}
.room-joined {
background: lightgreen;
background: lightgreen;
}
.room-selected {
background: lightblue;
background: lightblue;
}
#login-status-pane, #socket-status-pane {
flex: 1;
#login-status-pane,
#socket-status-pane {
flex: 1;
}
#connect-form {
display: flex;
flex-flow: column;
align-items: center;
display: flex;
flex-flow: column;
align-items: center;
}

+ 29
- 27
src/utils/ControlRequest.js View File

@ -1,34 +1,36 @@
export default {
loginStatus: () => ({
variant: "LoginStatusRequest",
fields: []
}),
loginStatus: () => ({
variant: "LoginStatusRequest",
fields: [],
}),
roomJoin: (room_name) => ({
variant: "RoomJoinRequest",
fields: [room_name]
}),
roomJoin: (room_name) => ({
variant: "RoomJoinRequest",
fields: [room_name],
}),
roomLeave: (room_name) => ({
variant: "RoomLeaveRequest",
fields: [room_name]
}),
roomLeave: (room_name) => ({
variant: "RoomLeaveRequest",
fields: [room_name],
}),
roomList: () => ({
variant: "RoomListRequest",
fields: []
}),
roomList: () => ({
variant: "RoomListRequest",
fields: [],
}),
roomMessage: (room_name, message) => ({
variant: "RoomMessageRequest",
fields: [{
room_name,
message
}]
}),
roomMessage: (room_name, message) => ({
variant: "RoomMessageRequest",
fields: [
{
room_name,
message,
},
],
}),
userList: () =>({
variant: "UserListRequest",
fields: []
})
userList: () => ({
variant: "UserListRequest",
fields: [],
}),
};

+ 58
- 58
src/utils/OrderedMap.js View File

@ -5,80 +5,80 @@ import md5 from "md5";
const UPDATE_INTERVAL_MS = 5 * 60 * 1000;
const MapRecord = Immutable.Record({
byName: Immutable.OrderedMap(),
byHash: Immutable.Map(),
lastUpdated: 0
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);
}
constructor(arg) {
super(arg);
}
getByName(name) {
return this.getIn(["byName", name]);
}
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);
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);
}
updateByName(name, updater) {
return this.updateIn(["byName", name], updater);
}
getNameByHash(hash) {
return this.getIn(["byHash", hash]);
}
getNameByHash(hash) {
return this.getIn(["byHash", hash]);
}
updateAll(nameAndDataList, merger) {
const { byName } = this;
let { byHash } = this;
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;
});
// 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();
// 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()
});
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);
}
shouldUpdate() {
return (Date.now() - this.lastUpdated) > UPDATE_INTERVAL_MS;
}
return new OrderedMap({
byName: newByName,
byHash,
lastUpdated: Date.now(),
});
}
shouldUpdate() {
return Date.now() - this.lastUpdated > UPDATE_INTERVAL_MS;
}
}
export default () => new OrderedMap();

+ 7
- 8
src/utils/propTypeRequiredWrapper.js View File

@ -1,17 +1,16 @@
const checkRequiredThenValidate = (validator) =>
(props, propName, componentName, location) =>
{
const checkRequiredThenValidate =
(validator) => (props, propName, componentName, location) => {
if (props[propName] != null) {
return validator(props, propName, componentName, location);
return validator(props, propName, componentName, location);
}
return new Error(
`Required prop \`${propName}\` was not specified in ` +
`Required prop \`${propName}\` was not specified in ` +
`\`${componentName}\`.`
);
};
};
export default (validator) => {
validator.isRequired = checkRequiredThenValidate(validator);
return validator;
validator.isRequired = checkRequiredThenValidate(validator);
return validator;
};

+ 12
- 13
src/utils/propTypeSymbol.js View File

@ -2,19 +2,18 @@ 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\``
);
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);

Loading…
Cancel
Save