mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-10 16:24:45 -07:00
Webatrice P.O.C. (#3854)
* port webclient POC into react shell * Abstract websocket messaging behind redux store * refactor architecture * add rooms store * introduce application service layer and login form * display room messages * implement roomSay * improve Room view styling * display room games * improve gameList update logic * hide protected games * improve game update logic * move mapping to earlier lifecycle hook * add autoscroll to bottom * tabs to spaces, refresh guard * implement server joins/leaves * show users in room * add material-ui to build * refactor, add room joins/leaves to store and render * begin using Material UI components * fix spectatorsCount * remove unused package * improve Server and Room styling * fix scroll context * route on room join * refactor room path * add auth guard * refactor authGuard export * add missing files * clear store on disconnect, add logout button to Account view * fix disconnect handling * Safari fixes * organize current todos * improve login page and server status tracking * improve login page * introduce sorting arch, refine reducers, begin viewLogHistory * audit fix for handlebars * implement moderator log view * comply with code style rules * remove original POC from codebase * add missing semi * minor improvements, begin registration functionality * retry as ws when wss fails additionally, dont mutate the default options when connecting * retain user/pass in WebClient.options for login * take protocol off of options, make it a connect param that defaults to wss * cleanup server page styling * match wss logic with desktop client * add virtual scroll component, add context menu to UserDisplay * revert VirtualTable on messages * improve styling for Room view * add routing to Player view * increase tooltip delay * begin implementing Account view * disable app level contextMenu * implement buddy/ignore list management * fix gitignore Co-authored-by: Jay Letto <jeremy.letto@merrillcorp.com> Co-authored-by: skwerlman <skwerlman@users.noreply.github.com> Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
parent
d5b36e8b8a
commit
0457e65751
152 changed files with 19573 additions and 1071 deletions
49
webclient/src/AppShell/Account/Account.css
Normal file
49
webclient/src/AppShell/Account/Account.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
.account {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.account-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
|
||||
.account-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.account-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.account-details__actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-details p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.account-details button {
|
||||
margin-top: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.account-details img {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
122
webclient/src/AppShell/Account/Account.tsx
Normal file
122
webclient/src/AppShell/Account/Account.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import UserDisplay from "AppShell/common/components/UserDisplay/UserDisplay";
|
||||
import VirtualList from "AppShell/common/components/VirtualList/VirtualList";
|
||||
|
||||
import { AuthenticationService, SessionService } from "AppShell/common/services";
|
||||
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
|
||||
import { Selectors } from "store/server";
|
||||
|
||||
import { User } from 'types';
|
||||
|
||||
import AddToBuddies from './AddToBuddies/AddToBuddies';
|
||||
import AddToIgnore from './AddToIgnore/AddToIgnore';
|
||||
|
||||
import "./Account.css";
|
||||
|
||||
class Account extends Component<AccountProps> {
|
||||
handleAddToBuddies({ userName }) {
|
||||
SessionService.addToBuddyList(userName);
|
||||
}
|
||||
|
||||
handleAddToIgnore({ userName }) {
|
||||
SessionService.addToIgnoreList(userName);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.props);
|
||||
|
||||
const { buddyList, ignoreList, serverName, serverVersion, user } = this.props;
|
||||
const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user;
|
||||
|
||||
var url = URL.createObjectURL(new Blob([avatarBmp], {'type': 'image/png'}));
|
||||
|
||||
return (
|
||||
<div className="account">
|
||||
<AuthGuard />
|
||||
<div className="account-column">
|
||||
<Paper className="account-list">
|
||||
<div className="">
|
||||
Buddies Online: ?/{buddyList.length}
|
||||
</div>
|
||||
<VirtualList
|
||||
itemKey={(index, data) => buddyList[index].name }
|
||||
items={ buddyList.map(user => (
|
||||
<ListItem button dense>
|
||||
<UserDisplay user={user} />
|
||||
</ListItem>
|
||||
) ) }
|
||||
/>
|
||||
<div className="" style={{borderTop: "1px solid"}}>
|
||||
<AddToBuddies onSubmit={this.handleAddToBuddies} />
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
<div className="account-column">
|
||||
<Paper className="account-list overflow-scroll">
|
||||
<div className="">
|
||||
Ignored Users Online: ?/{ignoreList.length}
|
||||
</div>
|
||||
<VirtualList
|
||||
itemKey={(index, data) => ignoreList[index].name }
|
||||
items={ ignoreList.map(user => (
|
||||
<ListItem button dense>
|
||||
<UserDisplay user={user} />
|
||||
</ListItem>
|
||||
) ) }
|
||||
/>
|
||||
<div className="" style={{borderTop: "1px solid"}}>
|
||||
<AddToIgnore onSubmit={this.handleAddToIgnore} />
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
<div className="account-column overflow-scroll">
|
||||
<Paper className="account-details" style={{margin: "0 0 5px 0"}}>
|
||||
<img src={url} alt={name} />
|
||||
<p><strong>{name}</strong></p>
|
||||
<p>Location: ({country.toUpperCase()})</p>
|
||||
<p>User Level: {userLevel}</p>
|
||||
<p>Account Age: {accountageSecs}</p>
|
||||
<p>Real Name: {realName}</p>
|
||||
<div className="account-details__actions">
|
||||
<Button size="small" color="primary" variant="contained">Edit</Button>
|
||||
<Button size="small" color="primary" variant="contained">Change<br />Password</Button>
|
||||
<Button size="small" color="primary" variant="contained">Change<br />Avatar</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
<Paper className="account-details">
|
||||
<p>Server Name: {serverName}</p>
|
||||
<p>Server Version: {serverVersion}</p>
|
||||
<Button color="primary" variant="contained" onClick={() => AuthenticationService.disconnect()}>Disconnect</Button>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface AccountProps {
|
||||
buddyList: User[];
|
||||
ignoreList: User[];
|
||||
serverName: string;
|
||||
serverVersion: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
buddyList: Selectors.getBuddyList(state),
|
||||
ignoreList: Selectors.getIgnoreList(state),
|
||||
serverName: Selectors.getName(state),
|
||||
serverVersion: Selectors.getVersion(state),
|
||||
user: Selectors.getUser(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Account);
|
||||
18
webclient/src/AppShell/Account/AddToBuddies/AddToBuddies.tsx
Normal file
18
webclient/src/AppShell/Account/AddToBuddies/AddToBuddies.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Form, reduxForm } from "redux-form"
|
||||
|
||||
import InputAction from 'AppShell/common/components/InputAction/InputAction';
|
||||
|
||||
const AddToBuddies = ({ handleSubmit }) => (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputAction action="Add" label="Add to Buddies" name="userName" />
|
||||
</Form>
|
||||
);
|
||||
|
||||
const propsMap = {
|
||||
form: "addToBuddies"
|
||||
};
|
||||
|
||||
export default connect()(reduxForm(propsMap)(AddToBuddies));
|
||||
18
webclient/src/AppShell/Account/AddToIgnore/AddToIgnore.tsx
Normal file
18
webclient/src/AppShell/Account/AddToIgnore/AddToIgnore.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Form, reduxForm } from "redux-form"
|
||||
|
||||
import InputAction from 'AppShell/common/components/InputAction/InputAction';
|
||||
|
||||
const AddToIgnore = ({ handleSubmit }) => (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputAction action="Add" label="Add to Ignore" name="userName" />
|
||||
</Form>
|
||||
);
|
||||
|
||||
const propsMap = {
|
||||
form: "addToIgnore"
|
||||
};
|
||||
|
||||
export default connect()(reduxForm(propsMap)(AddToIgnore));
|
||||
10
webclient/src/AppShell/AppShell.css
Normal file
10
webclient/src/AppShell/AppShell.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.AppShell,
|
||||
.AppShell-routes {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.AppShell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 768px;
|
||||
}
|
||||
39
webclient/src/AppShell/AppShell.tsx
Normal file
39
webclient/src/AppShell/AppShell.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { MemoryRouter as Router } from "react-router-dom";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
|
||||
import { store } from "store";
|
||||
|
||||
import "./AppShell.css";
|
||||
import Routes from "./AppShellRoutes";
|
||||
import Header from "./Header/Header";
|
||||
|
||||
|
||||
class AppShell extends Component {
|
||||
componentDidMount() {
|
||||
// @TODO (1)
|
||||
window.onbeforeunload = () => true;
|
||||
}
|
||||
|
||||
handleContextMenu(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<CssBaseline />
|
||||
<div className="AppShell" onContextMenu={this.handleContextMenu}>
|
||||
<Router>
|
||||
<Header />
|
||||
<Routes />
|
||||
</Router>
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppShell;
|
||||
29
webclient/src/AppShell/AppShellRoutes.tsx
Normal file
29
webclient/src/AppShell/AppShellRoutes.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import React from "react";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
|
||||
import { RouteEnum } from "./common/types";
|
||||
|
||||
import Account from "./Account/Account";
|
||||
import Decks from "./Decks/Decks";
|
||||
import Game from "./Game/Game";
|
||||
import Logs from "./Logs/Logs";
|
||||
import Player from "./Player/Player";
|
||||
import Room from "./Room/Room";
|
||||
import Server from "./Server/Server";
|
||||
|
||||
const Routes = () => (
|
||||
<div className="AppShell-routes overflow-scroll">
|
||||
<Switch>
|
||||
<Route path={RouteEnum.ACCOUNT} render={() => <Account />} />
|
||||
<Route path={RouteEnum.DECKS} render={() => <Decks />} />
|
||||
<Route path={RouteEnum.GAME} render={() => <Game />} />
|
||||
<Route path={RouteEnum.LOGS} render={() => <Logs />} />
|
||||
<Route path={RouteEnum.PLAYER} render={() => <Player />} />
|
||||
{<Route path={RouteEnum.ROOM} render={() => <Room />} />}
|
||||
<Route path={RouteEnum.SERVER} render={() => <Server />} />
|
||||
<Redirect from="/" to={RouteEnum.SERVER} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Routes;
|
||||
0
webclient/src/AppShell/Decks/Decks.css
Normal file
0
webclient/src/AppShell/Decks/Decks.css
Normal file
19
webclient/src/AppShell/Decks/Decks.tsx
Normal file
19
webclient/src/AppShell/Decks/Decks.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
|
||||
import "./Decks.css";
|
||||
|
||||
class Decks extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<AuthGuard />
|
||||
<span>"Decks"</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Decks;
|
||||
0
webclient/src/AppShell/Game/Game.css
Normal file
0
webclient/src/AppShell/Game/Game.css
Normal file
19
webclient/src/AppShell/Game/Game.tsx
Normal file
19
webclient/src/AppShell/Game/Game.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
|
||||
import "./Game.css";
|
||||
|
||||
class Game extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<AuthGuard />
|
||||
<span>"Game"</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Game;
|
||||
77
webclient/src/AppShell/Header/Header.css
Normal file
77
webclient/src/AppShell/Header/Header.css
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
.Header {
|
||||
}
|
||||
|
||||
.Header__logo {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Header__logo img {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.Header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Header-serverDetails {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.Header-nav {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.Header-nav__items {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.Header-nav__item {
|
||||
list-style: none;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.Header-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Header-account__name {
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Header-account__indicator {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background: red;
|
||||
border: 2px solid;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.temp-subnav__rooms {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.temp-chip {
|
||||
margin-left: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.temp-chip > div {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
119
webclient/src/AppShell/Header/Header.tsx
Normal file
119
webclient/src/AppShell/Header/Header.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { NavLink, withRouter, generatePath } from "react-router-dom";
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import Chip from "@material-ui/core/Chip";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
import * as _ from "lodash";
|
||||
|
||||
import { Selectors as RoomsSelectors } from "store/rooms";
|
||||
import { Selectors as ServerSelectors } from "store/server";
|
||||
import { Room, User } from "types";
|
||||
|
||||
import { AuthenticationService } from "AppShell/common/services";
|
||||
import { RouteEnum } from "AppShell/common/types";
|
||||
|
||||
import "./Header.css";
|
||||
import logo from "./logo.png";
|
||||
|
||||
class Header extends Component<HeaderProps> {
|
||||
componentDidUpdate(prevProps) {
|
||||
const currentRooms = this.props.joinedRooms;
|
||||
const previousRooms = prevProps.joinedRooms;
|
||||
|
||||
if (currentRooms > previousRooms) {
|
||||
const { roomId } = _.difference(currentRooms, previousRooms)[0];
|
||||
this.props.history.push(generatePath(RouteEnum.ROOM, { roomId }));
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { joinedRooms, server, state, user } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/*<header className="Header">*/}
|
||||
<AppBar position="static">
|
||||
<Toolbar variant="dense">
|
||||
<NavLink to={RouteEnum.SERVER} className="Header__logo">
|
||||
<img src={logo} alt="logo" />
|
||||
</NavLink>
|
||||
{ AuthenticationService.isConnected(state) && (
|
||||
<div className="Header-content">
|
||||
<nav className="Header-nav">
|
||||
<ul className="Header-nav__items">
|
||||
{
|
||||
AuthenticationService.isModerator(user) && (
|
||||
<li className="Header-nav__item">
|
||||
<NavLink to={RouteEnum.LOGS}>
|
||||
<button>Logs</button>
|
||||
</NavLink>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
<li className="Header-nav__item">
|
||||
<NavLink to={RouteEnum.SERVER} className="plain-link">
|
||||
Server ({server})
|
||||
</NavLink>
|
||||
</li>
|
||||
<NavLink to={RouteEnum.ACCOUNT} className="plain-link">
|
||||
<div className="Header-account">
|
||||
<span className="Header-account__name">
|
||||
{user.name}
|
||||
</span>
|
||||
<span className="Header-account__indicator"></span>
|
||||
</div>
|
||||
</NavLink>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
) }
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<div className="temp-subnav">
|
||||
{
|
||||
!!joinedRooms.length && (
|
||||
<Rooms rooms={joinedRooms} />
|
||||
)
|
||||
}
|
||||
<div className="temp-subnav__games">
|
||||
</div>
|
||||
<div className="temp-subnav__chats">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Rooms = props => (
|
||||
<div className="temp-subnav__rooms">
|
||||
<span>Rooms: </span>
|
||||
{
|
||||
_.reduce(props.rooms, (rooms, { name, roomId}) => {
|
||||
rooms.push(
|
||||
<NavLink to={generatePath(RouteEnum.ROOM, { roomId })} className="temp-chip" key={roomId}>
|
||||
<Chip label={name} color="primary" />
|
||||
</NavLink>
|
||||
);
|
||||
return rooms;
|
||||
}, [])
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface HeaderProps {
|
||||
state: number;
|
||||
server: string;
|
||||
user: User;
|
||||
joinedRooms: Room[];
|
||||
history: any;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
state: ServerSelectors.getState(state),
|
||||
server: ServerSelectors.getName(state),
|
||||
user: ServerSelectors.getUser(state),
|
||||
joinedRooms: RoomsSelectors.getJoinedRooms(state)
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Header));
|
||||
BIN
webclient/src/AppShell/Header/logo.png
Normal file
BIN
webclient/src/AppShell/Header/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
3
webclient/src/AppShell/Logs/LogResults/LogResults.css
Normal file
3
webclient/src/AppShell/Logs/LogResults/LogResults.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.log-results {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
122
webclient/src/AppShell/Logs/LogResults/LogResults.tsx
Normal file
122
webclient/src/AppShell/Logs/LogResults/LogResults.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React from "react";
|
||||
import * as _ from "lodash";
|
||||
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import Box from "@material-ui/core/Box";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import Tab from "@material-ui/core/Tab";
|
||||
import Tabs from "@material-ui/core/Tabs";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import "./LogResults.css";
|
||||
|
||||
const LogResults = (props) => {
|
||||
const { logs } = props;
|
||||
|
||||
const hasRoomLogs = logs.room && logs.room.length;
|
||||
const hasGameLogs = logs.game && logs.game.length;
|
||||
const hasChatLogs = logs.chat && logs.chat.length;
|
||||
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const headerCells = [
|
||||
{
|
||||
label: "Time"
|
||||
},
|
||||
{
|
||||
label: "Sender Name"
|
||||
},
|
||||
{
|
||||
label: "Sender IP"
|
||||
},
|
||||
{
|
||||
label: "Message"
|
||||
},
|
||||
{
|
||||
label: "Target ID"
|
||||
},
|
||||
{
|
||||
label: "Target Name"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppBar position="static">
|
||||
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
|
||||
<Tab label={"Rooms" + (hasRoomLogs ? ` [${logs.room.length}]` : "")} {...a11yProps(0)} />
|
||||
<Tab label={"Games" + (hasGameLogs ? ` [${logs.game.length}]` : "")} {...a11yProps(1)} />
|
||||
<Tab label={"Chats" + (hasChatLogs ? ` [${logs.chat.length}]` : "")} {...a11yProps(2)} />
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
<TabPanel value={value} index={0}>
|
||||
<Results logs={logs.room} headerCells={headerCells} />
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={1}>
|
||||
<Results logs={logs.game} headerCells={headerCells} />
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={2}>
|
||||
<Results logs={logs.chat} headerCells={headerCells} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const a11yProps = index => {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
"aria-controls": `simple-tabpanel-${index}`,
|
||||
};
|
||||
};
|
||||
|
||||
const TabPanel = ({ children, value, index, ...other }) => {
|
||||
return (
|
||||
<Typography
|
||||
component="div"
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
<Box>{children}</Box>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Results = ({headerCells, logs}) => (
|
||||
<Paper className="log-results">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{ _.map(headerCells, ({ label }) => (
|
||||
<TableCell key={label}>{label}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ _.map(logs, ({ time, senderName, senderIp, message, targetId, targetName }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{time}</TableCell>
|
||||
<TableCell>{senderName}</TableCell>
|
||||
<TableCell>{senderIp}</TableCell>
|
||||
<TableCell>{message}</TableCell>
|
||||
<TableCell>{targetId}</TableCell>
|
||||
<TableCell>{targetName}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export default LogResults;
|
||||
14
webclient/src/AppShell/Logs/Logs.css
Normal file
14
webclient/src/AppShell/Logs/Logs.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.moderator-logs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.moderator-logs__form {
|
||||
width: 40%;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.moderator-logs__results {
|
||||
width: 100%;
|
||||
}
|
||||
102
webclient/src/AppShell/Logs/Logs.tsx
Normal file
102
webclient/src/AppShell/Logs/Logs.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import * as _ from "lodash";
|
||||
|
||||
import { Dispatch, Selectors, ServerStateLogs } from "store/server"
|
||||
|
||||
import { ModeratorService } from "AppShell/common/services";
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
import ModGuard from "AppShell/common/guards/ModGuard";
|
||||
|
||||
import LogResults from "./LogResults/LogResults";
|
||||
import SearchForm from "./SearchForm/SearchForm";
|
||||
|
||||
import "./Logs.css";
|
||||
|
||||
class Logs extends Component<LogsTypes> {
|
||||
MAXIMUM_RESULTS = 1000;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dispatch.clearLogs();
|
||||
}
|
||||
|
||||
onSubmit(fields) {
|
||||
const trimmedFields: any = this.trimFields(fields);
|
||||
|
||||
const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields;
|
||||
|
||||
const required = _.filter({
|
||||
userName, ipAddress, gameName, gameId, message
|
||||
}, field => field);
|
||||
|
||||
if (logLocation) {
|
||||
trimmedFields.logLocation = this.flattenLogLocations(logLocation);
|
||||
}
|
||||
|
||||
trimmedFields.maximumResults = this.MAXIMUM_RESULTS;
|
||||
|
||||
if (_.size(required)) {
|
||||
ModeratorService.viewLogHistory(trimmedFields);
|
||||
} else {
|
||||
// @TODO use yet-to-be-implemented banner/alert
|
||||
}
|
||||
}
|
||||
|
||||
private trimFields(fields) {
|
||||
return _.reduce(fields, (obj, field, key) => {
|
||||
if (typeof field === "string") {
|
||||
const trimmed = _.trim(field);
|
||||
|
||||
if (!!trimmed) {
|
||||
obj[key] = trimmed;
|
||||
}
|
||||
} else {
|
||||
obj[key] = field;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private flattenLogLocations(logLocations) {
|
||||
return _.reduce(logLocations, (arr, loc, key) => {
|
||||
arr.push(key);
|
||||
return arr;
|
||||
}, [])
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="moderator-logs overflow-scroll">
|
||||
<AuthGuard />
|
||||
<ModGuard />
|
||||
|
||||
<div className="moderator-logs__form">
|
||||
<SearchForm onSubmit={this.onSubmit} />
|
||||
</div>
|
||||
|
||||
<div className="moderator-logs__results">
|
||||
<LogResults logs={this.props.logs} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface LogsTypes {
|
||||
logs: ServerStateLogs
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
logs: Selectors.getLogs(state)
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Logs));
|
||||
35
webclient/src/AppShell/Logs/SearchForm/SearchForm.css
Normal file
35
webclient/src/AppShell/Logs/SearchForm/SearchForm.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.log-search {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
hr.MuiDivider-root {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.log-search__form {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.log-search__form-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.log-search__form-item.log-location {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.log-search__form-item.log-location .checkbox-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log-search__form-item.log-location .checkbox-field__box {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.log-search__form-submit.MuiButton-root {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
68
webclient/src/AppShell/Logs/SearchForm/SearchForm.tsx
Normal file
68
webclient/src/AppShell/Logs/SearchForm/SearchForm.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Form, Field, InjectedFormProps, reduxForm } from "redux-form"
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import InputField from "AppShell/common/components/InputField/InputField";
|
||||
import CheckboxField from "AppShell/common/components/CheckboxField/CheckboxField";
|
||||
|
||||
import "./SearchForm.css";
|
||||
|
||||
class SearchForm extends Component<InjectedFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Paper className="log-search">
|
||||
<Form className="log-search__form" onSubmit={this.props.handleSubmit}>
|
||||
<div className="log-search__form-item">
|
||||
<Field label="Username" name="userName" component={InputField} />
|
||||
</div>
|
||||
<div className="log-search__form-item">
|
||||
<Field label="IP Address" name="ipAddress" component={InputField} />
|
||||
</div>
|
||||
<div className="log-search__form-item">
|
||||
<Field label="Game Name" name="gameName" component={InputField} />
|
||||
</div>
|
||||
<div className="log-search__form-item">
|
||||
<Field label="GameID" name="gameId" component={InputField} />
|
||||
</div>
|
||||
<div className="log-search__form-item">
|
||||
<Field label="Message" name="message" component={InputField} />
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-search__form-item log-location">
|
||||
<Field label="Rooms" name="logLocation.room" component={CheckboxField} />
|
||||
<Field label="Games" name="logLocation.game" component={CheckboxField} />
|
||||
<Field label="Chats" name="logLocation.chat" component={CheckboxField} />
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-search__form-item">
|
||||
<span>Date Range: Coming Soon</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-search__form-item">
|
||||
<span>Maximum Results: 1000</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<Button className="log-search__form-submit" color="primary" variant="contained" type="submit">
|
||||
Search Logs
|
||||
</Button>
|
||||
</Form>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const propsMap = {
|
||||
form: "logs"
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(reduxForm(propsMap)(SearchForm));
|
||||
|
||||
0
webclient/src/AppShell/Player/Player.css
Normal file
0
webclient/src/AppShell/Player/Player.css
Normal file
19
webclient/src/AppShell/Player/Player.tsx
Normal file
19
webclient/src/AppShell/Player/Player.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
|
||||
import "./Player.css";
|
||||
|
||||
class Player extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<AuthGuard />
|
||||
<span>"Player"</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Player;
|
||||
30
webclient/src/AppShell/Room/Games/Games.css
Normal file
30
webclient/src/AppShell/Room/Games/Games.css
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
.games {
|
||||
}
|
||||
|
||||
.games-header,
|
||||
.game {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.games-header__cell {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.games-header__label,
|
||||
.game__detail {
|
||||
width: 10%;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.games-header__label.description,
|
||||
.game__detail.description {
|
||||
width: 20%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.games-header__label.creator,
|
||||
.game__detail.creator {
|
||||
width: 20%;
|
||||
}
|
||||
144
webclient/src/AppShell/Room/Games/Games.tsx
Normal file
144
webclient/src/AppShell/Room/Games/Games.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as _ from "lodash";
|
||||
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import TableSortLabel from "@material-ui/core/TableSortLabel";
|
||||
import Tooltip from "@material-ui/core/Tooltip";
|
||||
|
||||
// import { RoomsService } from "AppShell/common/services";
|
||||
|
||||
import { SortUtil } from "store/common";
|
||||
import { Dispatch, Selectors } from "store/rooms";
|
||||
import UserDisplay from "AppShell/common/components/UserDisplay/UserDisplay";
|
||||
|
||||
import "./Games.css";
|
||||
|
||||
// @TODO run interval to update timeSinceCreated
|
||||
class Games extends Component<GamesProps> {
|
||||
private headerCells = [
|
||||
{
|
||||
label: "Age",
|
||||
field: "startTime"
|
||||
},
|
||||
{
|
||||
label: "Description",
|
||||
field: "description"
|
||||
},
|
||||
{
|
||||
label: "Creator",
|
||||
field: "creatorInfo.name"
|
||||
},
|
||||
{
|
||||
label: "Type",
|
||||
field: "gameType"
|
||||
},
|
||||
{
|
||||
label: "Restrictions",
|
||||
// field: "?"
|
||||
},
|
||||
{
|
||||
label: "Players",
|
||||
// field: ["maxPlayers", "playerCount"]
|
||||
},
|
||||
{
|
||||
label: "Spectators",
|
||||
field: "spectatorsCount"
|
||||
},
|
||||
];
|
||||
|
||||
handleSort(sortByField) {
|
||||
const { room: { roomId }, sortBy } = this.props;
|
||||
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
|
||||
Dispatch.sortGames(roomId, field, order);
|
||||
}
|
||||
|
||||
private isUnavailableGame({ started, maxPlayers, playerCount }) {
|
||||
return !started && playerCount < maxPlayers;
|
||||
}
|
||||
|
||||
private isPasswordProtectedGame({ withPassword }) {
|
||||
return !withPassword;
|
||||
}
|
||||
|
||||
private isBuddiesOnlyGame({ onlyBuddies }) {
|
||||
return !onlyBuddies;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { room, sortBy } = this.props;
|
||||
|
||||
const games = room.gameList.filter(game => (
|
||||
this.isUnavailableGame(game) &&
|
||||
this.isPasswordProtectedGame(game) &&
|
||||
this.isBuddiesOnlyGame(game)
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="games">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{ _.map(this.headerCells, ({ label, field }) => {
|
||||
const active = field === sortBy.field;
|
||||
const order = sortBy.order.toLowerCase();
|
||||
const sortDirection = active ? order : false;
|
||||
|
||||
return (
|
||||
<TableCell sortDirection={sortDirection} key={label}>
|
||||
{!field ? label : (
|
||||
<TableSortLabel
|
||||
active={active}
|
||||
direction={order}
|
||||
onClick={() => this.handleSort(field)}
|
||||
>
|
||||
{label}
|
||||
</TableSortLabel>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
|
||||
<TableRow key={gameId}>
|
||||
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
|
||||
<TableCell className="games-header__cell">
|
||||
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
|
||||
<div className="single-line-ellipsis">
|
||||
{description}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="games-header__cell">
|
||||
<UserDisplay user={ creatorInfo } />
|
||||
</TableCell>
|
||||
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
|
||||
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
|
||||
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
|
||||
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface GamesProps {
|
||||
room: any;
|
||||
sortBy: any;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
sortBy: Selectors.getSortGamesBy(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Games);
|
||||
17
webclient/src/AppShell/Room/Messages/Messages.css
Normal file
17
webclient/src/AppShell/Room/Messages/Messages.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.messages {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 5px 0;
|
||||
margin: 2px 0;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.message:last-of-type {
|
||||
border: 0;
|
||||
}
|
||||
31
webclient/src/AppShell/Room/Messages/Messages.tsx
Normal file
31
webclient/src/AppShell/Room/Messages/Messages.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
|
||||
import "./Messages.css";
|
||||
|
||||
const Messages = ({ messages }) => (
|
||||
<div className="messages">
|
||||
{
|
||||
messages && messages.map(({ message, messageType, timeOf, timeReceived }) => (
|
||||
<div className="message" key={timeReceived}>
|
||||
<div className="message__detail">{ParsedMessage(message)}</div>
|
||||
</div>
|
||||
) )
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const ParsedMessage = (message) => {
|
||||
const name = message.match("^[^:]+:");
|
||||
|
||||
if (name && name.length) {
|
||||
message = message.slice(name[0].length, message.length);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<strong>{name}</strong>
|
||||
{message}
|
||||
</div>
|
||||
};
|
||||
|
||||
export default Messages;
|
||||
39
webclient/src/AppShell/Room/Room.css
Normal file
39
webclient/src/AppShell/Room/Room.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.room-view,
|
||||
.room-view__games,
|
||||
.room-view__messages,
|
||||
.room-view__messages-content,
|
||||
.room-view__side {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.room-view__messages,
|
||||
.room-view__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.room-view__messages-sayMessage {
|
||||
width: 100%;
|
||||
margin: 10px auto 2px;
|
||||
}
|
||||
|
||||
.room-view__side-label {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.room-view__side-list,
|
||||
.room-view__side-list .room-view__side-list__item {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.room-view__side-list .room-view__side-list__item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.room-view__side-list .room-view__side-list__item .user-display__details {
|
||||
padding: 0 10px;
|
||||
}
|
||||
103
webclient/src/AppShell/Room/Room.tsx
Normal file
103
webclient/src/AppShell/Room/Room.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter /*, RouteComponentProps */ } from "react-router-dom";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import { RoomsStateMessages, RoomsStateRooms, Selectors } from "store/rooms";
|
||||
|
||||
import AuthGuard from "AppShell/common/guards/AuthGuard";
|
||||
import { RoomsService } from "AppShell/common/services";
|
||||
|
||||
import ScrollToBottomOnChanges from "AppShell/common/components/ScrollToBottomOnChanges/ScrollToBottomOnChanges";
|
||||
import ThreePaneLayout from "AppShell/common/components/ThreePaneLayout/ThreePaneLayout";
|
||||
import UserDisplay from "AppShell/common/components/UserDisplay/UserDisplay";
|
||||
import VirtualList from "AppShell/common/components/VirtualList/VirtualList";
|
||||
|
||||
import Games from "./Games/Games";
|
||||
import Messages from "./Messages/Messages";
|
||||
import SayMessage from "./SayMessage/SayMessage";
|
||||
|
||||
import "./Room.css";
|
||||
|
||||
// @TODO (3)
|
||||
class Room extends Component<any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleRoomSay = this.handleRoomSay.bind(this);
|
||||
}
|
||||
|
||||
handleRoomSay({ message }) {
|
||||
if (message) {
|
||||
const { roomId } = this.props.match.params;
|
||||
RoomsService.roomSay(roomId, message);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match, rooms} = this.props;
|
||||
const { roomId } = match.params;
|
||||
const room = rooms[roomId];
|
||||
|
||||
const messages = this.props.messages[roomId];
|
||||
const users = room.userList;
|
||||
|
||||
return (
|
||||
<div className="room-view">
|
||||
<AuthGuard />
|
||||
<ThreePaneLayout
|
||||
fixedHeight
|
||||
|
||||
top={(
|
||||
<Paper className="room-view__games overflow-scroll">
|
||||
<Games room={room} />
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
bottom={(
|
||||
<div className="room-view__messages">
|
||||
<Paper className="room-view__messages-content overflow-scroll">
|
||||
<ScrollToBottomOnChanges changes={messages} content={(
|
||||
<Messages messages={messages} />
|
||||
)} />
|
||||
</Paper>
|
||||
<Paper className="room-view__messages-sayMessage">
|
||||
<SayMessage onSubmit={this.handleRoomSay} />
|
||||
</Paper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
side={(
|
||||
<Paper className="room-view__side overflow-scroll">
|
||||
<div className="room-view__side-label">
|
||||
Users in this room: {users.length}
|
||||
</div>
|
||||
<VirtualList
|
||||
className="room-view__side-list"
|
||||
itemKey={(index, data) => users[index].name }
|
||||
items={ users.map(user => (
|
||||
<ListItem button className="room-view__side-list__item">
|
||||
<UserDisplay user={user} />
|
||||
</ListItem>
|
||||
) ) }
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface RoomProps {
|
||||
messages: RoomsStateMessages;
|
||||
rooms: RoomsStateRooms;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
messages: Selectors.getMessages(state),
|
||||
rooms: Selectors.getRooms(state)
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Room));
|
||||
18
webclient/src/AppShell/Room/SayMessage/SayMessage.tsx
Normal file
18
webclient/src/AppShell/Room/SayMessage/SayMessage.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Form, reduxForm } from "redux-form"
|
||||
|
||||
import InputAction from 'AppShell/common/components/InputAction/InputAction';
|
||||
|
||||
const SayMessage = ({ handleSubmit }) => (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputAction action="Say" label="Chat" name="message" />
|
||||
</Form>
|
||||
);
|
||||
|
||||
const propsMap = {
|
||||
form: "sayMessage"
|
||||
};
|
||||
|
||||
export default connect()(reduxForm(propsMap)(SayMessage));
|
||||
14
webclient/src/AppShell/Server/ConnectForm/ConnectForm.css
Normal file
14
webclient/src/AppShell/Server/ConnectForm/ConnectForm.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.connectForm {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.connectForm-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.connectForm-submit.MuiButton-root {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
49
webclient/src/AppShell/Server/ConnectForm/ConnectForm.tsx
Normal file
49
webclient/src/AppShell/Server/ConnectForm/ConnectForm.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Form, Field, InjectedFormProps, reduxForm } from "redux-form"
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
|
||||
import InputField from "AppShell/common/components/InputField/InputField";
|
||||
|
||||
import "./ConnectForm.css";
|
||||
|
||||
class ConnectForm extends Component<InjectedFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Form className="connectForm" onSubmit={this.props.handleSubmit}>
|
||||
<div className="connectForm-item">
|
||||
<Field label="Host" name="host" component={InputField} />
|
||||
</div>
|
||||
<div className="connectForm-item">
|
||||
<Field label="Port" name="port" component={InputField} />
|
||||
</div>
|
||||
<div className="connectForm-item">
|
||||
<Field label="User" name="user" component={InputField} autoComplete="username" />
|
||||
</div>
|
||||
<div className="connectForm-item">
|
||||
<Field label="Pass" name="pass" type="password" component={InputField} autoComplete="current-password" />
|
||||
</div>
|
||||
<Button className="connectForm-submit" color="primary" variant="contained" type="submit">
|
||||
Connect
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const propsMap = {
|
||||
form: "connect"
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
initialValues: {
|
||||
// host: "mtg.tetrarch.co/servatrice",
|
||||
// port: "443"
|
||||
host: "server.cockatrice.us",
|
||||
port: "4748"
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(reduxForm(propsMap)(ConnectForm));
|
||||
14
webclient/src/AppShell/Server/RegisterForm/RegisterForm.css
Normal file
14
webclient/src/AppShell/Server/RegisterForm/RegisterForm.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.registerForm {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.registerForm-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.registerForm-submit.MuiButton-root {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
58
webclient/src/AppShell/Server/RegisterForm/RegisterForm.tsx
Normal file
58
webclient/src/AppShell/Server/RegisterForm/RegisterForm.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Form, Field, InjectedFormProps, reduxForm } from 'redux-form'
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
import InputField from 'AppShell/common/components/InputField/InputField';
|
||||
|
||||
import './RegisterForm.css';
|
||||
|
||||
class RegisterForm extends Component<InjectedFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Form className="registerForm" onSubmit={this.props.handleSubmit} autoComplete="off">
|
||||
<div className="registerForm-item">
|
||||
<Field label="Host" name="host" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Port" name="port" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Player Name" name="userName" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Password" name="password" type="password" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Password (again)" name="passwordConfirm" type="password" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Email" name="email" type="email" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Email (again)" name="emailConfirm" type="email" component={InputField} />
|
||||
</div>
|
||||
<div className="registerForm-item">
|
||||
<Field label="Real Name" name="realName" component={InputField} />
|
||||
</div>
|
||||
<Button className="registerForm-submit" color="primary" variant="contained" type="submit">
|
||||
Register
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const propsMap = {
|
||||
form: 'register'
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
initialValues: {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(reduxForm(propsMap)(RegisterForm));
|
||||
26
webclient/src/AppShell/Server/Rooms/Rooms.css
Normal file
26
webclient/src/AppShell/Server/Rooms/Rooms.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
.rooms {
|
||||
}
|
||||
|
||||
.rooms-header,
|
||||
.room {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.rooms-header__label,
|
||||
.room__detail {
|
||||
width: 10%;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.rooms-header__label.name,
|
||||
.room__detail.name {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.rooms-header__label.description,
|
||||
.room__detail.description {
|
||||
width: 30%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
61
webclient/src/AppShell/Server/Rooms/Rooms.tsx
Normal file
61
webclient/src/AppShell/Server/Rooms/Rooms.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import * as _ from "lodash";
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
|
||||
import { RoomsService } from "AppShell/common/services";
|
||||
import { RouteEnum } from "AppShell/common/types";
|
||||
|
||||
import "./Rooms.css";
|
||||
|
||||
const Rooms = ({ rooms, joinedRooms, history }) => {
|
||||
function onClick(roomId) {
|
||||
if (_.find(joinedRooms, room => room.roomId === roomId)) {
|
||||
history.push(generatePath(RouteEnum.ROOM, { roomId }));
|
||||
} else {
|
||||
RoomsService.joinRoom(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rooms">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Permissions</TableCell>
|
||||
<TableCell>Players</TableCell>
|
||||
<TableCell>Games</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ _.map(rooms, ({ description, gameCount, name, permissionlevel, playerCount, roomId }) => (
|
||||
<TableRow key={roomId}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell>{description}</TableCell>
|
||||
<TableCell>{permissionlevel}</TableCell>
|
||||
<TableCell>{playerCount}</TableCell>
|
||||
<TableCell>{gameCount}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" color="primary" variant="contained" onClick={() => onClick(roomId)}>
|
||||
Join
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rooms;
|
||||
61
webclient/src/AppShell/Server/Server.css
Normal file
61
webclient/src/AppShell/Server/Server.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
.server,
|
||||
.server-rooms,
|
||||
.server-rooms__side {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.server {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.server .form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.server-connect {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.server-connect__form,
|
||||
.server-connect__description {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.server-connect__form {
|
||||
margin: 50px 0;
|
||||
}
|
||||
|
||||
.server-connect__description {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.serverRoomWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.serverMessage {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.server-rooms {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.server-rooms__side-label {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
}
|
||||
162
webclient/src/AppShell/Server/Server.tsx
Normal file
162
webclient/src/AppShell/Server/Server.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import Button from "@material-ui/core/Button";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import { Selectors as RoomsSelectors } from "store/rooms";
|
||||
import { Selectors as ServerSelectors } from "store/server";
|
||||
import { Room, StatusEnum, User } from "types";
|
||||
|
||||
import ThreePaneLayout from "AppShell/common/components/ThreePaneLayout/ThreePaneLayout";
|
||||
import UserDisplay from "AppShell/common/components/UserDisplay/UserDisplay";
|
||||
import VirtualList from "AppShell/common/components/VirtualList/VirtualList";
|
||||
|
||||
import { AuthenticationService } from "AppShell/common/services";
|
||||
|
||||
import ConnectForm from "./ConnectForm/ConnectForm";
|
||||
import RegisterForm from "./RegisterForm/RegisterForm";
|
||||
import Rooms from "./Rooms/Rooms";
|
||||
|
||||
import "./Server.css";
|
||||
|
||||
class Server extends Component<ServerProps, ServerState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.showDescription = this.showDescription.bind(this);
|
||||
this.showRegisterForm = this.showRegisterForm.bind(this);
|
||||
this.hideRegisterForm = this.hideRegisterForm.bind(this);
|
||||
this.onRegister = this.onRegister.bind(this);
|
||||
|
||||
this.state = {
|
||||
register: false
|
||||
};
|
||||
}
|
||||
|
||||
showDescription(state, description) {
|
||||
const isDisconnected = state === StatusEnum.DISCONNECTED;
|
||||
const hasDescription = description && !!description.length;
|
||||
|
||||
return isDisconnected && hasDescription;
|
||||
}
|
||||
|
||||
showRegisterForm() {
|
||||
this.setState({register: true});
|
||||
}
|
||||
|
||||
hideRegisterForm() {
|
||||
this.setState({register: false});
|
||||
}
|
||||
|
||||
onRegister(fields) {
|
||||
console.log("register", fields);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, rooms, joinedRooms, history, state, description, users } = this.props;
|
||||
const { register } = this.state;
|
||||
const isConnected = AuthenticationService.isConnected(state);
|
||||
|
||||
return (
|
||||
<div className="server">
|
||||
{
|
||||
isConnected
|
||||
? ( <ServerRooms rooms={rooms} joinedRooms={joinedRooms} history={history} message={message} users={users} /> )
|
||||
: (
|
||||
<div className="server-connect">
|
||||
<Paper className="server-connect__form">
|
||||
{
|
||||
register
|
||||
? ( <Register connect={this.hideRegisterForm} onRegister={this.onRegister} /> )
|
||||
: ( <Connect register={this.showRegisterForm} /> )
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isConnected && this.showDescription(state, description) && (
|
||||
<Paper className="server-connect__description">
|
||||
{description}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ServerRooms = ({ rooms, joinedRooms, history, message, users}) => (
|
||||
<div className="server-rooms">
|
||||
<ThreePaneLayout
|
||||
top={(
|
||||
<Paper className="serverRoomWrapper overflow-scroll">
|
||||
<Rooms rooms={rooms} joinedRooms={joinedRooms} history={history} />
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
bottom={(
|
||||
<Paper className="serverMessage overflow-scroll" dangerouslySetInnerHTML={{ __html: message }} />
|
||||
)}
|
||||
|
||||
side={(
|
||||
<Paper className="server-rooms__side overflow-scroll">
|
||||
<div className="server-rooms__side-label">
|
||||
Users connected to server: {users.length}
|
||||
</div>
|
||||
<VirtualList
|
||||
itemKey={(index, data) => users[index].name }
|
||||
items={ users.map(user => (
|
||||
<ListItem button dense>
|
||||
<UserDisplay user={user} />
|
||||
</ListItem>
|
||||
) ) }
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Connect = ({register}) => (
|
||||
<div className="form-wrapper">
|
||||
<ConnectForm onSubmit={AuthenticationService.connect} />
|
||||
{/*{<Button variant="outlined" onClick={register}>Register</Button>}*/}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Register = ({ onRegister, connect }) => (
|
||||
<div className="form-wrapper">
|
||||
<RegisterForm onSubmit={event => onRegister(event)} />
|
||||
<Button variant="outlined" onClick={connect}>Connect</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ServerProps {
|
||||
message: string;
|
||||
state: number;
|
||||
description: string;
|
||||
rooms: Room[];
|
||||
joinedRooms: Room[];
|
||||
users: User[];
|
||||
history: any;
|
||||
}
|
||||
|
||||
interface ServerState {
|
||||
register: boolean;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: ServerSelectors.getMessage(state),
|
||||
state: ServerSelectors.getState(state),
|
||||
description: ServerSelectors.getDescription(state),
|
||||
rooms: RoomsSelectors.getRooms(state),
|
||||
joinedRooms: RoomsSelectors.getJoinedRooms(state),
|
||||
users: ServerSelectors.getUsers(state)
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Server));
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
|
||||
const CheckboxField = ({ input, label }) => {
|
||||
const { value, onChange } = input;
|
||||
|
||||
// @TODO this isnt unchecking properly
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-field"
|
||||
label={label}
|
||||
control={
|
||||
<Checkbox
|
||||
className="checkbox-field__box"
|
||||
checked={!!value}
|
||||
onChange={onChange}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxField;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.input-action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-action,
|
||||
.input-action__item,
|
||||
.input-action__submit {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.input-action__item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.input-action__item > div {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
import { Field } from "redux-form"
|
||||
import Button from "@material-ui/core/Button";
|
||||
|
||||
import InputField from '../InputField/InputField';
|
||||
|
||||
import "./InputAction.css";
|
||||
|
||||
const InputAction = ({ action, label, name }) => (
|
||||
<div className="input-action">
|
||||
<div className="input-action__item">
|
||||
<Field label={label} name={name} component={InputField} />
|
||||
</div>
|
||||
<div className="input-action__submit">
|
||||
<Button color="primary" variant="contained" type="submit">
|
||||
{action}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default InputAction;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
|
||||
const InputField = ({ input, label, name, autoComplete, type }) => (
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="dense"
|
||||
fullWidth={true}
|
||||
label={label}
|
||||
name={name}
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
{ ...input }
|
||||
/>
|
||||
);
|
||||
|
||||
export default InputField;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
const ScrollToBottomOnChanges = ({ content, changes }) => {
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
// @TODO (2)
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
||||
useEffect(scrollToBottom, [changes]);
|
||||
|
||||
const styling = {
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styling}>
|
||||
{content}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScrollToBottomOnChanges;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.select-field label {
|
||||
background: white;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import FormControl from "@material-ui/core/FormControl";
|
||||
import InputLabel from "@material-ui/core/InputLabel";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import Select from "@material-ui/core/Select";
|
||||
|
||||
import './SelectField.css';
|
||||
|
||||
const SelectField = ({ input, label, options, value }) => {
|
||||
const id = label + "-select-field";
|
||||
const labelId = id + "-label";
|
||||
|
||||
return (
|
||||
<FormControl variant="outlined" margin="dense" className="select-field">
|
||||
<InputLabel id={labelId}>{label}</InputLabel>
|
||||
<Select
|
||||
labelId={labelId}
|
||||
id={id}
|
||||
value={value}
|
||||
{ ...input }
|
||||
>{
|
||||
options.map((option, index) => (
|
||||
<MenuItem value={index} key={index}> { option } </MenuItem>
|
||||
))
|
||||
}</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectField;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
.three-pane-layout,
|
||||
.three-pane-layout .grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.three-pane-layout .grid-main,
|
||||
.three-pane-layout .grid-side {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.three-pane-layout .grid-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.three-pane-layout .grid-main__top {
|
||||
max-height: 50%;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.three-pane-layout .grid-main__top.fixedHeight {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.three-pane-layout .grid-main__bottom {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component, CElement } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Hidden from "@material-ui/core/Hidden";
|
||||
|
||||
import "./ThreePaneLayout.css";
|
||||
|
||||
class ThreePaneLayout extends Component<ThreePaneLayoutProps> {
|
||||
render() {
|
||||
return (
|
||||
<div className="three-pane-layout">
|
||||
<Grid container spacing={2} className="grid">
|
||||
<Grid item xs={12} md={9} lg={10} className="grid-main">
|
||||
<Grid item className={
|
||||
"grid-main__top"
|
||||
+ (this.props.fixedHeight ? " fixedHeight" : "")
|
||||
}>
|
||||
{this.props.top}
|
||||
</Grid>
|
||||
<Grid item className="grid-main__bottom">
|
||||
{this.props.bottom}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Hidden smDown>
|
||||
<Grid item md={3} lg={2} className="grid-side">
|
||||
{this.props.side}
|
||||
</Grid>
|
||||
</Hidden>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ThreePaneLayoutProps {
|
||||
top: CElement<any, any>,
|
||||
bottom: CElement<any, any>,
|
||||
side?: CElement<any, any>,
|
||||
fixedHeight?: boolean,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ThreePaneLayout);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.user-display,
|
||||
.user-display__link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-display__details {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// eslint-disable-next-line
|
||||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { NavLink, generatePath } from "react-router-dom";
|
||||
|
||||
import Menu from "@material-ui/core/Menu";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
|
||||
import { SessionService } from "AppShell/common/services";
|
||||
import { RouteEnum } from "AppShell/common/types";
|
||||
|
||||
import { Selectors } from "store/server";
|
||||
|
||||
import { User } from "types";
|
||||
|
||||
import "./UserDisplay.css";
|
||||
|
||||
|
||||
class UserDisplay extends Component<UserDisplayProps, UserDisplayState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.navigateToUserProfile = this.navigateToUserProfile.bind(this);
|
||||
this.addToBuddyList = this.addToBuddyList.bind(this);
|
||||
this.removeFromBuddyList = this.removeFromBuddyList.bind(this);
|
||||
this.addToIgnoreList = this.addToIgnoreList.bind(this);
|
||||
this.removeFromIgnoreList = this.removeFromIgnoreList.bind(this);
|
||||
|
||||
this.isABuddy = this.isABuddy.bind(this);
|
||||
this.isIgnored = this.isIgnored.bind(this);
|
||||
|
||||
this.state = {
|
||||
position: null
|
||||
};
|
||||
}
|
||||
|
||||
handleClick(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
position: {
|
||||
x: event.clientX + 2,
|
||||
y: event.clientY + 4,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.setState({
|
||||
position: null
|
||||
});
|
||||
}
|
||||
|
||||
navigateToUserProfile() {
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
addToBuddyList() {
|
||||
SessionService.addToBuddyList(this.props.user.name);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
removeFromBuddyList() {
|
||||
SessionService.removeFromBuddyList(this.props.user.name);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
addToIgnoreList() {
|
||||
SessionService.addToIgnoreList(this.props.user.name);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
removeFromIgnoreList() {
|
||||
SessionService.removeFromIgnoreList(this.props.user.name);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
isABuddy() {
|
||||
return this.props.buddyList.filter(user => user.name === this.props.user.name).length;
|
||||
}
|
||||
|
||||
isIgnored() {
|
||||
return this.props.ignoreList.filter(user => user.name === this.props.user.name).length;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { position } = this.state;
|
||||
const { name } = user;
|
||||
|
||||
const isABuddy = this.isABuddy();
|
||||
const isIgnored = this.isIgnored();
|
||||
|
||||
|
||||
console.log('user', name, !!isABuddy, !!isIgnored);
|
||||
|
||||
return (
|
||||
<div className="user-display">
|
||||
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="plain-link">
|
||||
<div className="user-display__details" onContextMenu={this.handleClick}>
|
||||
<div className="user-display__country"></div>
|
||||
<div className="user-display__name single-line-ellipsis">{name}</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
<div className="user-display__menu">
|
||||
<Menu
|
||||
open={Boolean(position)}
|
||||
onClose={this.handleClose}
|
||||
anchorReference='anchorPosition'
|
||||
anchorPosition={
|
||||
position !== null
|
||||
? { top: position.y, left: position.x }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="user-display__link plain-link">
|
||||
<MenuItem dense>Chat</MenuItem>
|
||||
</NavLink>
|
||||
{
|
||||
!isABuddy
|
||||
? ( <MenuItem dense onClick={this.addToBuddyList}>Add to Buddy List</MenuItem> )
|
||||
: ( <MenuItem dense onClick={this.removeFromBuddyList}>Remove From Buddy List</MenuItem> )
|
||||
}
|
||||
{
|
||||
!isIgnored
|
||||
? ( <MenuItem dense onClick={this.addToIgnoreList}>Add to Ignore List</MenuItem> )
|
||||
: ( <MenuItem dense onClick={this.removeFromIgnoreList}>Remove From Ignore List</MenuItem> )
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface UserDisplayProps {
|
||||
user: User;
|
||||
buddyList: User[];
|
||||
ignoreList: User[];
|
||||
}
|
||||
|
||||
interface UserDisplayState {
|
||||
position: any;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
buddyList: Selectors.getBuddyList(state),
|
||||
ignoreList: Selectors.getIgnoreList(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(UserDisplay);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.virtual-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// eslint-disable-next-line
|
||||
import React from "react";
|
||||
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import './VirtualList.css';
|
||||
|
||||
const VirtualList = ({ items, itemKey, className = {}, size = 30 }) => (
|
||||
<div className="virtual-list">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className={`virtual-list__list ${className}`}
|
||||
height={height}
|
||||
width={width}
|
||||
itemData={items}
|
||||
itemCount={items.length}
|
||||
itemSize={size}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Row = ({ data, index, style }) => (
|
||||
<div style={style}>
|
||||
{data[index]}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default VirtualList;
|
||||
26
webclient/src/AppShell/common/guards/AuthGuard.tsx
Normal file
26
webclient/src/AppShell/common/guards/AuthGuard.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
import { Selectors } from "store/server";
|
||||
|
||||
import { AuthenticationService } from "AppShell/common/services";
|
||||
import { RouteEnum } from "AppShell/common/types";
|
||||
|
||||
class AuthGuard extends Component<AuthGuardProps> {
|
||||
render() {
|
||||
return !AuthenticationService.isConnected(this.props.state)
|
||||
? <Redirect from="*" to={RouteEnum.SERVER} />
|
||||
: "";
|
||||
}
|
||||
};
|
||||
|
||||
interface AuthGuardProps {
|
||||
state: number;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
state: Selectors.getState(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AuthGuard);
|
||||
27
webclient/src/AppShell/common/guards/ModGuard.tsx
Normal file
27
webclient/src/AppShell/common/guards/ModGuard.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React, { Component } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
import { Selectors } from "store/server";
|
||||
import { User } from "types";
|
||||
|
||||
import { AuthenticationService } from "AppShell/common/services";
|
||||
import { RouteEnum } from "AppShell/common/types";
|
||||
|
||||
class ModGuard extends Component<ModGuardProps> {
|
||||
render() {
|
||||
return !AuthenticationService.isModerator(this.props.user)
|
||||
? <Redirect from="*" to={RouteEnum.SERVER} />
|
||||
: "";
|
||||
}
|
||||
};
|
||||
|
||||
interface ModGuardProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: Selectors.getUser(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ModGuard);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { StatusEnum } from "types";
|
||||
import webClient from "WebClient/WebClient";
|
||||
|
||||
export class AuthenticationService {
|
||||
static connect(options) {
|
||||
webClient.services.session.connectServer(options);
|
||||
}
|
||||
static disconnect() {
|
||||
webClient.services.session.disconnectServer();
|
||||
}
|
||||
|
||||
static isConnected(state) {
|
||||
return state === StatusEnum.LOGGEDIN;
|
||||
}
|
||||
|
||||
static isModerator(user) {
|
||||
return user.userLevel >= webClient.pb.ServerInfo_User.UserLevelFlag.IsModerator;
|
||||
}
|
||||
|
||||
static isAdmin() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import webClient from "WebClient/WebClient";
|
||||
|
||||
export class ModeratorService {
|
||||
static viewLogHistory(filters) {
|
||||
webClient.commands.session.viewLogHistory(filters);
|
||||
}
|
||||
}
|
||||
11
webclient/src/AppShell/common/services/RoomsService.tsx
Normal file
11
webclient/src/AppShell/common/services/RoomsService.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import webClient from "WebClient/WebClient";
|
||||
|
||||
export class RoomsService {
|
||||
static joinRoom(roomId) {
|
||||
webClient.commands.session.joinRoom(roomId);
|
||||
}
|
||||
|
||||
static roomSay(roomId, message) {
|
||||
webClient.commands.room.roomSay(roomId, message);
|
||||
}
|
||||
}
|
||||
7
webclient/src/AppShell/common/services/RouterService.tsx
Normal file
7
webclient/src/AppShell/common/services/RouterService.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { RouteEnum } from "../types";
|
||||
|
||||
export class RouterService {
|
||||
resolveUrl(path, params) {
|
||||
|
||||
}
|
||||
}
|
||||
19
webclient/src/AppShell/common/services/SessionService.tsx
Normal file
19
webclient/src/AppShell/common/services/SessionService.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import webClient from "WebClient/WebClient";
|
||||
|
||||
export class SessionService {
|
||||
static addToBuddyList(userName) {
|
||||
webClient.commands.session.addToBuddyList(userName);
|
||||
}
|
||||
|
||||
static removeFromBuddyList(userName) {
|
||||
webClient.commands.session.removeFromBuddyList(userName);
|
||||
}
|
||||
|
||||
static addToIgnoreList(userName) {
|
||||
webClient.commands.session.addToIgnoreList(userName);
|
||||
}
|
||||
|
||||
static removeFromIgnoreList(userName) {
|
||||
webClient.commands.session.removeFromIgnoreList(userName);
|
||||
}
|
||||
}
|
||||
4
webclient/src/AppShell/common/services/index.ts
Normal file
4
webclient/src/AppShell/common/services/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./AuthenticationService";
|
||||
export * from "./ModeratorService";
|
||||
export * from "./RoomsService";
|
||||
export * from "./SessionService";
|
||||
2
webclient/src/AppShell/common/types/index.tsx
Normal file
2
webclient/src/AppShell/common/types/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./routes";
|
||||
export * from "./user";
|
||||
10
webclient/src/AppShell/common/types/routes.tsx
Normal file
10
webclient/src/AppShell/common/types/routes.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export enum RouteEnum {
|
||||
PLAYER = "/player/:name",
|
||||
SERVER = "/server",
|
||||
ROOM = "/room/:roomId",
|
||||
LOGS = "/logs",
|
||||
GAME = "/game",
|
||||
DECKS = "/decks",
|
||||
DECK = "/deck",
|
||||
ACCOUNT = "/account",
|
||||
}
|
||||
3
webclient/src/AppShell/common/types/user.tsx
Normal file
3
webclient/src/AppShell/common/types/user.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface User {
|
||||
name: string;
|
||||
}
|
||||
157
webclient/src/WebClient/ProtoFiles.tsx
Normal file
157
webclient/src/WebClient/ProtoFiles.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
const ProtoFiles = [
|
||||
"admin_commands.proto",
|
||||
"card_attributes.proto",
|
||||
"color.proto",
|
||||
"command_attach_card.proto",
|
||||
"command_change_zone_properties.proto",
|
||||
"command_concede.proto",
|
||||
"command_create_arrow.proto",
|
||||
"command_create_counter.proto",
|
||||
"command_create_token.proto",
|
||||
"command_deck_del.proto",
|
||||
"command_deck_del_dir.proto",
|
||||
"command_deck_download.proto",
|
||||
"command_deck_list.proto",
|
||||
"command_deck_new_dir.proto",
|
||||
"command_deck_select.proto",
|
||||
"command_deck_upload.proto",
|
||||
"command_del_counter.proto",
|
||||
"command_delete_arrow.proto",
|
||||
"command_draw_cards.proto",
|
||||
"command_dump_zone.proto",
|
||||
"command_flip_card.proto",
|
||||
"command_game_say.proto",
|
||||
"command_inc_card_counter.proto",
|
||||
"command_inc_counter.proto",
|
||||
"command_kick_from_game.proto",
|
||||
"command_leave_game.proto",
|
||||
"command_move_card.proto",
|
||||
"command_mulligan.proto",
|
||||
"command_next_turn.proto",
|
||||
"command_ready_start.proto",
|
||||
"command_replay_delete_match.proto",
|
||||
"command_replay_download.proto",
|
||||
"command_replay_list.proto",
|
||||
"command_replay_modify_match.proto",
|
||||
"command_reveal_cards.proto",
|
||||
"command_roll_die.proto",
|
||||
"command_set_active_phase.proto",
|
||||
"command_set_card_attr.proto",
|
||||
"command_set_card_counter.proto",
|
||||
"command_set_counter.proto",
|
||||
"command_set_sideboard_lock.proto",
|
||||
"command_set_sideboard_plan.proto",
|
||||
"command_shuffle.proto",
|
||||
"command_stop_dump_zone.proto",
|
||||
"command_undo_draw.proto",
|
||||
"commands.proto",
|
||||
"context_concede.proto",
|
||||
"context_connection_state_changed.proto",
|
||||
"context_deck_select.proto",
|
||||
"context_move_card.proto",
|
||||
"context_mulligan.proto",
|
||||
"context_ping_changed.proto",
|
||||
"context_ready_start.proto",
|
||||
"context_set_sideboard_lock.proto",
|
||||
"context_undo_draw.proto",
|
||||
"event_add_to_list.proto",
|
||||
"event_attach_card.proto",
|
||||
"event_change_zone_properties.proto",
|
||||
"event_connection_closed.proto",
|
||||
"event_create_arrow.proto",
|
||||
"event_create_counter.proto",
|
||||
"event_create_token.proto",
|
||||
"event_del_counter.proto",
|
||||
"event_delete_arrow.proto",
|
||||
"event_destroy_card.proto",
|
||||
"event_draw_cards.proto",
|
||||
"event_dump_zone.proto",
|
||||
"event_flip_card.proto",
|
||||
"event_game_closed.proto",
|
||||
"event_game_host_changed.proto",
|
||||
"event_game_joined.proto",
|
||||
"event_game_say.proto",
|
||||
"event_game_state_changed.proto",
|
||||
"event_join.proto",
|
||||
"event_join_room.proto",
|
||||
"event_kicked.proto",
|
||||
"event_leave.proto",
|
||||
"event_leave_room.proto",
|
||||
"event_list_games.proto",
|
||||
"event_list_rooms.proto",
|
||||
"event_move_card.proto",
|
||||
"event_notify_user.proto",
|
||||
"event_player_properties_changed.proto",
|
||||
"event_remove_from_list.proto",
|
||||
"event_replay_added.proto",
|
||||
"event_reveal_cards.proto",
|
||||
"event_roll_die.proto",
|
||||
"event_room_say.proto",
|
||||
"event_server_complete_list.proto",
|
||||
"event_server_identification.proto",
|
||||
"event_server_message.proto",
|
||||
"event_server_shutdown.proto",
|
||||
"event_set_active_phase.proto",
|
||||
"event_set_active_player.proto",
|
||||
"event_set_card_attr.proto",
|
||||
"event_set_card_counter.proto",
|
||||
"event_set_counter.proto",
|
||||
"event_shuffle.proto",
|
||||
"event_stop_dump_zone.proto",
|
||||
"event_user_joined.proto",
|
||||
"event_user_left.proto",
|
||||
"event_user_message.proto",
|
||||
"game_commands.proto",
|
||||
"game_event.proto",
|
||||
"game_event_container.proto",
|
||||
"game_event_context.proto",
|
||||
"game_replay.proto",
|
||||
"isl_message.proto",
|
||||
"moderator_commands.proto",
|
||||
"move_card_to_zone.proto",
|
||||
"response.proto",
|
||||
"response_activate.proto",
|
||||
"response_adjust_mod.proto",
|
||||
"response_ban_history.proto",
|
||||
"response_deck_download.proto",
|
||||
"response_deck_list.proto",
|
||||
"response_deck_upload.proto",
|
||||
"response_dump_zone.proto",
|
||||
"response_forgotpasswordrequest.proto",
|
||||
"response_get_games_of_user.proto",
|
||||
"response_get_user_info.proto",
|
||||
"response_join_room.proto",
|
||||
"response_list_users.proto",
|
||||
"response_login.proto",
|
||||
"response_register.proto",
|
||||
"response_replay_download.proto",
|
||||
"response_replay_list.proto",
|
||||
"response_viewlog_history.proto",
|
||||
"response_warn_history.proto",
|
||||
"response_warn_list.proto",
|
||||
"room_commands.proto",
|
||||
"room_event.proto",
|
||||
"server_message.proto",
|
||||
"serverinfo_arrow.proto",
|
||||
"serverinfo_ban.proto",
|
||||
"serverinfo_card.proto",
|
||||
"serverinfo_cardcounter.proto",
|
||||
"serverinfo_chat_message.proto",
|
||||
"serverinfo_counter.proto",
|
||||
"serverinfo_deckstorage.proto",
|
||||
"serverinfo_game.proto",
|
||||
"serverinfo_gametype.proto",
|
||||
"serverinfo_player.proto",
|
||||
"serverinfo_playerping.proto",
|
||||
"serverinfo_playerproperties.proto",
|
||||
"serverinfo_replay.proto",
|
||||
"serverinfo_replay_match.proto",
|
||||
"serverinfo_room.proto",
|
||||
"serverinfo_user.proto",
|
||||
"serverinfo_warning.proto",
|
||||
"serverinfo_zone.proto",
|
||||
"session_commands.proto",
|
||||
"session_event.proto",
|
||||
];
|
||||
|
||||
export default ProtoFiles;
|
||||
329
webclient/src/WebClient/WebClient.tsx
Normal file
329
webclient/src/WebClient/WebClient.tsx
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
import protobuf from "protobufjs";
|
||||
|
||||
import { StatusEnum } from "types";
|
||||
|
||||
import * as roomEvents from "./events/RoomEvents";
|
||||
import * as sessionEvents from "./events/SessionEvents";
|
||||
|
||||
import { RoomService, SessionService } from "./services";
|
||||
import { RoomCommands, SessionCommands } from "./commands";
|
||||
|
||||
import ProtoFiles from "./ProtoFiles";
|
||||
|
||||
const roomEventKeys = Object.keys(roomEvents);
|
||||
const sessionEventKeys = Object.keys(sessionEvents);
|
||||
|
||||
interface ApplicationCommands {
|
||||
room: RoomCommands;
|
||||
session: SessionCommands;
|
||||
}
|
||||
|
||||
interface ApplicationServices {
|
||||
room: RoomService;
|
||||
session: SessionService;
|
||||
}
|
||||
|
||||
export class WebClient {
|
||||
private socket: WebSocket;
|
||||
private status: StatusEnum = StatusEnum.DISCONNECTED;
|
||||
private keepalivecb;
|
||||
private lastPingPending = false;
|
||||
private cmdId = 0;
|
||||
private pendingCommands = {};
|
||||
|
||||
public commands: ApplicationCommands;
|
||||
public services: ApplicationServices;
|
||||
|
||||
public protocolVersion = 14;
|
||||
public pb;
|
||||
|
||||
public clientConfig = {
|
||||
"clientver" : "webclient-1.0 (2019-10-31)",
|
||||
"clientfeatures" : [
|
||||
"client_id",
|
||||
"client_ver",
|
||||
"feature_set",
|
||||
"room_chat_history",
|
||||
"client_warnings",
|
||||
/* unimplemented features */
|
||||
"forgot_password",
|
||||
"idle_client",
|
||||
"mod_log_lookup",
|
||||
"user_ban_history",
|
||||
// satisfy server reqs for POC
|
||||
"websocket",
|
||||
"2.6.1_min_version",
|
||||
"2.7.0_min_version",
|
||||
]
|
||||
};
|
||||
|
||||
public options: any = {
|
||||
host: "",
|
||||
port: "",
|
||||
user: "",
|
||||
pass: "",
|
||||
debug: false,
|
||||
autojoinrooms: true,
|
||||
keepalive: 5000
|
||||
};
|
||||
|
||||
constructor() {
|
||||
const files = ProtoFiles.map(file => `${WebClient.PB_FILE_DIR}/${file}`);
|
||||
|
||||
this.pb = new protobuf.Root();
|
||||
this.pb.load(files, { keepCase: false }, (err, root) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// This sucks. I can"t seem to get out of this
|
||||
// circular dependency trap, so this is my current best.
|
||||
this.commands = {
|
||||
room: new RoomCommands(this),
|
||||
session: new SessionCommands(this),
|
||||
};
|
||||
|
||||
this.services = {
|
||||
room: new RoomService(this),
|
||||
session: new SessionService(this),
|
||||
};
|
||||
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
private clearStores() {
|
||||
this.services.room.clearStore();
|
||||
this.services.session.clearStore();
|
||||
}
|
||||
|
||||
public updateStatus(status, description) {
|
||||
console.log(`Status: [${status}]: ${description}`);
|
||||
this.status = status;
|
||||
this.services.session.updateStatus(status, description);
|
||||
|
||||
if (status === StatusEnum.DISCONNECTED) {
|
||||
this.clearStores();
|
||||
this.endPingLoop();
|
||||
this.resetConnectionvars();
|
||||
}
|
||||
}
|
||||
|
||||
public resetConnectionvars() {
|
||||
this.cmdId = 0;
|
||||
this.pendingCommands = {};
|
||||
this.lastPingPending = false;
|
||||
}
|
||||
|
||||
public sendCommand(cmd, callback) {
|
||||
this.cmdId++;
|
||||
cmd["cmdId"] = this.cmdId;
|
||||
|
||||
this.pendingCommands[this.cmdId] = callback;
|
||||
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(this.pb.CommandContainer.encode(cmd).finish());
|
||||
this.debug(() => console.log("Sent: " + cmd.toString()));
|
||||
} else {
|
||||
this.debug(() => console.log("Send: Not connected"));
|
||||
}
|
||||
}
|
||||
|
||||
public sendRoomCommand(roomId, roomCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"roomId" : roomId,
|
||||
"roomCommand" : [ roomCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, raw => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sendSessionCommand(sesCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"sessionCommand" : [ sesCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, (raw) => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sendModeratorCommand(modCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"moderatorCommand" : [ modCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, (raw) => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public startPingLoop() {
|
||||
this.keepalivecb = setInterval(() => {
|
||||
// check if the previous ping got no reply
|
||||
if (this.lastPingPending) {
|
||||
this.disconnect();
|
||||
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection timeout");
|
||||
}
|
||||
|
||||
// stop the ping loop if we"re disconnected
|
||||
if (this.status !== StatusEnum.LOGGEDIN) {
|
||||
this.endPingLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
// send a ping
|
||||
this.lastPingPending = true;
|
||||
|
||||
const ping = this.pb.Command_Ping.create();
|
||||
const command = this.pb.SessionCommand.create({
|
||||
".Command_Ping.ext" : ping
|
||||
});
|
||||
|
||||
this.sendSessionCommand(command, () => this.lastPingPending = false);
|
||||
}, this.options.keepalive);
|
||||
}
|
||||
|
||||
private endPingLoop() {
|
||||
clearInterval(this.keepalivecb);
|
||||
this.keepalivecb = null;
|
||||
}
|
||||
|
||||
public connect(options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
|
||||
const { host, port } = this.options;
|
||||
const protocol = port === '443' ? 'wss' : 'ws';
|
||||
|
||||
this.socket = new WebSocket(protocol + "://" + host + ":" + port);
|
||||
this.socket.binaryType = "arraybuffer"; // We are talking binary
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.updateStatus(StatusEnum.CONNECTED, "Connected");
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
// dont overwrite failure messages
|
||||
if (this.status !== StatusEnum.DISCONNECTED) {
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Closed");
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = () => {
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Failed");
|
||||
};
|
||||
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const msg = this.decodeServerMessage(event);
|
||||
|
||||
if (msg) {
|
||||
switch (msg.messageType) {
|
||||
case this.pb.ServerMessage.MessageType.RESPONSE:
|
||||
this.processServerResponse(msg.response);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.ROOM_EVENT:
|
||||
this.processRoomEvent(msg.roomEvent, msg);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.SESSION_EVENT:
|
||||
this.processSessionEvent(msg.sessionEvent, msg);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.GAME_EVENT_CONTAINER:
|
||||
// @TODO
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public debug(debug) {
|
||||
if (this.options.debug) {
|
||||
debug();
|
||||
}
|
||||
}
|
||||
|
||||
private decodeServerMessage(event) {
|
||||
const uint8msg = new Uint8Array(event.data);
|
||||
let msg;
|
||||
|
||||
try {
|
||||
msg = this.pb.ServerMessage.decode(uint8msg);
|
||||
|
||||
this.debug(() => console.log(msg));
|
||||
|
||||
return msg;
|
||||
} catch (err) {
|
||||
console.error("Processing failed:", err);
|
||||
|
||||
this.debug(() => {
|
||||
let str = "";
|
||||
|
||||
for (let i = 0; i < uint8msg.length; i++) {
|
||||
str += String.fromCharCode(uint8msg[i]);
|
||||
}
|
||||
|
||||
console.log(str);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private processServerResponse(response) {
|
||||
const cmdId = response.cmdId;
|
||||
|
||||
if (!this.pendingCommands.hasOwnProperty(cmdId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingCommands[cmdId](response);
|
||||
delete this.pendingCommands[cmdId];
|
||||
}
|
||||
|
||||
private processRoomEvent(response, raw) {
|
||||
this.processEvent(response, roomEvents, roomEventKeys, raw);
|
||||
}
|
||||
|
||||
private processSessionEvent(response, raw) {
|
||||
this.processEvent(response, sessionEvents, sessionEventKeys, raw);
|
||||
}
|
||||
|
||||
private processEvent(response, events, keys, raw) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const event = events[key];
|
||||
const payload = response[event.id];
|
||||
|
||||
if (payload) {
|
||||
events[key].action(payload, this, raw);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`;
|
||||
}
|
||||
|
||||
const webClient = new WebClient();
|
||||
export default webClient;
|
||||
27
webclient/src/WebClient/commands/RoomCommands.tsx
Normal file
27
webclient/src/WebClient/commands/RoomCommands.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import * as _ from 'lodash';
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
export class RoomCommands {
|
||||
private webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
roomSay(roomId, message) {
|
||||
const trimmed = _.trim(message);
|
||||
|
||||
if (!trimmed) return;
|
||||
|
||||
var CmdRoomSay = this.webClient.pb.Command_RoomSay.create({
|
||||
"message" : trimmed
|
||||
});
|
||||
|
||||
var rc = this.webClient.pb.RoomCommand.create({
|
||||
".Command_RoomSay.ext" : CmdRoomSay
|
||||
});
|
||||
|
||||
this.webClient.sendRoomCommand(roomId, rc);
|
||||
}
|
||||
}
|
||||
234
webclient/src/WebClient/commands/SessionCommands.tsx
Normal file
234
webclient/src/WebClient/commands/SessionCommands.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { StatusEnum } from "types";
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
import { guid } from "../util";
|
||||
|
||||
export class SessionCommands {
|
||||
private webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
login() {
|
||||
const loginConfig = {
|
||||
...this.webClient.clientConfig,
|
||||
"userName" : this.webClient.options.user,
|
||||
"password" : this.webClient.options.pass,
|
||||
"clientid" : guid()
|
||||
};
|
||||
|
||||
const CmdLogin = this.webClient.pb.Command_Login.create(loginConfig);
|
||||
|
||||
const command = this.webClient.pb.SessionCommand.create({
|
||||
".Command_Login.ext" : CmdLogin
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(command, raw => {
|
||||
const resp = raw[".Response_Login.ext"];
|
||||
|
||||
this.webClient.debug(() => console.log(".Response_Login.ext", resp));
|
||||
|
||||
switch(raw.responseCode) {
|
||||
case this.webClient.pb.Response.ResponseCode.RespOk:
|
||||
const { buddyList, ignoreList, userInfo } = resp;
|
||||
|
||||
this.webClient.services.session.updateBuddyList(buddyList);
|
||||
this.webClient.services.session.updateIgnoreList(ignoreList);
|
||||
this.webClient.services.session.updateUser(userInfo);
|
||||
|
||||
this.webClient.commands.session.listUsers();
|
||||
this.webClient.commands.session.listRooms();
|
||||
|
||||
this.webClient.updateStatus(StatusEnum.LOGGEDIN, "Logged in.");
|
||||
this.webClient.startPingLoop();
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespClientUpdateRequired:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: missing features");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespWrongPassword:
|
||||
case this.webClient.pb.Response.ResponseCode.RespUsernameInvalid:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: incorrect username or password");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespWouldOverwriteOldSession:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: duplicated user session");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespUserIsBanned:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: banned user");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespRegistrationRequired:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: registration required");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespClientIdRequired:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: missing client ID");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespContextError:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: server error");
|
||||
break;
|
||||
|
||||
case this.webClient.pb.Response.ResponseCode.RespAccountNotActivated:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: account not activated");
|
||||
break;
|
||||
|
||||
default:
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTED, "Login failed: unknown error " + raw.responseCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listUsers() {
|
||||
const CmdListUsers = this.webClient.pb.Command_ListUsers.create();
|
||||
|
||||
const sc = this.webClient.pb.SessionCommand.create({
|
||||
".Command_ListUsers.ext" : CmdListUsers
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(sc, raw => {
|
||||
const { responseCode } = raw;
|
||||
const response = raw[".Response_ListUsers.ext"];
|
||||
|
||||
if (response) {
|
||||
switch (responseCode) {
|
||||
case this.webClient.pb.Response.ResponseCode.RespOk:
|
||||
this.webClient.services.session.updateUsers(response.userList);
|
||||
break;
|
||||
default:
|
||||
console.log(`Failed to fetch Server Rooms [${responseCode}] : `, raw);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
listRooms() {
|
||||
const CmdListRooms = this.webClient.pb.Command_ListRooms.create();
|
||||
|
||||
const sc = this.webClient.pb.SessionCommand.create({
|
||||
".Command_ListRooms.ext" : CmdListRooms
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(sc);
|
||||
}
|
||||
|
||||
joinRoom(roomId: string) {
|
||||
const CmdJoinRoom = this.webClient.pb.Command_JoinRoom.create({
|
||||
"roomId" : roomId
|
||||
});
|
||||
|
||||
const sc = this.webClient.pb.SessionCommand.create({
|
||||
".Command_JoinRoom.ext" : CmdJoinRoom
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(sc, (raw) => {
|
||||
const { responseCode } = raw;
|
||||
|
||||
let error;
|
||||
|
||||
switch(responseCode) {
|
||||
case this.webClient.pb.Response.ResponseCode.RespOk:
|
||||
const { roomInfo } = raw[".Response_JoinRoom.ext"];
|
||||
|
||||
this.webClient.services.room.joinRoom(roomInfo);
|
||||
this.webClient.debug(() => console.log("Join Room: ", roomInfo.name));
|
||||
return;
|
||||
case this.webClient.pb.Response.ResponseCode.RespNameNotFound:
|
||||
error = "Failed to join the room: it doesn\"t exist on the server.";
|
||||
break;
|
||||
case this.webClient.pb.Response.ResponseCode.RespContextError:
|
||||
error = "The server thinks you are in the room but Cockatrice is unable to display it. Try restarting Cockatrice.";
|
||||
break;
|
||||
case this.webClient.pb.Response.ResponseCode.RespUserLevelTooLow:
|
||||
error = "You do not have the required permission to join this room.";
|
||||
break;
|
||||
default:
|
||||
error = "Failed to join the room due to an unknown error.";
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error(responseCode, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addToBuddyList(userName) {
|
||||
this.addToList('buddy', userName);
|
||||
}
|
||||
|
||||
removeFromBuddyList(userName) {
|
||||
this.removeFromList('buddy', userName);
|
||||
}
|
||||
|
||||
addToIgnoreList(userName) {
|
||||
this.addToList('ignore', userName);
|
||||
}
|
||||
|
||||
removeFromIgnoreList(userName) {
|
||||
this.removeFromList('ignore', userName);
|
||||
}
|
||||
|
||||
addToList(list: string, userName: string) {
|
||||
const CmdAddToList = this.webClient.pb.Command_AddToList.create({ list, userName });
|
||||
|
||||
const sc = this.webClient.pb.SessionCommand.create({
|
||||
".Command_AddToList.ext" : CmdAddToList
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(sc, ({ responseCode }) => {
|
||||
// @TODO: filter responseCode, pop snackbar for error
|
||||
this.webClient.debug(() => console.log('Added to List Response: ', responseCode));
|
||||
});
|
||||
}
|
||||
|
||||
removeFromList(list: string, userName: string) {
|
||||
const CmdRemoveFromList = this.webClient.pb.Command_RemoveFromList.create({ list, userName });
|
||||
|
||||
const sc = this.webClient.pb.SessionCommand.create({
|
||||
".Command_RemoveFromList.ext" : CmdRemoveFromList
|
||||
});
|
||||
|
||||
this.webClient.sendSessionCommand(sc, ({ responseCode }) => {
|
||||
// @TODO: filter responseCode, pop snackbar for error
|
||||
this.webClient.debug(() => console.log('Removed from List Response: ', responseCode));
|
||||
});
|
||||
}
|
||||
|
||||
viewLogHistory(filters) {
|
||||
const CmdViewLogHistory = this.webClient.pb.Command_ViewLogHistory.create(filters);
|
||||
|
||||
const sc = this.webClient.pb.ModeratorCommand.create({
|
||||
".Command_ViewLogHistory.ext" : CmdViewLogHistory
|
||||
});
|
||||
|
||||
this.webClient.sendModeratorCommand(sc, (raw) => {
|
||||
const { responseCode } = raw;
|
||||
|
||||
let error;
|
||||
|
||||
switch(responseCode) {
|
||||
case this.webClient.pb.Response.ResponseCode.RespOk:
|
||||
const { logMessage } = raw[".Response_ViewLogHistory.ext"];
|
||||
|
||||
console.log("Response_ViewLogHistory: ", logMessage)
|
||||
this.webClient.services.session.viewLogs(logMessage)
|
||||
|
||||
this.webClient.debug(() => console.log("View Log History: ", logMessage));
|
||||
return;
|
||||
default:
|
||||
error = "Failed to retrieve log history.";
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error(responseCode, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
2
webclient/src/WebClient/commands/index.tsx
Normal file
2
webclient/src/WebClient/commands/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./RoomCommands";
|
||||
export * from "./SessionCommands";
|
||||
7
webclient/src/WebClient/events/RoomEvents/JoinRoom.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/JoinRoom.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const JoinRoom = {
|
||||
id: ".Event_JoinRoom.ext",
|
||||
action: ({ userInfo }, webClient, { roomEvent }) => {
|
||||
const { roomId } = roomEvent;
|
||||
webClient.services.room.userJoined(roomId, userInfo);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/LeaveRoom.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/LeaveRoom.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const LeaveRoom = {
|
||||
id: ".Event_LeaveRoom.ext",
|
||||
action: ({ name }, webClient, { roomEvent }) => {
|
||||
const { roomId } = roomEvent;
|
||||
webClient.services.room.userLeft(roomId, name);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/ListGames.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/ListGames.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const ListGames = {
|
||||
id: ".Event_ListGames.ext",
|
||||
action: ({ gameList }, webClient, { roomEvent }) => {
|
||||
const { roomId } = roomEvent;
|
||||
webClient.services.room.updateGames(roomId, gameList);
|
||||
}
|
||||
};
|
||||
7
webclient/src/WebClient/events/RoomEvents/RoomSay.tsx
Normal file
7
webclient/src/WebClient/events/RoomEvents/RoomSay.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const RoomSay = {
|
||||
id: ".Event_RoomSay.ext",
|
||||
action: (message, webClient, { roomEvent }) => {
|
||||
const { roomId } = roomEvent;
|
||||
webClient.services.room.addMessage(roomId, message);
|
||||
}
|
||||
};
|
||||
4
webclient/src/WebClient/events/RoomEvents/index.tsx
Normal file
4
webclient/src/WebClient/events/RoomEvents/index.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./JoinRoom";
|
||||
export * from "./LeaveRoom";
|
||||
export * from "./ListGames";
|
||||
export * from "./RoomSay";
|
||||
18
webclient/src/WebClient/events/SessionEvents/AddToList.tsx
Normal file
18
webclient/src/WebClient/events/SessionEvents/AddToList.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export const AddToList = {
|
||||
id: ".Event_AddToList.ext",
|
||||
action: ({ listName, userInfo}, webClient) => {
|
||||
switch (listName) {
|
||||
case 'buddy': {
|
||||
webClient.services.session.addToBuddyList(userInfo);
|
||||
break;
|
||||
}
|
||||
case 'ignore': {
|
||||
webClient.services.session.addToIgnoreList(userInfo);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
webClient.debug(() => console.log('Attempted to add to unknown list: ', listName));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { StatusEnum } from "types";
|
||||
|
||||
export const ConnectionClosed = {
|
||||
id: ".Event_ConnectionClosed.ext",
|
||||
action: ({ reason }, webClient) => {
|
||||
let message = "";
|
||||
|
||||
// @TODO (5)
|
||||
switch(reason) {
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED:
|
||||
message = "The server has reached its maximum user capacity";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS:
|
||||
message = "There are too many concurrent connections from your address";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.BANNED:
|
||||
message = "You are banned";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.DEMOTED:
|
||||
message = "You were demoted";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN:
|
||||
message = "Scheduled server shutdown";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.USERNAMEINVALID:
|
||||
message = "Invalid username";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE:
|
||||
message = "You have been logged out due to logging in at another location";
|
||||
break;
|
||||
case webClient.pb.Event_ConnectionClosed.CloseReason.OTHER:
|
||||
default:
|
||||
message = "Unknown reason";
|
||||
break;
|
||||
}
|
||||
|
||||
webClient.updateStatus(StatusEnum.DISCONNECTED, message);
|
||||
}
|
||||
};
|
||||
16
webclient/src/WebClient/events/SessionEvents/ListRooms.tsx
Normal file
16
webclient/src/WebClient/events/SessionEvents/ListRooms.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import * as _ from "lodash";
|
||||
|
||||
export const ListRooms = {
|
||||
id: ".Event_ListRooms.ext",
|
||||
action: ({ roomList }, webClient) => {
|
||||
webClient.services.room.updateRooms(roomList);
|
||||
|
||||
if (webClient.options.autojoinrooms) {
|
||||
_.each(roomList, ({ autoJoin, roomId }) => {
|
||||
if (autoJoin) {
|
||||
webClient.commands.session.joinRoom(roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const NotifyUser = {
|
||||
id: ".Event_NotifyUser.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_NotifyUser", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const PlayerPropertiesChanges = {
|
||||
id: ".Event_PlayerPropertiesChanges.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_PlayerPropertiesChanges", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
export const RemoveFromList = {
|
||||
id: ".Event_RemoveFromList.ext",
|
||||
action: ({ listName, userName }, webClient) => {
|
||||
switch (listName) {
|
||||
case 'buddy': {
|
||||
webClient.services.session.removeFromBuddyList(userName);
|
||||
break;
|
||||
}
|
||||
case 'ignore': {
|
||||
webClient.services.session.removeFromIgnoreList(userName);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
webClient.debug(() => console.log('Attempted to remove from unknown list: ', listName));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { StatusEnum } from "types";
|
||||
|
||||
export const ServerIdentification = {
|
||||
id: ".Event_ServerIdentification.ext",
|
||||
action: (info, webClient, _raw) => {
|
||||
const { serverName, serverVersion, protocolVersion } = info;
|
||||
|
||||
if (protocolVersion !== webClient.protocolVersion) {
|
||||
webClient.disconnect();
|
||||
webClient.updateStatus(StatusEnum.DISCONNECTED, "Protocol version mismatch: " + protocolVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
webClient.resetConnectionvars();
|
||||
webClient.updateStatus(StatusEnum.LOGGINGIN, "Logging in...");
|
||||
webClient.services.session.updateInfo(serverName, serverVersion);
|
||||
webClient.commands.session.login();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const ServerMessage = {
|
||||
id: ".Event_ServerMessage.ext",
|
||||
action: ({ message }, webClient) => {
|
||||
webClient.services.session.serverMessage(message);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const ServerShutdown = {
|
||||
id: ".Event_ServerShutdown.ext",
|
||||
action: (payload, webClient) => {
|
||||
// console.info("Event_ServerShutdown", payload);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserJoined = {
|
||||
id: ".Event_UserJoined.ext",
|
||||
action: ({ userInfo }, webClient) => {
|
||||
webClient.services.session.userJoined(userInfo);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserLeft = {
|
||||
id: ".Event_UserLeft.ext",
|
||||
action: ({ name }, webClient) => {
|
||||
webClient.services.session.userLeft(name);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const UserMessage = {
|
||||
id: ".Event_UserMessage.ext",
|
||||
action: (payload) => {
|
||||
// console.info("Event_UserMessage", payload);
|
||||
}
|
||||
};
|
||||
12
webclient/src/WebClient/events/SessionEvents/index.ts
Normal file
12
webclient/src/WebClient/events/SessionEvents/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export * from "./ConnectionClosed";
|
||||
export * from "./ListRooms";
|
||||
export * from "./AddToList";
|
||||
export * from "./RemoveFromList";
|
||||
export * from "./NotifyUser"; // @TODO
|
||||
export * from "./PlayerPropertiesChanges"; // @TODO
|
||||
export * from "./ServerIdentification";
|
||||
export * from "./ServerMessage";
|
||||
export * from "./ServerShutdown"; // @TODO
|
||||
export * from "./UserJoined";
|
||||
export * from "./UserLeft";
|
||||
export * from "./UserMessage"; // @TODO
|
||||
44
webclient/src/WebClient/services/NormalizeService.tsx
Normal file
44
webclient/src/WebClient/services/NormalizeService.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
export class NormalizeService {
|
||||
// Flatten room gameTypes into map object
|
||||
static normalizeRoomInfo(roomInfo) {
|
||||
roomInfo.gametypeMap = {};
|
||||
|
||||
const { gametypeList, gametypeMap, gameList } = roomInfo;
|
||||
|
||||
gametypeList.reduce((map, type) => {
|
||||
map[type.gameTypeId] = type.description;
|
||||
return map;
|
||||
}, gametypeMap);
|
||||
|
||||
gameList.forEach((game) => NormalizeService.normalizeGameObject(game, gametypeMap));
|
||||
}
|
||||
|
||||
// Flatten gameTypes[] into gameType field
|
||||
// Default sortable values ("" || 0 || -1)
|
||||
static normalizeGameObject(game, gametypeMap) {
|
||||
const { gameTypes, description } = game;
|
||||
const hasType = gameTypes && gameTypes.length;
|
||||
game.gameType = hasType ? gametypeMap[gameTypes[0]] : "";
|
||||
|
||||
game.description = description || "";
|
||||
}
|
||||
|
||||
// Flatten logs[] into object mapped by targetType (room, game, chat)
|
||||
static normalizeLogs(logs) {
|
||||
return logs.reduce((obj, log) => {
|
||||
const { targetType } = log;
|
||||
obj[targetType] = obj[targetType] || [];
|
||||
obj[targetType].push(log);
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// messages sent by current user dont have their username prepended
|
||||
static normalizeUserMessage(message) {
|
||||
const { name } = message;
|
||||
|
||||
if (name) {
|
||||
message.message = `${name}: ${message.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
webclient/src/WebClient/services/RoomService.tsx
Normal file
53
webclient/src/WebClient/services/RoomService.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Dispatch, Selectors } from "store/rooms";
|
||||
import { store } from "store";
|
||||
|
||||
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
import { NormalizeService } from "./NormalizeService";
|
||||
|
||||
export class RoomService {
|
||||
webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
clearStore() {
|
||||
Dispatch.clearStore();
|
||||
}
|
||||
|
||||
joinRoom(roomInfo) {
|
||||
NormalizeService.normalizeRoomInfo(roomInfo);
|
||||
Dispatch.joinRoom(roomInfo);
|
||||
}
|
||||
|
||||
updateRooms(rooms) {
|
||||
Dispatch.updateRooms(rooms);
|
||||
}
|
||||
|
||||
updateGames(roomId, gameList) {
|
||||
const game = gameList[0];
|
||||
|
||||
if (!game.gameType) {
|
||||
const { gametypeMap } = Selectors.getRoom(store.getState(), roomId);
|
||||
NormalizeService.normalizeGameObject(game, gametypeMap);
|
||||
}
|
||||
|
||||
Dispatch.updateGames(roomId, gameList);
|
||||
}
|
||||
|
||||
addMessage(roomId, message) {
|
||||
NormalizeService.normalizeUserMessage(message);
|
||||
|
||||
Dispatch.addMessage(roomId, message);
|
||||
}
|
||||
|
||||
userJoined(roomId, user) {
|
||||
Dispatch.userJoined(roomId, user);
|
||||
}
|
||||
|
||||
userLeft(roomId, name) {
|
||||
Dispatch.userLeft(roomId, name);
|
||||
}
|
||||
}
|
||||
94
webclient/src/WebClient/services/SessionService.tsx
Normal file
94
webclient/src/WebClient/services/SessionService.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Dispatch, ServerConnectParams } from "store/server";
|
||||
import { StatusEnum } from "types";
|
||||
|
||||
import { sanitizeHtml } from "../util";
|
||||
import { WebClient } from "../WebClient";
|
||||
|
||||
import { NormalizeService } from "./NormalizeService";
|
||||
|
||||
export class SessionService {
|
||||
webClient: WebClient;
|
||||
|
||||
constructor(webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
clearStore() {
|
||||
Dispatch.clearStore();
|
||||
}
|
||||
|
||||
connectServer(options: ServerConnectParams) {
|
||||
Dispatch.connectServer();
|
||||
this.webClient.updateStatus(StatusEnum.CONNECTING, "Connecting...");
|
||||
this.webClient.connect(options);
|
||||
}
|
||||
|
||||
disconnectServer() {
|
||||
this.webClient.updateStatus(StatusEnum.DISCONNECTING, "Disconnecting...");
|
||||
this.webClient.disconnect();
|
||||
}
|
||||
|
||||
connectionClosed(reason) {
|
||||
Dispatch.connectionClosed(reason);
|
||||
}
|
||||
|
||||
updateBuddyList(buddyList) {
|
||||
Dispatch.updateBuddyList(buddyList);
|
||||
}
|
||||
|
||||
addToBuddyList(user) {
|
||||
Dispatch.addToBuddyList(user);
|
||||
}
|
||||
|
||||
removeFromBuddyList(userName) {
|
||||
Dispatch.removeFromBuddyList(userName);
|
||||
}
|
||||
|
||||
updateIgnoreList(ignoreList) {
|
||||
Dispatch.updateIgnoreList(ignoreList);
|
||||
}
|
||||
|
||||
addToIgnoreList(user) {
|
||||
Dispatch.addToIgnoreList(user);
|
||||
}
|
||||
|
||||
removeFromIgnoreList(userName) {
|
||||
Dispatch.removeFromIgnoreList(userName);
|
||||
}
|
||||
|
||||
updateInfo(name, version) {
|
||||
Dispatch.updateInfo(name, version);
|
||||
}
|
||||
|
||||
updateStatus(state, description) {
|
||||
Dispatch.updateStatus(state, description);
|
||||
|
||||
if (state === StatusEnum.DISCONNECTED) {
|
||||
this.connectionClosed({ reason: description });
|
||||
}
|
||||
}
|
||||
|
||||
updateUser(user) {
|
||||
Dispatch.updateUser(user);
|
||||
}
|
||||
|
||||
updateUsers(users) {
|
||||
Dispatch.updateUsers(users);
|
||||
}
|
||||
|
||||
userJoined(user) {
|
||||
Dispatch.userJoined(user);
|
||||
}
|
||||
|
||||
userLeft(userId) {
|
||||
Dispatch.userLeft(userId);
|
||||
}
|
||||
|
||||
viewLogs(logs) {
|
||||
Dispatch.viewLogs(NormalizeService.normalizeLogs(logs));
|
||||
}
|
||||
|
||||
serverMessage(message) {
|
||||
Dispatch.serverMessage(sanitizeHtml(message));
|
||||
}
|
||||
}
|
||||
3
webclient/src/WebClient/services/index.ts
Normal file
3
webclient/src/WebClient/services/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./NormalizeService";
|
||||
export * from "./RoomService";
|
||||
export * from "./SessionService";
|
||||
8
webclient/src/WebClient/util/guid.util.tsx
Normal file
8
webclient/src/WebClient/util/guid.util.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
function s4() {
|
||||
const s4 = Math.floor((1 + Math.random()) * 0x10000);
|
||||
return s4.toString(16).substring(1);
|
||||
}
|
||||
|
||||
export function guid() {
|
||||
return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
|
||||
}
|
||||
2
webclient/src/WebClient/util/index.tsx
Normal file
2
webclient/src/WebClient/util/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./guid.util";
|
||||
export * from "./sanitizeHtml.util";
|
||||
51
webclient/src/WebClient/util/sanitizeHtml.util.tsx
Normal file
51
webclient/src/WebClient/util/sanitizeHtml.util.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import $ from "jquery";
|
||||
|
||||
export function sanitizeHtml(msg) {
|
||||
const $div = $("<div>").html(msg);
|
||||
const whitelist = {
|
||||
tags: "br,a,img,center,b,font",
|
||||
attrs: ["href","color"],
|
||||
href: ["http://","https://","ftp://","//"]
|
||||
};
|
||||
|
||||
// remove all tags, attributes, and href protocols except some
|
||||
enforceTagWhitelist($div, whitelist.tags);
|
||||
enforceAttrWhitelist($div, whitelist.attrs);
|
||||
enforceHrefWhitelist($div, whitelist.href);
|
||||
|
||||
return $div.html();
|
||||
}
|
||||
|
||||
function enforceTagWhitelist($el, tags) {
|
||||
$el.find("*").not(tags).each(function() {
|
||||
$(this).replaceWith(this.innerHTML);
|
||||
});
|
||||
}
|
||||
|
||||
function enforceAttrWhitelist($el, attrs) {
|
||||
$el.find("*").each(function() {
|
||||
var attributes = this.attributes;
|
||||
var i = attributes.length;
|
||||
while( i-- ) {
|
||||
var attr = attributes[i];
|
||||
if( $.inArray(attr.name,attrs) === -1 )
|
||||
this.removeAttributeNode(attr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enforceHrefWhitelist($el, hrefs) {
|
||||
$el.find("[href]").each(function() {
|
||||
const $_el = $(this);
|
||||
const attributeValue = $_el.attr("href");
|
||||
|
||||
for (let protocol in hrefs) {
|
||||
if (attributeValue.indexOf(hrefs[protocol]) === 0) {
|
||||
$_el.attr("target", "_blank");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$_el.removeAttr("href");
|
||||
});
|
||||
}
|
||||
7
webclient/src/images/Images.tsx
Normal file
7
webclient/src/images/Images.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Countries from "./countries/_Countries";
|
||||
|
||||
export class Images {
|
||||
static Countries = [
|
||||
...Countries
|
||||
];
|
||||
}
|
||||
6
webclient/src/images/countries/.gitignore
vendored
Normal file
6
webclient/src/images/countries/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Ignore all files
|
||||
*
|
||||
# Except gitignore
|
||||
!.gitignore
|
||||
|
||||
!_Countries.tsx
|
||||
501
webclient/src/images/countries/_Countries.tsx
Normal file
501
webclient/src/images/countries/_Countries.tsx
Normal file
|
|
@ -0,0 +1,501 @@
|
|||
import ad from "./ad.svg";
|
||||
import ae from "./ae.svg";
|
||||
import af from "./af.svg";
|
||||
import ag from "./ag.svg";
|
||||
import ai from "./ai.svg";
|
||||
import al from "./al.svg";
|
||||
import am from "./am.svg";
|
||||
import ao from "./ao.svg";
|
||||
import aq from "./aq.svg";
|
||||
import ar from "./ar.svg";
|
||||
import as from "./as.svg";
|
||||
import at from "./at.svg";
|
||||
import au from "./au.svg";
|
||||
import aw from "./aw.svg";
|
||||
import ax from "./ax.svg";
|
||||
import az from "./az.svg";
|
||||
import ba from "./ba.svg";
|
||||
import bb from "./bb.svg";
|
||||
import bd from "./bd.svg";
|
||||
import be from "./be.svg";
|
||||
import bf from "./bf.svg";
|
||||
import bg from "./bg.svg";
|
||||
import bh from "./bh.svg";
|
||||
import bi from "./bi.svg";
|
||||
import bj from "./bj.svg";
|
||||
import bl from "./bl.svg";
|
||||
import bm from "./bm.svg";
|
||||
import bn from "./bn.svg";
|
||||
import bo from "./bo.svg";
|
||||
import bq from "./bq.svg";
|
||||
import br from "./br.svg";
|
||||
import bs from "./bs.svg";
|
||||
import bt from "./bt.svg";
|
||||
import bv from "./bv.svg";
|
||||
import bw from "./bw.svg";
|
||||
import by from "./by.svg";
|
||||
import bz from "./bz.svg";
|
||||
import ca from "./ca.svg";
|
||||
import cc from "./cc.svg";
|
||||
import cd from "./cd.svg";
|
||||
import cf from "./cf.svg";
|
||||
import cg from "./cg.svg";
|
||||
import ch from "./ch.svg";
|
||||
import ci from "./ci.svg";
|
||||
import ck from "./ck.svg";
|
||||
import cl from "./cl.svg";
|
||||
import cm from "./cm.svg";
|
||||
import cr from "./cr.svg";
|
||||
import cu from "./cu.svg";
|
||||
import cv from "./cv.svg";
|
||||
import cw from "./cw.svg";
|
||||
import cx from "./cx.svg";
|
||||
import cy from "./cy.svg";
|
||||
import cz from "./cz.svg";
|
||||
import de from "./de.svg";
|
||||
import dj from "./dj.svg";
|
||||
import dk from "./dk.svg";
|
||||
import dm from "./dm.svg";
|
||||
import _do from "./do.svg";
|
||||
import dz from "./dz.svg";
|
||||
import ec from "./ec.svg";
|
||||
import ee from "./ee.svg";
|
||||
import eg from "./eg.svg";
|
||||
import eh from "./eh.svg";
|
||||
import er from "./er.svg";
|
||||
import es from "./es.svg";
|
||||
import et from "./et.svg";
|
||||
import eu from "./eu.svg";
|
||||
import fi from "./fi.svg";
|
||||
import fj from "./fj.svg";
|
||||
import fk from "./fk.svg";
|
||||
import fm from "./fm.svg";
|
||||
import fo from "./fo.svg";
|
||||
import fr from "./fr.svg";
|
||||
import ga from "./ga.svg";
|
||||
import gb from "./gb.svg";
|
||||
import gd from "./gd.svg";
|
||||
import ge from "./ge.svg";
|
||||
import gf from "./gf.svg";
|
||||
import gg from "./gg.svg";
|
||||
import gh from "./gh.svg";
|
||||
import gi from "./gi.svg";
|
||||
import gl from "./gl.svg";
|
||||
import gm from "./gm.svg";
|
||||
import gn from "./gn.svg";
|
||||
import gp from "./gp.svg";
|
||||
import gq from "./gq.svg";
|
||||
import gr from "./gr.svg";
|
||||
import gs from "./gs.svg";
|
||||
import gt from "./gt.svg";
|
||||
import gu from "./gu.svg";
|
||||
import gw from "./gw.svg";
|
||||
import gy from "./gy.svg";
|
||||
import hk from "./hk.svg";
|
||||
import hm from "./hm.svg";
|
||||
import hn from "./hn.svg";
|
||||
import hr from "./hr.svg";
|
||||
import ht from "./ht.svg";
|
||||
import hu from "./hu.svg";
|
||||
import id from "./id.svg";
|
||||
import ie from "./ie.svg";
|
||||
import il from "./il.svg";
|
||||
import im from "./im.svg";
|
||||
import _in from "./in.svg";
|
||||
import io from "./io.svg";
|
||||
import iq from "./iq.svg";
|
||||
import ir from "./ir.svg";
|
||||
import is from "./is.svg";
|
||||
import it from "./it.svg";
|
||||
import je from "./je.svg";
|
||||
import jm from "./jm.svg";
|
||||
import jo from "./jo.svg";
|
||||
import jp from "./jp.svg";
|
||||
import ke from "./ke.svg";
|
||||
import kg from "./kg.svg";
|
||||
import kh from "./kh.svg";
|
||||
import ki from "./ki.svg";
|
||||
import km from "./km.svg";
|
||||
import kn from "./kn.svg";
|
||||
import kp from "./kp.svg";
|
||||
import kr from "./kr.svg";
|
||||
import kw from "./kw.svg";
|
||||
import ky from "./ky.svg";
|
||||
import kz from "./kz.svg";
|
||||
import la from "./la.svg";
|
||||
import lb from "./lb.svg";
|
||||
import lc from "./lc.svg";
|
||||
import li from "./li.svg";
|
||||
import lk from "./lk.svg";
|
||||
import lr from "./lr.svg";
|
||||
import ls from "./ls.svg";
|
||||
import lt from "./lt.svg";
|
||||
import lu from "./lu.svg";
|
||||
import lv from "./lv.svg";
|
||||
import ly from "./ly.svg";
|
||||
import ma from "./ma.svg";
|
||||
import mc from "./mc.svg";
|
||||
import md from "./md.svg";
|
||||
import me from "./me.svg";
|
||||
import mf from "./mf.svg";
|
||||
import mg from "./mg.svg";
|
||||
import mh from "./mh.svg";
|
||||
import mk from "./mk.svg";
|
||||
import ml from "./ml.svg";
|
||||
import mm from "./mm.svg";
|
||||
import mn from "./mn.svg";
|
||||
import mo from "./mo.svg";
|
||||
import mp from "./mp.svg";
|
||||
import mq from "./mq.svg";
|
||||
import mr from "./mr.svg";
|
||||
import ms from "./ms.svg";
|
||||
import mt from "./mt.svg";
|
||||
import mu from "./mu.svg";
|
||||
import mv from "./mv.svg";
|
||||
import mw from "./mw.svg";
|
||||
import mx from "./mx.svg";
|
||||
import my from "./my.svg";
|
||||
import mz from "./mz.svg";
|
||||
import na from "./na.svg";
|
||||
import nc from "./nc.svg";
|
||||
import ne from "./ne.svg";
|
||||
import nf from "./nf.svg";
|
||||
import ng from "./ng.svg";
|
||||
import ni from "./ni.svg";
|
||||
import nl from "./nl.svg";
|
||||
import no from "./no.svg";
|
||||
import np from "./np.svg";
|
||||
import nr from "./nr.svg";
|
||||
import nu from "./nu.svg";
|
||||
import nz from "./nz.svg";
|
||||
import om from "./om.svg";
|
||||
import pa from "./pa.svg";
|
||||
import pe from "./pe.svg";
|
||||
import pf from "./pf.svg";
|
||||
import pg from "./pg.svg";
|
||||
import ph from "./ph.svg";
|
||||
import pk from "./pk.svg";
|
||||
import pl from "./pl.svg";
|
||||
import pm from "./pm.svg";
|
||||
import pn from "./pn.svg";
|
||||
import pr from "./pr.svg";
|
||||
import ps from "./ps.svg";
|
||||
import pt from "./pt.svg";
|
||||
import pw from "./pw.svg";
|
||||
import py from "./py.svg";
|
||||
import qa from "./qa.svg";
|
||||
import re from "./re.svg";
|
||||
import ro from "./ro.svg";
|
||||
import rs from "./rs.svg";
|
||||
import ru from "./ru.svg";
|
||||
import rw from "./rw.svg";
|
||||
import sa from "./sa.svg";
|
||||
import sb from "./sb.svg";
|
||||
import sc from "./sc.svg";
|
||||
import sd from "./sd.svg";
|
||||
import se from "./se.svg";
|
||||
import sg from "./sg.svg";
|
||||
import sh from "./sh.svg";
|
||||
import si from "./si.svg";
|
||||
import sj from "./sj.svg";
|
||||
import sk from "./sk.svg";
|
||||
import sl from "./sl.svg";
|
||||
import sm from "./sm.svg";
|
||||
import sn from "./sn.svg";
|
||||
import so from "./so.svg";
|
||||
import sr from "./sr.svg";
|
||||
import ss from "./ss.svg";
|
||||
import st from "./st.svg";
|
||||
import sv from "./sv.svg";
|
||||
import sx from "./sx.svg";
|
||||
import sy from "./sy.svg";
|
||||
import sz from "./sz.svg";
|
||||
import tc from "./tc.svg";
|
||||
import td from "./td.svg";
|
||||
import tf from "./tf.svg";
|
||||
import tg from "./tg.svg";
|
||||
import th from "./th.svg";
|
||||
import tj from "./tj.svg";
|
||||
import tk from "./tk.svg";
|
||||
import tl from "./tl.svg";
|
||||
import tm from "./tm.svg";
|
||||
import tn from "./tn.svg";
|
||||
import to from "./to.svg";
|
||||
import tr from "./tr.svg";
|
||||
import tt from "./tt.svg";
|
||||
import tv from "./tv.svg";
|
||||
import tw from "./tw.svg";
|
||||
import tz from "./tz.svg";
|
||||
import ua from "./ua.svg";
|
||||
import ug from "./ug.svg";
|
||||
import um from "./um.svg";
|
||||
import us from "./us.svg";
|
||||
import uy from "./uy.svg";
|
||||
import uz from "./uz.svg";
|
||||
import va from "./va.svg";
|
||||
import vc from "./vc.svg";
|
||||
import ve from "./ve.svg";
|
||||
import vg from "./vg.svg";
|
||||
import vi from "./vi.svg";
|
||||
import vn from "./vn.svg";
|
||||
import vu from "./vu.svg";
|
||||
import wf from "./wf.svg";
|
||||
import ws from "./ws.svg";
|
||||
import ye from "./ye.svg";
|
||||
import yt from "./yt.svg";
|
||||
import za from "./za.svg";
|
||||
import zm from "./zm.svg";
|
||||
import zw from "./zw.svg";
|
||||
|
||||
const Countries = [
|
||||
ad,
|
||||
ae,
|
||||
af,
|
||||
ag,
|
||||
ai,
|
||||
al,
|
||||
am,
|
||||
ao,
|
||||
aq,
|
||||
ar,
|
||||
as,
|
||||
at,
|
||||
au,
|
||||
aw,
|
||||
ax,
|
||||
az,
|
||||
ba,
|
||||
bb,
|
||||
bd,
|
||||
be,
|
||||
bf,
|
||||
bg,
|
||||
bh,
|
||||
bi,
|
||||
bj,
|
||||
bl,
|
||||
bm,
|
||||
bn,
|
||||
bo,
|
||||
bq,
|
||||
br,
|
||||
bs,
|
||||
bt,
|
||||
bv,
|
||||
bw,
|
||||
by,
|
||||
bz,
|
||||
ca,
|
||||
cc,
|
||||
cd,
|
||||
cf,
|
||||
cg,
|
||||
ch,
|
||||
ci,
|
||||
ck,
|
||||
cl,
|
||||
cm,
|
||||
cr,
|
||||
cu,
|
||||
cv,
|
||||
cw,
|
||||
cx,
|
||||
cy,
|
||||
cz,
|
||||
de,
|
||||
dj,
|
||||
dk,
|
||||
dm,
|
||||
_do,
|
||||
dz,
|
||||
ec,
|
||||
ee,
|
||||
eg,
|
||||
eh,
|
||||
er,
|
||||
es,
|
||||
et,
|
||||
eu,
|
||||
fi,
|
||||
fj,
|
||||
fk,
|
||||
fm,
|
||||
fo,
|
||||
fr,
|
||||
ga,
|
||||
gb,
|
||||
gd,
|
||||
ge,
|
||||
gf,
|
||||
gg,
|
||||
gh,
|
||||
gi,
|
||||
gl,
|
||||
gm,
|
||||
gn,
|
||||
gp,
|
||||
gq,
|
||||
gr,
|
||||
gs,
|
||||
gt,
|
||||
gu,
|
||||
gw,
|
||||
gy,
|
||||
hk,
|
||||
hm,
|
||||
hn,
|
||||
hr,
|
||||
ht,
|
||||
hu,
|
||||
id,
|
||||
ie,
|
||||
il,
|
||||
im,
|
||||
_in,
|
||||
io,
|
||||
iq,
|
||||
ir,
|
||||
is,
|
||||
it,
|
||||
je,
|
||||
jm,
|
||||
jo,
|
||||
jp,
|
||||
ke,
|
||||
kg,
|
||||
kh,
|
||||
ki,
|
||||
km,
|
||||
kn,
|
||||
kp,
|
||||
kr,
|
||||
kw,
|
||||
ky,
|
||||
kz,
|
||||
la,
|
||||
lb,
|
||||
lc,
|
||||
li,
|
||||
lk,
|
||||
lr,
|
||||
ls,
|
||||
lt,
|
||||
lu,
|
||||
lv,
|
||||
ly,
|
||||
ma,
|
||||
mc,
|
||||
md,
|
||||
me,
|
||||
mf,
|
||||
mg,
|
||||
mh,
|
||||
mk,
|
||||
ml,
|
||||
mm,
|
||||
mn,
|
||||
mo,
|
||||
mp,
|
||||
mq,
|
||||
mr,
|
||||
ms,
|
||||
mt,
|
||||
mu,
|
||||
mv,
|
||||
mw,
|
||||
mx,
|
||||
my,
|
||||
mz,
|
||||
na,
|
||||
nc,
|
||||
ne,
|
||||
nf,
|
||||
ng,
|
||||
ni,
|
||||
nl,
|
||||
no,
|
||||
np,
|
||||
nr,
|
||||
nu,
|
||||
nz,
|
||||
om,
|
||||
pa,
|
||||
pe,
|
||||
pf,
|
||||
pg,
|
||||
ph,
|
||||
pk,
|
||||
pl,
|
||||
pm,
|
||||
pn,
|
||||
pr,
|
||||
ps,
|
||||
pt,
|
||||
pw,
|
||||
py,
|
||||
qa,
|
||||
re,
|
||||
ro,
|
||||
rs,
|
||||
ru,
|
||||
rw,
|
||||
sa,
|
||||
sb,
|
||||
sc,
|
||||
sd,
|
||||
se,
|
||||
sg,
|
||||
sh,
|
||||
si,
|
||||
sj,
|
||||
sk,
|
||||
sl,
|
||||
sm,
|
||||
sn,
|
||||
so,
|
||||
sr,
|
||||
ss,
|
||||
st,
|
||||
sv,
|
||||
sx,
|
||||
sy,
|
||||
sz,
|
||||
tc,
|
||||
td,
|
||||
tf,
|
||||
tg,
|
||||
th,
|
||||
tj,
|
||||
tk,
|
||||
tl,
|
||||
tm,
|
||||
tn,
|
||||
to,
|
||||
tr,
|
||||
tt,
|
||||
tv,
|
||||
tw,
|
||||
tz,
|
||||
ua,
|
||||
ug,
|
||||
um,
|
||||
us,
|
||||
uy,
|
||||
uz,
|
||||
va,
|
||||
vc,
|
||||
ve,
|
||||
vg,
|
||||
vi,
|
||||
vn,
|
||||
vu,
|
||||
wf,
|
||||
ws,
|
||||
ye,
|
||||
yt,
|
||||
za,
|
||||
zm,
|
||||
zw,
|
||||
];
|
||||
|
||||
export default Countries;
|
||||
57
webclient/src/index.css
Normal file
57
webclient/src/index.css
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
:root {
|
||||
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Oxygen",
|
||||
"Ubuntu",
|
||||
"Cantarell",
|
||||
"Fira Sans",
|
||||
"Droid Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.overflow-scroll {
|
||||
overflow-y: scroll; /* has to be scroll, not auto */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.plain-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.single-line-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
7
webclient/src/index.tsx
Normal file
7
webclient/src/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
|
||||
import AppShell from "./AppShell/AppShell";
|
||||
|
||||
ReactDOM.render(<AppShell />, document.getElementById("root"));
|
||||
1
webclient/src/react-app-env.d.ts
vendored
Normal file
1
webclient/src/react-app-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
||||
147
webclient/src/store/common/SortUtil.tsx
Normal file
147
webclient/src/store/common/SortUtil.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { SortBy, SortDirection, User } from "types";
|
||||
|
||||
export class SortUtil {
|
||||
static sortByField(arr: any[], sortBy: SortBy): void {
|
||||
if (arr.length) {
|
||||
const field = SortUtil.resolveFieldChain(arr[0], sortBy.field);
|
||||
const fieldType = typeof field;
|
||||
|
||||
if (fieldType === "string") {
|
||||
SortUtil.sortByString(arr, sortBy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldType === "number") {
|
||||
SortUtil.sortByNumber(arr, sortBy);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("SortField must resolve to either a string or number");
|
||||
}
|
||||
}
|
||||
|
||||
static sortByFields(arr: any[], sorts: SortBy[]) {
|
||||
if (arr.length) {
|
||||
arr.sort((a, b) => {
|
||||
for (let i = 0; i < sorts.length; i++) {
|
||||
const sortBy = sorts[i];
|
||||
const field = SortUtil.resolveFieldChain(arr[0], sortBy.field);
|
||||
|
||||
const fieldType = typeof field;
|
||||
|
||||
if (fieldType === "string") {
|
||||
const result = SortUtil.stringComparator(a, b, sortBy);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldType === "number") {
|
||||
const result = SortUtil.numberComparator(a, b, sortBy);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("SortField must resolve to either a string or number");
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static sortUsersByField(users: User[], sortBy: SortBy) {
|
||||
if (users.length) {
|
||||
users.sort((a, b) => SortUtil.userComparator(a, b, sortBy))
|
||||
}
|
||||
}
|
||||
|
||||
static toggleSortBy(field: string, sortBy: SortBy) {
|
||||
const sameField = field === sortBy.field;
|
||||
const isASC = sortBy.order === SortDirection.ASC;
|
||||
|
||||
return {
|
||||
field,
|
||||
order: sameField && isASC ? SortDirection.DESC : SortDirection.ASC
|
||||
}
|
||||
}
|
||||
|
||||
private static sortByNumber(arr: any[], sortBy: SortBy): void {
|
||||
arr.sort((a, b) => SortUtil.numberComparator(a, b, sortBy));
|
||||
}
|
||||
|
||||
private static sortByString(arr: any[], sortBy: SortBy): void {
|
||||
arr.sort((a, b) => SortUtil.stringComparator(a, b, sortBy));
|
||||
}
|
||||
|
||||
private static userComparator(a, b, sortBy, sortByUserLevel = true) {
|
||||
if (sortByUserLevel) {
|
||||
const adminSortBy = {
|
||||
field: "userLevel",
|
||||
order: SortDirection.DESC
|
||||
};
|
||||
|
||||
const adminSorted = SortUtil.numberComparator(a, b, adminSortBy);
|
||||
|
||||
if (adminSorted) {
|
||||
return adminSorted;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = SortUtil.stringComparator(a, b, sortBy);
|
||||
|
||||
if (sorted) {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static numberComparator(a, b, { field, order }: SortBy) {
|
||||
const aResolved = SortUtil.resolveFieldChain(a, field);
|
||||
const bResolved = SortUtil.resolveFieldChain(b, field);
|
||||
|
||||
if (order === SortDirection.ASC) {
|
||||
return aResolved - bResolved;
|
||||
} else {
|
||||
return bResolved - aResolved;
|
||||
}
|
||||
}
|
||||
|
||||
private static stringComparator(a, b, { field, order }: SortBy) {
|
||||
const aResolved = SortUtil.resolveFieldChain(a, field);
|
||||
const bResolved = SortUtil.resolveFieldChain(b, field);
|
||||
|
||||
// Force empty strings to sort to bottom
|
||||
if (!aResolved && !bResolved) { return 0; }
|
||||
if (!aResolved) { return 1; }
|
||||
if (!bResolved) { return -1; }
|
||||
|
||||
if (order === SortDirection.ASC) {
|
||||
return aResolved.localeCompare(bResolved);
|
||||
} else {
|
||||
return bResolved.localeCompare(aResolved);
|
||||
}
|
||||
}
|
||||
|
||||
private static resolveFieldChain(obj: object, field: string) {
|
||||
const links = field.split(".");
|
||||
|
||||
if (links.length > 1) {
|
||||
return links.reduce((obj, link) => {
|
||||
const parsed = parseInt(link, 10);
|
||||
|
||||
if (parsed.toLocaleString() === "NaN") {
|
||||
return obj[link];
|
||||
} else {
|
||||
return obj[parsed];
|
||||
}
|
||||
}, obj) || null;
|
||||
} else {
|
||||
return obj[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
1
webclient/src/store/common/index.ts
Normal file
1
webclient/src/store/common/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./SortUtil";
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue