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>
23
webclient/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
70
webclient/README.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
## To-Do List
|
||||
|
||||
1) RefreshGuard modal
|
||||
- there is no browser support for displaying custom output to window.onbeforeunload
|
||||
- we should also display a custom modal explaining why they shouldnt refresh or navigate from the site
|
||||
- ideally, the custom popup can be synced with the alert, so when the alert is closed, the modal closes too
|
||||
|
||||
2) Disable AutoScrollToBottom when the user has scrolled up
|
||||
- when the user scrolls back to bottom, it should renable
|
||||
- renable after a period of inactivity (3 minutes?)
|
||||
|
||||
3) Figure out how to type components w/ RouteComponentProps
|
||||
- Component<RouteComponentProps<???, ???, ???>>
|
||||
|
||||
4) clear input onSubmit
|
||||
|
||||
5) figure out how to reflect server status changes in the ui
|
||||
|
||||
6) Account page
|
||||
|
||||
7) Register/Forgot Passoword forms
|
||||
|
||||
8) Message User
|
||||
|
||||
9) Main Nav scheme
|
3
webclient/copy_shared_files.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
cp -a ../common/pb/. ./public/pb/
|
||||
cp -a ../cockatrice/resources/countries/. ./src/images/countries
|
Before Width: | Height: | Size: 3.1 KiB |
|
@ -1,433 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Cockatrice web client</title>
|
||||
<link rel="shortcut icon" href="imgs/cockatrice.png" />
|
||||
<link rel="stylesheet" href="js/jquery-ui-1.12.1.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1><img src="imgs/cockatrice.png" align="absmiddle"/> Cockatrice web client</h1>
|
||||
</header>
|
||||
<div id="loading">
|
||||
Loading cockatrice web client...
|
||||
</div>
|
||||
|
||||
<div id="error-dialog" title="Error" style="display:none"></div>
|
||||
<div id="info-dialog" title="Information" style="display:none"></div>
|
||||
|
||||
<div id="tabs" style="display:none">
|
||||
<ul>
|
||||
<li><a href="#tab-login">Login</a></li>
|
||||
<li><a href="#tab-server">Server</a></li>
|
||||
<li><a href="#tab-account">Account</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="tab-login">
|
||||
<h3>Login to server</h3>
|
||||
<label for="host">Host</label>
|
||||
<input type="text" id="host" value="" />
|
||||
<br/>
|
||||
<label for="port">Port</label>
|
||||
<input type="text" id="port" value ="4748" />
|
||||
<br/>
|
||||
<label for="user">Username</label>
|
||||
<input type="text" id="user" />
|
||||
<br/>
|
||||
<label for="pass">Password</label>
|
||||
<input type="password" id="pass" />
|
||||
<br/>
|
||||
<button id="loginnow">Connect</button>
|
||||
<button id="quit" style="display:none">Disconnect</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
|
||||
<div id="tab-server">
|
||||
<!--
|
||||
<h3>Rooms</h3>
|
||||
<span id="roomslist"></span>
|
||||
-->
|
||||
<h3>Server messages</h3>
|
||||
<div id="servermessages"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-account">
|
||||
<div class="buddies-container">
|
||||
<h3>Buddies</h3>
|
||||
<ul id="buddies" size="10"></ul>
|
||||
</div>
|
||||
<div class="ignores-container">
|
||||
<h3>Ignores</h3>
|
||||
<ul id="ignores" size="10"></ul>
|
||||
</div>
|
||||
<div class="userinfo-container">
|
||||
<h3>User info</h3>
|
||||
<span id="userinfo"></span>
|
||||
</div>
|
||||
<div class="missingfeatures-container">
|
||||
<h3>Missing features</h3>
|
||||
<span id="features"></span>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="js/jquery-3.2.1.min.js"></script>
|
||||
<script src="js/jquery-ui-1.12.1.min.js"></script>
|
||||
<script src="js/protobuf-6.7.0.min.js"></script>
|
||||
<script src="webclient.js"></script>
|
||||
|
||||
<script>
|
||||
$("#tabs").tabs();
|
||||
$("#tabs").tabs("disable").tabs("enable", "tab-login");
|
||||
$("#loading").hide();
|
||||
$("#tabs").show();
|
||||
|
||||
$( "#host" ).autocomplete({
|
||||
source: [
|
||||
// add custom servers here
|
||||
"server.cockatrice.us",
|
||||
"chickatrice.net",
|
||||
"127.0.0.1"
|
||||
]
|
||||
});
|
||||
|
||||
$( document ).ready(function() {
|
||||
|
||||
function ts2time(ts) {
|
||||
var d = new Date(Number(ts));
|
||||
return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
function getTime() {
|
||||
var d = new Date();
|
||||
return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
function htmlEscape(msg) {
|
||||
return $("<div>").text(msg).html();
|
||||
}
|
||||
|
||||
function htmlSanitize(msg) {
|
||||
var div = $("<div>").html(msg);
|
||||
|
||||
// remove all tags except some
|
||||
var tagsWhitelist = "br,a,img,center,b,font";
|
||||
$(div).find("*").not(tagsWhitelist).each(function() {
|
||||
$(this).replaceWith(this.innerHTML);
|
||||
});
|
||||
|
||||
// remove all attributes except some
|
||||
var attributesWhitelist = ["href","color"];
|
||||
|
||||
$(div).find("*").each(function() {
|
||||
var attributes = this.attributes;
|
||||
var i = attributes.length;
|
||||
while( i-- ) {
|
||||
var attr = attributes[i];
|
||||
if( $.inArray(attr.name,attributesWhitelist) == -1 )
|
||||
this.removeAttributeNode(attr);
|
||||
}
|
||||
});
|
||||
|
||||
// permit only some protocols in href
|
||||
var hrefWhitelist = ["http://","https://","ftp://","//"];
|
||||
|
||||
$(div).find("[href]").each(function() {
|
||||
var attributeValue = $(this).attr('href');
|
||||
for(var protocol in hrefWhitelist)
|
||||
{
|
||||
if(attributeValue.indexOf(hrefWhitelist[protocol]) == 0)
|
||||
{
|
||||
$(this).attr('target', '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$(this).removeAttr('href');
|
||||
});
|
||||
|
||||
return $(div).html();
|
||||
}
|
||||
|
||||
function timeAgoToInterval(secs)
|
||||
{
|
||||
var days = Math.floor(secs / 86400);
|
||||
var years = Math.floor(days / 365);
|
||||
days -= years * 365;
|
||||
var txt = '';
|
||||
|
||||
switch(years)
|
||||
{
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
txt += '1 year ';
|
||||
break;
|
||||
default:
|
||||
txt += years + ' years ';
|
||||
break;
|
||||
}
|
||||
|
||||
switch(days)
|
||||
{
|
||||
case 0:
|
||||
txt += '0 days ';
|
||||
case 1:
|
||||
txt += '1 day ';
|
||||
break;
|
||||
default:
|
||||
txt += days + ' days ';
|
||||
break;
|
||||
}
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function decodeUserLevel(id) {
|
||||
var levels = WebClient.pb.ServerInfo_User.UserLevelFlag;
|
||||
if (id & levels.IsAdmin)
|
||||
return "Administrator";
|
||||
else if (id & levels.IsModerator)
|
||||
return "Moderator";
|
||||
else if (id & levels.IsRegistered)
|
||||
return "Registered user";
|
||||
else
|
||||
return "Unregistered user";
|
||||
}
|
||||
|
||||
function sortUserlist(list) {
|
||||
var li = $(list + ' li');
|
||||
li.sort(function(a, b) {
|
||||
return $(a).text().toLowerCase().localeCompare($(b).text().toLowerCase());
|
||||
});
|
||||
$(list).empty().html(li);
|
||||
$(list).selectable();
|
||||
}
|
||||
|
||||
$("#loginnow").click(connect);
|
||||
$("#port, #user, #pass").keydown(function(e) {
|
||||
if (e.keyCode == 13) { connect(); }
|
||||
});
|
||||
|
||||
function connect() {
|
||||
var host = $("#host").val();
|
||||
var port = $("#port").val();
|
||||
if(!host.length || !port.length)
|
||||
{
|
||||
alert('Please enter a valid host and port.');
|
||||
return;
|
||||
}
|
||||
|
||||
var options = {
|
||||
"debug": false,
|
||||
"autojoinrooms" : true,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"user": $("#user").val(),
|
||||
"pass": $("#pass").val(),
|
||||
"statusCallback" : function(id, desc) {
|
||||
$("#status").text(desc);
|
||||
if(id == StatusEnum.LOGGEDIN)
|
||||
{
|
||||
$("#tabs").tabs("enable").tabs("option", "active", 1);
|
||||
$("#loginnow").hide();
|
||||
$("#quit").show();
|
||||
} else {
|
||||
$("#tabs").tabs("disable").tabs("enable", "tab-login").tabs("option", "active", 0);
|
||||
$("#quit").hide();
|
||||
$("#loginnow").show().prop("disabled", false);
|
||||
// close rooms
|
||||
$(".room-header, .room-container").remove();
|
||||
|
||||
}
|
||||
},
|
||||
"connectionClosedCallback" : function(id, desc) {
|
||||
$("#status").text('Connection closed: ' + desc);
|
||||
$("#tabs").tabs("disable").tabs("enable", "tab-login").tabs("option", "active", 0);
|
||||
$("#quit").hide();
|
||||
$("#loginnow").show().prop("disabled", false);
|
||||
// close rooms
|
||||
$(".room-header, .room-container").remove();
|
||||
},
|
||||
"serverMessageCallback" : function(message) {
|
||||
$("#servermessages").append(htmlSanitize(message) + '<br/>');
|
||||
},
|
||||
"serverIdentificationCallback" : function(data) {
|
||||
console.log('Connected to: ' + data.serverName + ' version ' + data.serverVersion + ' protocol ' + data.protocolVersion);
|
||||
},
|
||||
"serverShutdownCallback" : function(data) {
|
||||
$("#info-dialog").text('The server is going to be restarted in ' + data.minutes + ' minute(s).\nAll running games will be lost.\nReason for shutdown: ' + data.reason);
|
||||
$("#info-dialog").dialog();
|
||||
},
|
||||
"notifyUserCallback" : function(data) {
|
||||
switch(data.type)
|
||||
{
|
||||
case WebClient.pb.Event_NotifyUser.NotificationType.PROMOTED:
|
||||
$("#info-dialog").text('You have been promoted to moderator. Please log out and back in for changes to take effect.');
|
||||
$("#info-dialog").dialog();
|
||||
break;
|
||||
case WebClient.pb.Event_NotifyUser.NotificationType.WARNING:
|
||||
$("#info-dialog").text('You have received a warning due to ' + data.warningReason + '.\nPlease refrain from engaging in this activity or further actions may be taken against you. If you have any questions, please private message a moderator.');
|
||||
$("#info-dialog").dialog();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
"userInfoCallback" : function(data) {
|
||||
$("#userinfo").empty();
|
||||
$.each(data.userInfo, function(key, value) {
|
||||
// filter out inherited properties
|
||||
if (!data.userInfo.hasOwnProperty(key))
|
||||
return true;
|
||||
|
||||
// filter out empty values
|
||||
if(value === null)
|
||||
return true;
|
||||
|
||||
switch(key)
|
||||
{
|
||||
case 'avatarBmp':
|
||||
$('#userinfo').prepend("<img id='avatar' src='data:image/JPEG;base64," + btoa(String.fromCharCode.apply(null, value)) + "' /><br/>");
|
||||
break;
|
||||
case 'accountageSecs':
|
||||
$('#userinfo').append('Registered since: ' + timeAgoToInterval(value) + '<br/>');
|
||||
break;
|
||||
case 'userLevel':
|
||||
$('#userinfo').append('User level: ' + decodeUserLevel(value) + '<br/>');
|
||||
break;
|
||||
case 'name':
|
||||
$('#userinfo').append('Name: ' + value + '<br/>');
|
||||
break;
|
||||
case 'realName':
|
||||
$('#userinfo').append('Real name: ' + value + '<br/>');
|
||||
break;
|
||||
case 'country':
|
||||
$('#userinfo').append('Country: ' + value + '<br/>');
|
||||
break;
|
||||
case 'privlevel':
|
||||
$('#userinfo').append('Privilege: ' + value + '<br/>');
|
||||
break;
|
||||
case 'id':
|
||||
case 'serverId':
|
||||
// don't output these fields
|
||||
break;
|
||||
default:
|
||||
$('#userinfo').append(key + ': ' + value + '<br/>');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$('#buddies').empty();
|
||||
$.each(data.buddyList, function(key, value) {
|
||||
$('#buddies').append('<li>' + value.name + '</li>');
|
||||
});
|
||||
$("#buddies").selectable();
|
||||
|
||||
$('#ignores').empty();
|
||||
$.each(data.ignoreList, function(key, value) {
|
||||
$('#ignores').append('<li>' + value.name + '</li>');
|
||||
});
|
||||
$("#ignores").selectable();
|
||||
|
||||
$("#features").empty();
|
||||
$.each(data.missingFeatures, function(key, value) {
|
||||
$('#features').append(value + '<br/>');
|
||||
});
|
||||
},
|
||||
"listRoomsCallback" : function(rooms) {
|
||||
// hide
|
||||
//$("#roomslist").text(JSON.stringify(rooms, null, 4));
|
||||
},
|
||||
"errorCallback" : function(id, desc) {
|
||||
$("#error-dialog").text(desc);
|
||||
$("#error-dialog").dialog();
|
||||
},
|
||||
"joinRoomCallback" : function(room) {
|
||||
$("div#tabs > ul").append(
|
||||
"<li class='room-header'><a href='#tab-room-" + room["roomId"] + "'>" + room["name"] + "</a></li>"
|
||||
);
|
||||
$("div#tabs").append(
|
||||
"<div class='room-container' id='tab-room-" + room["roomId"] + "'>" +
|
||||
"<div class='userlist-container'>" +
|
||||
"<h3>Userlist</h3>" +
|
||||
"<ul class=\"userlist\" size=\"10\"></ul>" +
|
||||
"</div><div class='chat-container'>" +
|
||||
"<h3>Chat</h3>" +
|
||||
"<div class=\"output\"></div>" +
|
||||
"<br/><input type=\"text\" class=\"input\" />" +
|
||||
"<button class=\"say\">say</button>" +
|
||||
"</div></div><div class='clearfix'></div>"
|
||||
);
|
||||
$("div#tabs").tabs("refresh");
|
||||
|
||||
$("#tab-room-" + room["roomId"] + " .userlist").empty();
|
||||
$.each(room["userList"], function(key, value) {
|
||||
$("#tab-room-" + room["roomId"] + " .userlist").append('<li>' + value.name + '</li>');
|
||||
});
|
||||
sortUserlist("#tab-room-" + room["roomId"] + " .userlist");
|
||||
|
||||
$("#tab-room-" + room["roomId"] + " .say").click(function() {
|
||||
var msg = $("#tab-room-" + room["roomId"] + " .input").val();
|
||||
$("#tab-room-" + room["roomId"] + " .input").val("");
|
||||
WebClient.roomSay(room["roomId"], msg);
|
||||
});
|
||||
$("#tab-room-" + room["roomId"] + " .input").keydown(function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
var msg = $("#tab-room-" + room["roomId"] + " .input").val();
|
||||
$("#tab-room-" + room["roomId"] + " .input").val("");
|
||||
WebClient.roomSay(room["roomId"], msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
"roomMessageCallback" : function(roomId, message) {
|
||||
var text;
|
||||
var out = $("#tab-room-" + roomId + " .output")[0];
|
||||
var isScrolledToBottom = out.scrollHeight - out.clientHeight <= out.scrollTop + 1;
|
||||
switch(message["messageType"])
|
||||
{
|
||||
case WebClient.pb.Event_RoomSay.RoomMessageType.Welcome:
|
||||
text = "<span class='serverwelcome'>" + htmlEscape(message["message"]) + "</span>";
|
||||
break;
|
||||
case WebClient.pb.Event_RoomSay.RoomMessageType.ChatHistory:
|
||||
text = "<span class='chathistory'>[" + ts2time(message["timeOf"]) + "] " + htmlEscape(message["message"]) + "</span>";
|
||||
break;
|
||||
default:
|
||||
text = "[" + getTime() + "] " + htmlEscape(message["name"]) + ": " + htmlEscape(message["message"]);
|
||||
break;
|
||||
}
|
||||
$("#tab-room-" + roomId + " .output").append(text + '<br/>');
|
||||
if(isScrolledToBottom)
|
||||
out.scrollTop = out.scrollHeight - out.clientHeight;
|
||||
},
|
||||
"roomJoinCallback" : function(roomId, message) {
|
||||
$("#tab-room-" + roomId + " .userlist").append('<li>' + message['userInfo'].name + '</li>');
|
||||
sortUserlist("#tab-room-" + roomId + " .userlist");
|
||||
},
|
||||
"roomLeaveCallback" : function(roomId, message) {
|
||||
var name = message['name'];
|
||||
$("#tab-room-" + roomId + " .userlist li").filter(function() {
|
||||
return $.text([this]) === name;
|
||||
}).remove();
|
||||
sortUserlist("#tab-room-" + roomId + " .userlist");
|
||||
},
|
||||
"roomListGamesCallback" : function(roomId, message) {
|
||||
// TBD
|
||||
}
|
||||
};
|
||||
|
||||
$(this).prop("disabled", true);
|
||||
WebClient.connect(options);
|
||||
};
|
||||
|
||||
$("#quit").click(function() {
|
||||
WebClient.disconnect();
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.2 KiB |
4
webclient/js/jquery-3.2.1.min.js
vendored
7
webclient/js/jquery-ui-1.12.1.min.css
vendored
13
webclient/js/jquery-ui-1.12.1.min.js
vendored
10
webclient/js/protobuf-6.7.0.min.js
vendored
14059
webclient/package-lock.json
generated
Normal file
58
webclient/package.json
Normal file
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "webclient",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.6.1",
|
||||
"@types/jest": "24.0.20",
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/lodash": "^4.14.145",
|
||||
"@types/material-ui": "^0.21.7",
|
||||
"@types/node": "12.11.7",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"@types/protobufjs": "^6.0.0",
|
||||
"@types/react": "16.9.11",
|
||||
"@types/react-dom": "16.9.3",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/react-router-dom": "^5.1.0",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/redux-form": "^8.2.0",
|
||||
"jquery": "^3.4.1",
|
||||
"lodash": "^4.17.15",
|
||||
"prop-types": "^15.7.2",
|
||||
"protobufjs": "^6.8.8",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"redux": "^4.0.4",
|
||||
"redux-form": "^8.2.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"typescript": "3.6.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"postinstall": "./copy_shared_files.sh"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../common/pb
|
BIN
webclient/public/favicon.ico
Normal file
After Width: | Height: | Size: 22 KiB |
44
webclient/public/index.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/reset.css">
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Webatrice: A Cockatrice Web Client"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Webatrice</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
webclient/public/logo192.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
webclient/public/logo512.png
Normal file
After Width: | Height: | Size: 22 KiB |
25
webclient/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
4
webclient/public/pb/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Ignore all files
|
||||
*
|
||||
# Except gitignore
|
||||
!.gitignore
|
48
webclient/public/reset.css
Normal file
|
@ -0,0 +1,48 @@
|
|||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
2
webclient/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
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
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
|
@ -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
|
@ -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
After Width: | Height: | Size: 25 KiB |
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,7 @@
|
|||
import { RouteEnum } from "../types";
|
||||
|
||||
export class RouterService {
|
||||
resolveUrl(path, params) {
|
||||
|
||||
}
|
||||
}
|
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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
export * from "./routes";
|
||||
export * from "./user";
|
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
|
@ -0,0 +1,3 @@
|
|||
export interface User {
|
||||
name: string;
|
||||
}
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
export * from "./RoomCommands";
|
||||
export * from "./SessionCommands";
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||
}
|
||||
};
|