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:
Jeremy Letto
2020-12-31 16:08:15 -06:00
committed by GitHub
parent d5b36e8b8a
commit 0457e65751
152 changed files with 19573 additions and 1071 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
.select-field label {
background: white;
padding: 0 5px;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -0,0 +1,11 @@
.user-display,
.user-display__link {
height: 100%;
width: 100%;
}
.user-display__details {
height: 100%;
display: flex;
align-items: center;
}

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
.virtual-list {
height: 100%;
}

View File

@@ -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;