aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/web/src
diff options
context:
space:
mode:
authorPhilip Paquette <pcpaquette@gmail.com>2019-07-07 09:23:59 -0400
committerPhilip Paquette <pcpaquette@gmail.com>2019-07-07 15:33:01 -0400
commit5c71a0f73717bffefb5e23a9e100adb62fc54a61 (patch)
treea841ca27065db2c68e07570780549624bbb70f40 /diplomacy/web/src
parentd67963031211b32df6dd39d610a6424717729094 (diff)
Updated the web interface to have 3 tabs - Results / Messages / Current
- Updated dependencies in package-lock.json - Set default homepage to "." so that built pages are relative to "index.html". - Add module "helmet" to handle page title. - Simplified page loading (replace static function builder with direct call to a method with component as argument). - Move function loadGameFromDisk in a separate file. - Use React context to access page object. - Add a new React component "Navigation" and simplify page rendering. - Add ability to choose power for any kind of loaded game. In phase history: - Show messages from all past and currently displayed phase. - Display messages from past phase with gray background. - Show messages per protagonist in tabs. - Show message phase in message header - Display message wide (header left, body right). - Display short names for powers in message tabs header. - Add warn function to page component. - Messages from previous phase are displayed with gray color text. - Game registration password input is displayed only if required - On games page: - sorted by descending timestamp created. - In table, game ID is displayed with human readable created date. - Prevent messages from displaying twice. - Re-add checkbox "show orders" to display arrow orders on past maps. - Handle HTML break-lines <br/> and remove all other HTML tags when displaying messages. - Use latest phase as current game phase when loading a game from disk.
Diffstat (limited to 'diplomacy/web/src')
-rw-r--r--diplomacy/web/src/diplomacy/engine/game.js62
-rw-r--r--diplomacy/web/src/diplomacy/utils/sorted_dict.js9
-rw-r--r--diplomacy/web/src/gui/core/action.jsx (renamed from diplomacy/web/src/gui/core/widgets.jsx)50
-rw-r--r--diplomacy/web/src/gui/core/button.jsx52
-rw-r--r--diplomacy/web/src/gui/core/content.jsx51
-rw-r--r--diplomacy/web/src/gui/core/fancybox.jsx2
-rw-r--r--diplomacy/web/src/gui/core/forms.jsx2
-rw-r--r--diplomacy/web/src/gui/core/page.jsx277
-rw-r--r--diplomacy/web/src/gui/core/tab.jsx29
-rw-r--r--diplomacy/web/src/gui/core/tabs.jsx29
-rw-r--r--diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx41
-rw-r--r--diplomacy/web/src/gui/diplomacy/contents/content_game.jsx455
-rw-r--r--diplomacy/web/src/gui/diplomacy/contents/content_games.jsx83
-rw-r--r--diplomacy/web/src/gui/diplomacy/forms/join_form.jsx19
-rw-r--r--diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx33
-rw-r--r--diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx2
-rw-r--r--diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx2
-rw-r--r--diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx20
-rw-r--r--diplomacy/web/src/gui/diplomacy/utils/load_game_from_disk.js83
-rw-r--r--diplomacy/web/src/gui/diplomacy/widgets/help.jsx13
-rw-r--r--diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx24
-rw-r--r--diplomacy/web/src/gui/diplomacy/widgets/navigation.jsx61
-rw-r--r--diplomacy/web/src/gui/diplomacy/widgets/page_context.jsx3
-rw-r--r--diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx2
-rw-r--r--diplomacy/web/src/index.css47
25 files changed, 791 insertions, 660 deletions
diff --git a/diplomacy/web/src/diplomacy/engine/game.js b/diplomacy/web/src/diplomacy/engine/game.js
index cc9803e..93da77c 100644
--- a/diplomacy/web/src/diplomacy/engine/game.js
+++ b/diplomacy/web/src/diplomacy/engine/game.js
@@ -72,14 +72,14 @@ export class Game {
this.messages = new SortedDict(gameData instanceof Game ? null : gameData.messages, parseInt);
// {short phase name => state}
- this.state_history = gameData instanceof Game ? gameData.state_history : new SortedDict(gameData.state_history, comparablePhase);
+ this.state_history = new SortedDict(gameData instanceof Game ? gameData.state_history.toDict() : gameData.state_history, comparablePhase);
// {short phase name => {power name => [orders]}}
- this.order_history = gameData instanceof Game ? gameData.order_history : new SortedDict(gameData.order_history, comparablePhase);
+ this.order_history = new SortedDict(gameData instanceof Game ? gameData.order_history.toDict() : gameData.order_history, comparablePhase);
// {short phase name => {unit => [results]}}
- this.result_history = gameData instanceof Game ? gameData.result_history : new SortedDict(gameData.result_history, comparablePhase);
+ this.result_history = new SortedDict(gameData instanceof Game ? gameData.result_history.toDict() : gameData.result_history, comparablePhase);
// {short phase name => {message.time_sent => message}}
if (gameData instanceof Game) {
- this.message_history = gameData.message_history;
+ this.message_history = new SortedDict(gameData.message_history.toDict(), comparablePhase);
} else {
this.message_history = new SortedDict(null, comparablePhase);
for (let entry of Object.entries(gameData.message_history)) {
@@ -115,7 +115,7 @@ export class Game {
this.powers[power_name].setState(powerState);
}
}
- } else if(this.state_history.size()) {
+ } else if (this.state_history.size()) {
const lastState = this.state_history.lastValue();
if (lastState.units) {
for (let powerName of Object.keys(lastState.units)) {
@@ -387,15 +387,21 @@ export class Game {
cloneAt(pastPhase) {
if (pastPhase !== null && this.state_history.contains(pastPhase)) {
- const messages = this.message_history.get(pastPhase);
- const orders = this.order_history.get(pastPhase);
- const state = this.state_history.get(pastPhase);
const game = new Game(this);
+ const pastPhaseIndex = this.state_history.indexOf(pastPhase);
+ const nbPastPhases = this.state_history.size();
+ for (let i = nbPastPhases - 1; i > pastPhaseIndex; --i) {
+ const keyToRemove = this.state_history.keyFromIndex(i);
+ game.message_history.remove(keyToRemove);
+ game.state_history.remove(keyToRemove);
+ game.order_history.remove(keyToRemove);
+ game.result_history.remove(keyToRemove);
+ }
game.setPhaseData({
name: pastPhase,
- state: state,
- orders: orders,
- messages: messages
+ state: this.state_history.get(pastPhase),
+ orders: this.order_history.get(pastPhase),
+ messages: this.message_history.get(pastPhase)
});
return game;
}
@@ -409,32 +415,36 @@ export class Game {
}
getControllablePowers() {
- if (!this.isObserverGame()) {
- if (this.isOmniscientGame())
- return Object.keys(this.powers);
- return [this.role];
- }
- return [];
+ if (this.isObserverGame() || this.isOmniscientGame())
+ return Object.keys(this.powers);
+ return [this.role];
}
- getMessageChannels() {
+ getMessageChannels(role, all) {
const messageChannels = {};
- let messages = this.messages;
- if (!messages.size() && this.message_history.contains(this.phase))
- messages = this.message_history.get(this.phase);
- if (this.isPlayerGame()) {
+ role = role || this.role;
+ let messagesToShow = null;
+ if (all) {
+ messagesToShow = this.message_history.values();
+ if (this.messages.size() && !this.message_history.contains(this.phase))
+ messagesToShow.push(this.messages);
+ } else {
+ if (this.messages.size())
+ messagesToShow = [this.messages];
+ else if (this.message_history.contains(this.phase))
+ messagesToShow = this.message_history.get(this.phase);
+ }
+ for (let messages of messagesToShow) {
for (let message of messages.values()) {
let protagonist = null;
- if (message.sender === this.role || message.recipient === 'GLOBAL')
+ if (message.sender === role || message.recipient === 'GLOBAL')
protagonist = message.recipient;
- else if (message.recipient === this.role)
+ else if (message.recipient === role)
protagonist = message.sender;
if (!messageChannels.hasOwnProperty(protagonist))
messageChannels[protagonist] = [];
messageChannels[protagonist].push(message);
}
- } else {
- messageChannels['messages'] = messages.values();
}
return messageChannels;
}
diff --git a/diplomacy/web/src/diplomacy/utils/sorted_dict.js b/diplomacy/web/src/diplomacy/utils/sorted_dict.js
index 6a27f00..8800dba 100644
--- a/diplomacy/web/src/diplomacy/utils/sorted_dict.js
+++ b/diplomacy/web/src/diplomacy/utils/sorted_dict.js
@@ -106,4 +106,13 @@ export class SortedDict {
values() {
return this.__values.slice();
}
+
+ toDict() {
+ const len = this.__real_keys.length;
+ const dict = {};
+ for (let i = 0; i < len; ++i) {
+ dict[this.__real_keys[i]] = this.__values[i];
+ }
+ return dict;
+ }
}
diff --git a/diplomacy/web/src/gui/core/widgets.jsx b/diplomacy/web/src/gui/core/action.jsx
index 62a5eb4..73fe8cb 100644
--- a/diplomacy/web/src/gui/core/widgets.jsx
+++ b/diplomacy/web/src/gui/core/action.jsx
@@ -17,56 +17,6 @@
import React from "react";
import PropTypes from 'prop-types';
-export class Button extends React.Component {
- /** Bootstrap button.
- * Bootstrap classes:
- * - btn
- * - btn-primary
- * - mx-1 (margin-left 1px, margin-right 1px)
- * Props: title (str), onClick (function).
- * **/
- // title
- // onClick
- // pickEvent = false
- // large = false
- // small = false
-
- constructor(props) {
- super(props);
- this.onClick = this.onClick.bind(this);
- }
-
- onClick(event) {
- if (this.props.onClick)
- this.props.onClick(this.props.pickEvent ? event : null);
- }
-
- render() {
- return (
- <button
- className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')}
- disabled={this.props.disabled}
- onClick={this.onClick}>
- <strong>{this.props.title}</strong>
- </button>
- );
- }
-}
-
-Button.propTypes = {
- title: PropTypes.string.isRequired,
- onClick: PropTypes.func.isRequired,
- color: PropTypes.string,
- large: PropTypes.bool,
- small: PropTypes.bool,
- pickEvent: PropTypes.bool,
- disabled: PropTypes.bool
-};
-
-Button.defaultPropTypes = {
- disabled: false
-};
-
export class Action extends React.Component {
// title
diff --git a/diplomacy/web/src/gui/core/button.jsx b/diplomacy/web/src/gui/core/button.jsx
new file mode 100644
index 0000000..0d5dadd
--- /dev/null
+++ b/diplomacy/web/src/gui/core/button.jsx
@@ -0,0 +1,52 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+export class Button extends React.Component {
+ /** Bootstrap button.
+ * Bootstrap classes:
+ * - btn
+ * - btn-primary
+ * - mx-1 (margin-left 1px, margin-right 1px)
+ * Props: title (str), onClick (function).
+ * **/
+ // title
+ // onClick
+ // pickEvent = false
+ // large = false
+ // small = false
+
+ constructor(props) {
+ super(props);
+ this.onClick = this.onClick.bind(this);
+ }
+
+ onClick(event) {
+ if (this.props.onClick)
+ this.props.onClick(this.props.pickEvent ? event : null);
+ }
+
+ render() {
+ return (
+ <button
+ className={`btn btn-${this.props.color || 'secondary'}` + (this.props.large ? ' btn-block' : '') + (this.props.small ? ' btn-sm' : '')}
+ disabled={this.props.disabled}
+ onClick={this.onClick}>
+ <strong>{this.props.title}</strong>
+ </button>
+ );
+ }
+}
+
+Button.propTypes = {
+ title: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+ color: PropTypes.string,
+ large: PropTypes.bool,
+ small: PropTypes.bool,
+ pickEvent: PropTypes.bool,
+ disabled: PropTypes.bool
+};
+
+Button.defaultPropTypes = {
+ disabled: false
+};
diff --git a/diplomacy/web/src/gui/core/content.jsx b/diplomacy/web/src/gui/core/content.jsx
deleted file mode 100644
index 416ba9e..0000000
--- a/diplomacy/web/src/gui/core/content.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-// ==============================================================================
-// Copyright (C) 2019 - Philip Paquette, Steven Bocco
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU Affero General Public License as published by the Free
-// Software Foundation, either version 3 of the License, or (at your option) any
-// later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-// details.
-//
-// You should have received a copy of the GNU Affero General Public License along
-// with this program. If not, see <https://www.gnu.org/licenses/>.
-// ==============================================================================
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export class Content extends React.Component {
- // PROPERTIES:
- // page: pointer to parent Page object
- // data: data for current content
-
- // Each derived class must implement this static method.
- static builder(page, data) {
- return {
- // page title (string)
- title: `${data ? 'with data' : 'without data'}`,
- // page navigation links: array of couples
- // (navigation title, navigation callback ( onClick=() => callback() ))
- navigation: [],
- // page content: React component (e.g. <MyComponent/>, or <div class="content">...</div>, etc).
- component: null
- };
- }
-
- getPage() {
- return this.props.page;
- }
-
- componentDidMount() {
- window.scrollTo(0, 0);
- }
-}
-
-
-Content.propTypes = {
- page: PropTypes.object.isRequired,
- data: PropTypes.object
-};
diff --git a/diplomacy/web/src/gui/core/fancybox.jsx b/diplomacy/web/src/gui/core/fancybox.jsx
index 4d1013d..66a1efe 100644
--- a/diplomacy/web/src/gui/core/fancybox.jsx
+++ b/diplomacy/web/src/gui/core/fancybox.jsx
@@ -15,8 +15,8 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from 'react';
-import {Button} from "./widgets";
import PropTypes from 'prop-types';
+import {Button} from "./button";
const TIMES = '\u00D7';
diff --git a/diplomacy/web/src/gui/core/forms.jsx b/diplomacy/web/src/gui/core/forms.jsx
index 76d188c..da7250d 100644
--- a/diplomacy/web/src/gui/core/forms.jsx
+++ b/diplomacy/web/src/gui/core/forms.jsx
@@ -15,8 +15,8 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {Button} from "./widgets";
import {UTILS} from "../../diplomacy/utils/utils";
+import {Button} from "./button";
export class Forms {
static createOnChangeCallback(component, callback) {
diff --git a/diplomacy/web/src/gui/core/page.jsx b/diplomacy/web/src/gui/core/page.jsx
index 5ca09fd..ad830f1 100644
--- a/diplomacy/web/src/gui/core/page.jsx
+++ b/diplomacy/web/src/gui/core/page.jsx
@@ -18,22 +18,14 @@
import React from "react";
import {ContentConnection} from "../diplomacy/contents/content_connection";
-import {ContentGames} from "../diplomacy/contents/content_games";
-import {ContentGame} from "../diplomacy/contents/content_game";
import {UTILS} from "../../diplomacy/utils/utils";
import {Diplog} from "../../diplomacy/utils/diplog";
-import {STRINGS} from "../../diplomacy/utils/strings";
-import {Game} from "../../diplomacy/engine/game";
-import Octicon, {Person} from '@githubprimer/octicons-react';
-import $ from "jquery";
import {FancyBox} from "./fancybox";
import {DipStorage} from "../diplomacy/utils/dipStorage";
-
-const CONTENTS = {
- connection: ContentConnection,
- games: ContentGames,
- game: ContentGame
-};
+import {PageContext} from "../diplomacy/widgets/page_context";
+import {ContentGames} from "../diplomacy/contents/content_games";
+import {loadGameFromDisk} from "../diplomacy/utils/load_game_from_disk";
+import {ContentGame} from "../diplomacy/contents/content_game";
export class Page extends React.Component {
@@ -50,23 +42,18 @@ export class Page extends React.Component {
error: null,
info: null,
success: null,
- title: null,
// Page content parameters
- contentName: 'connection',
- contentData: null,
+ name: null,
+ body: null,
// Games.
games: {}, // Games found.
myGames: {} // Games locally stored.
};
- this.loadPage = this.loadPage.bind(this);
- this.loadConnection = this.loadConnection.bind(this);
- this.loadGames = this.loadGames.bind(this);
- this.loadGame = this.loadGame.bind(this);
- this.loadGameFromDisk = this.loadGameFromDisk.bind(this);
- this.logout = this.logout.bind(this);
this.error = this.error.bind(this);
this.info = this.info.bind(this);
this.success = this.success.bind(this);
+ this.logout = this.logout.bind(this);
+ this.loadGameFromDisk = this.loadGameFromDisk.bind(this);
this.unloadFancyBox = this.unloadFancyBox.bind(this);
}
@@ -80,26 +67,8 @@ export class Page extends React.Component {
return games;
}
- copyState(updatedFields) {
- return Object.assign({}, this.state, updatedFields || {});
- }
-
- //// Methods to check page type.
-
- __page_is(contentName, contentData) {
- return this.state.contentName === contentName && (!contentData || this.state.contentData === contentData);
- }
-
- pageIsConnection(contentData) {
- return this.__page_is('connection', contentData);
- }
-
- pageIsGames(contentData) {
- return this.__page_is('games', contentData);
- }
-
- pageIsGame(contentData) {
- return this.__page_is('game', contentData);
+ static defaultPage() {
+ return <ContentConnection/>;
}
//// Methods to load a global fancybox.
@@ -114,91 +83,39 @@ export class Page extends React.Component {
//// Methods to load a page.
- loadPage(contentName, contentData, messages) {
- messages = messages || {};
- messages.error = Page.wrapMessage(messages.error);
- messages.info = Page.wrapMessage(messages.info);
- messages.success = Page.wrapMessage(messages.success);
- Diplog.printMessages(messages);
- this.setState(this.copyState({
- error: messages.error,
- info: messages.info,
- success: messages.success,
- contentName: contentName,
- contentData: contentData,
- title: null,
- fancyTitle: null,
- onFancyBox: null
- }));
- }
-
- loadConnection(contentData, messages) {
- this.loadPage('connection', contentData, messages);
+ load(name, body, messages) {
+ const newState = {};
+ if (messages) {
+ for (let key of ['error', 'info', 'success'])
+ newState[key] = Page.wrapMessage(messages[key]);
+ }
+ Diplog.printMessages(newState);
+ newState.name = name;
+ newState.body = body;
+ this.setState(newState);
}
- loadGames(contentData, messages) {
- this.loadPage('games', contentData, messages);
+ loadGames(messages) {
+ this.load(
+ 'games',
+ <ContentGames myGames={this.getMyGames()} gamesFound={this.getGamesFound()}/>,
+ messages
+ );
}
- loadGame(gameInfo, messages) {
- this.loadPage('game', gameInfo, messages);
+ loadGameFromDisk() {
+ loadGameFromDisk(
+ (game) => this.load(
+ `game: ${game.game_id}`,
+ <ContentGame data={game}/>,
+ {success: `Game loaded from disk: ${game.game_id}`}
+ ),
+ this.error
+ );
}
- loadGameFromDisk() {
- const input = $(document.createElement('input'));
- input.attr("type", "file");
- input.trigger('click');
- input.change(event => {
- const file = event.target.files[0];
- if (!file.name.match(/\.json$/i)) {
- this.error(`Invalid JSON filename ${file.name}`);
- } else {
- const reader = new FileReader();
- reader.onload = () => {
- const savedData = JSON.parse(reader.result);
- const gameObject = {};
- gameObject.game_id = `(local) ${savedData.id}`;
- gameObject.map_name = savedData.map;
- gameObject.rules = savedData.rules;
- const state_history = {};
- const message_history = {};
- const order_history = {};
- const result_history = {};
- for (let savedPhase of savedData.phases) {
- const gameState = savedPhase.state;
- const phaseOrders = savedPhase.orders || {};
- const phaseResults = savedPhase.results || {};
- const phaseMessages = {};
- if (savedPhase.messages) {
- for (let message of savedPhase.messages) {
- phaseMessages[message.time_sent] = message;
- }
- }
- if (!gameState.name)
- gameState.name = savedPhase.name;
- state_history[gameState.name] = gameState;
- order_history[gameState.name] = phaseOrders;
- message_history[gameState.name] = phaseMessages;
- result_history[gameState.name] = phaseResults;
- }
- gameObject.state_history = state_history;
- gameObject.message_history = message_history;
- gameObject.order_history = order_history;
- gameObject.state_history = state_history;
- gameObject.result_history = result_history;
- gameObject.messages = [];
- gameObject.role = STRINGS.OBSERVER_TYPE;
- gameObject.status = STRINGS.COMPLETED;
- gameObject.timestamp_created = 0;
- gameObject.deadline = 0;
- gameObject.n_controls = 0;
- gameObject.registration_password = '';
- const game = new Game(gameObject);
- this.loadGame(game);
- };
- reader.readAsText(file);
- }
- });
+ getName() {
+ return this.state.name;
}
//// Methods to sign out channel and go back to connection page.
@@ -211,16 +128,16 @@ export class Page extends React.Component {
this.availableMaps = null;
const message = Page.wrapMessage(`Disconnected from channel and server.`);
Diplog.success(message);
- this.setState(this.copyState({
+ this.setState({
error: null,
info: null,
success: message,
- contentName: 'connection',
- contentData: null,
+ name: null,
+ body: null,
// When disconnected, remove all games previously loaded.
games: {},
myGames: {}
- }));
+ });
}
logout() {
@@ -236,10 +153,6 @@ export class Page extends React.Component {
//// Methods to be used to set page title and messages.
- setTitle(title) {
- this.setState({title: title});
- }
-
error(message) {
message = Page.wrapMessage(message);
Diplog.error(message);
@@ -306,11 +219,14 @@ export class Page extends React.Component {
if (game.client) {
game.client.leave()
.then(() => {
- this.disconnectGame(gameID);
- this.loadGames(null, {info: `Game ${gameID} left.`});
+ this.disconnectGame(gameID).then(() => {
+ this.loadGames({info: `Game ${gameID} left.`});
+ });
})
.catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`));
}
+ } else {
+ this.loadGames({info: `No game to left.`});
}
}
@@ -319,12 +235,13 @@ export class Page extends React.Component {
const game = this.state.myGames[gameID];
if (game.client)
game.client.clearAllCallbacks();
- this.channel.getGamesInfo({games: [gameID]})
+ return this.channel.getGamesInfo({games: [gameID]})
.then(gamesInfo => {
this.updateMyGames(gamesInfo);
})
.catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`));
}
+ return null;
}
addToMyGames(game) {
@@ -335,7 +252,7 @@ export class Page extends React.Component {
if (gamesFound.hasOwnProperty(game.game_id))
gamesFound[game.game_id] = game;
DipStorage.addUserGame(this.channel.username, game.game_id);
- this.setState({myGames: myGames, games: gamesFound});
+ this.setState({myGames: myGames, games: gamesFound}, () => this.loadGames());
}
removeFromMyGames(gameID) {
@@ -343,7 +260,7 @@ export class Page extends React.Component {
const games = Object.assign({}, this.state.myGames);
delete games[gameID];
DipStorage.removeUserGame(this.channel.username, gameID);
- this.setState({myGames: games});
+ this.setState({myGames: games}, () => this.loadGames());
}
}
@@ -354,81 +271,37 @@ export class Page extends React.Component {
//// Render method.
render() {
- const content = CONTENTS[this.state.contentName].builder(this, this.state.contentData);
- const hasNavigation = UTILS.javascript.hasArray(content.navigation);
-
- // NB: I currently don't find a better way to update document title from content details.
const successMessage = this.state.success || '-';
const infoMessage = this.state.info || '-';
const errorMessage = this.state.error || '-';
- const title = this.state.title || content.title;
- document.title = title + ' | Diplomacy';
-
return (
- <div className="page container-fluid" id={this.state.contentName}>
- <div className={'top-msg row'}>
- <div title={successMessage !== '-' ? successMessage : ''}
- className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')}
- onClick={() => this.success()}>
- {successMessage}
- </div>
- <div title={infoMessage !== '-' ? infoMessage : ''}
- className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')}
- onClick={() => this.info()}>
- {infoMessage}
- </div>
- <div title={errorMessage !== '-' ? errorMessage : ''}
- className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')}
- onClick={() => this.error()}>
- {errorMessage}
- </div>
- </div>
- {((hasNavigation || this.channel) && (
- <div className={'title row'}>
- <div className={'col align-self-center'}><strong>{title}</strong></div>
- <div className={'col-sm-1'}>
- {(!hasNavigation && (
- <div className={'float-right'}>
- <strong>
- <u className={'mr-2'}>{this.channel.username}</u>
- <Octicon icon={Person}/>
- </strong>
- </div>
- )) || (
- <div className="dropdown float-right">
- <button className="btn btn-secondary dropdown-toggle" type="button"
- id="dropdownMenuButton" data-toggle="dropdown"
- aria-haspopup="true" aria-expanded="false">
- {(this.channel && this.channel.username && (
- <span>
- <u className={'mr-2'}>{this.channel.username}</u>
- <Octicon icon={Person}/>
- </span>
- )) || 'Menu'}
- </button>
- <div className="dropdown-menu dropdown-menu-right"
- aria-labelledby="dropdownMenuButton">
- {content.navigation.map((nav, index) => {
- const navTitle = nav[0];
- const navAction = nav[1];
- return <a key={index} className="dropdown-item"
- onClick={navAction}>{navTitle}</a>;
- })}
- </div>
- </div>
- )}
+ <PageContext.Provider value={this}>
+ <div className="page container-fluid" id={this.state.contentName}>
+ <div className={'top-msg row'}>
+ <div title={successMessage !== '-' ? successMessage : ''}
+ className={'col-sm-4 msg success ' + (this.state.success ? 'with-msg' : 'no-msg')}
+ onClick={() => this.success()}>
+ {successMessage}
+ </div>
+ <div title={infoMessage !== '-' ? infoMessage : ''}
+ className={'col-sm-4 msg info ' + (this.state.info ? 'with-msg' : 'no-msg')}
+ onClick={() => this.info()}>
+ {infoMessage}
+ </div>
+ <div title={errorMessage !== '-' ? errorMessage : ''}
+ className={'col-sm-4 msg error ' + (this.state.error ? 'with-msg' : 'no-msg')}
+ onClick={() => this.error()}>
+ {errorMessage}
</div>
</div>
- )) || (
- <div className={'title'}><strong>{title}</strong></div>
- )}
- {content.component}
- {this.state.onFancyBox && (
- <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}>
- {this.state.onFancyBox()}
- </FancyBox>
- )}
- </div>
+ {this.state.body || Page.defaultPage()}
+ {this.state.onFancyBox && (
+ <FancyBox title={this.state.fancyTitle} onClose={this.unloadFancyBox}>
+ {this.state.onFancyBox()}
+ </FancyBox>
+ )}
+ </div>
+ </PageContext.Provider>
);
}
}
diff --git a/diplomacy/web/src/gui/core/tab.jsx b/diplomacy/web/src/gui/core/tab.jsx
new file mode 100644
index 0000000..f1ad4aa
--- /dev/null
+++ b/diplomacy/web/src/gui/core/tab.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+export class Tab extends React.Component {
+ render() {
+ const style = {
+ display: this.props.display ? 'block' : 'none'
+ };
+ const id = this.props.id ? {id: this.props.id} : {};
+ return (
+ <div className={'tab mb-4 ' + this.props.className} style={style} {...id}>
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+Tab.propTypes = {
+ display: PropTypes.bool,
+ className: PropTypes.string,
+ id: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
+};
+
+Tab.defaultProps = {
+ display: false,
+ className: '',
+ id: ''
+};
diff --git a/diplomacy/web/src/gui/core/tabs.jsx b/diplomacy/web/src/gui/core/tabs.jsx
index 6123219..a3f6b9b 100644
--- a/diplomacy/web/src/gui/core/tabs.jsx
+++ b/diplomacy/web/src/gui/core/tabs.jsx
@@ -15,36 +15,9 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {Action} from "./widgets";
+import {Action} from "./action";
import PropTypes from 'prop-types';
-export class Tab extends React.Component {
- render() {
- const style = {
- display: this.props.display ? 'block' : 'none'
- };
- const id = this.props.id ? {id: this.props.id} : {};
- return (
- <div className={'tab mb-4 ' + this.props.className} style={style} {...id}>
- {this.props.children}
- </div>
- );
- }
-}
-
-Tab.propTypes = {
- display: PropTypes.bool,
- className: PropTypes.string,
- id: PropTypes.string,
- children: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
-};
-
-Tab.defaultProps = {
- display: false,
- className: '',
- id: ''
-};
-
export class Tabs extends React.Component {
/** PROPERTIES
* active: index of active menu (must be > menu.length).
diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx
index 8aa7fb1..8c952a4 100644
--- a/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx
+++ b/diplomacy/web/src/gui/diplomacy/contents/content_connection.jsx
@@ -15,28 +15,22 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from 'react';
-import {Content} from "../../core/content";
import {Connection} from "../../../diplomacy/client/connection";
import {ConnectionForm} from "../forms/connection_form";
import {DipStorage} from "../utils/dipStorage";
+import {Helmet} from "react-helmet";
+import {Navigation} from "../widgets/navigation";
+import {PageContext} from "../widgets/page_context";
-export class ContentConnection extends Content {
+export class ContentConnection extends React.Component {
constructor(props) {
super(props);
this.connection = null;
this.onSubmit = this.onSubmit.bind(this);
}
- static builder(page, data) {
- return {
- title: 'Connection',
- navigation: [],
- component: <ContentConnection page={page} data={data}/>
- };
- }
-
onSubmit(data) {
- const page = this.getPage();
+ const page = this.context;
for (let fieldName of ['hostname', 'port', 'username', 'password', 'showServerFields'])
if (!data.hasOwnProperty(fieldName))
return page.error(`Missing ${fieldName}, got ${JSON.stringify(data)}`);
@@ -46,7 +40,7 @@ export class ContentConnection extends Content {
}
this.connection = new Connection(data.hostname, data.port, window.location.protocol.toLowerCase() === 'https:');
// Page is passed as logger object (with methods info(), error(), success()) when connecting.
- this.connection.connect(this.getPage())
+ this.connection.connect(page)
.then(() => {
page.connection = this.connection;
this.connection = null;
@@ -71,10 +65,10 @@ export class ContentConnection extends Content {
})
.then((gamesInfo) => {
if (gamesInfo) {
- this.getPage().success('Found ' + gamesInfo.length + ' user games.');
- this.getPage().updateMyGames(gamesInfo);
+ page.success('Found ' + gamesInfo.length + ' user games.');
+ page.updateMyGames(gamesInfo);
}
- page.loadGames(null, {success: `Account ${data.username} connected.`});
+ page.loadGames({success: `Account ${data.username} connected.`});
})
.catch((error) => {
page.error('Error while authenticating: ' + error + ' Please re-try.');
@@ -86,6 +80,21 @@ export class ContentConnection extends Content {
}
render() {
- return <main><ConnectionForm onSubmit={this.onSubmit}/></main>;
+ const title = 'Connection';
+ return (
+ <main>
+ <Helmet>
+ <title>{title} | Diplomacy</title>
+ </Helmet>
+ <Navigation title={title}/>
+ <ConnectionForm onSubmit={this.onSubmit}/>
+ </main>
+ );
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
}
}
+
+ContentConnection.contextType = PageContext; \ No newline at end of file
diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx
index 81a689d..b3d933b 100644
--- a/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx
+++ b/diplomacy/web/src/gui/diplomacy/contents/content_game.jsx
@@ -19,10 +19,8 @@ import Scrollchor from 'react-scrollchor';
import {SelectLocationForm} from "../forms/select_location_form";
import {SelectViaForm} from "../forms/select_via_form";
import {Order} from "../utils/order";
-import {Button} from "../../core/widgets";
import {Bar, Row} from "../../core/layouts";
-import {Content} from "../../core/content";
-import {Tab, Tabs} from "../../core/tabs";
+import {Tabs} from "../../core/tabs";
import {Map} from "../map/map";
import {extendOrderBuilding, ORDER_BUILDER, POSSIBLE_ORDERS} from "../utils/order_building";
import {PowerActionsForm} from "../forms/power_actions_form";
@@ -37,6 +35,13 @@ import {Table} from "../../core/table";
import {PowerView} from "../utils/power_view";
import {FancyBox} from "../../core/fancybox";
import {DipStorage} from "../utils/dipStorage";
+import Helmet from 'react-helmet';
+import {Navigation} from "../widgets/navigation";
+import {PageContext} from "../widgets/page_context";
+import PropTypes from 'prop-types';
+import {Help} from "../widgets/help";
+import {Tab} from "../../core/tab";
+import {Button} from "../../core/button";
const HotKey = require('react-shortcut');
@@ -63,19 +68,13 @@ const TABLE_POWER_VIEW = {
wait: ['Waiting', 3]
};
-function Help() {
- return (
- <div>
- <p>When building an order, press <strong>ESC</strong> to reset build.</p>
- <p>Press letter associated to an order type to start building an order of this type.
- <br/> Order type letter is indicated in order type name after order type radio button.
- </p>
- <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p>
- </div>
- );
+function gameReloaded(game, updates) {
+ if (updates)
+ return Object.assign({}, updates, game);
+ return Object.assign({}, game);
}
-export class ContentGame extends Content {
+export class ContentGame extends React.Component {
constructor(props) {
super(props);
@@ -109,7 +108,6 @@ export class ContentGame extends Content {
messageHighlights: {},
historyPhaseIndex: null,
historyShowOrders: true,
- historySubView: 0,
historyCurrentLoc: null,
historyCurrentOrders: null,
wait: null, // {power name => bool}
@@ -191,21 +189,6 @@ export class ContentGame extends Content {
}
}
- static builder(page, data) {
- return {
- title: ContentGame.gameTitle(data),
- navigation: [
- ['Help', () => page.loadFancyBox('Help', () => <Help/>)],
- ['Load a game from disk', page.loadGameFromDisk],
- ['Save game to disk', () => ContentGame.saveGameToDisk(data)],
- [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Games`, page.loadGames],
- [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Leave game`, () => page.leaveGame(data.game_id)],
- [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Logout`, page.logout]
- ],
- component: <ContentGame page={page} data={data}/>
- };
- }
-
static getServerWaitFlags(engine) {
const wait = {};
const controllablePowers = engine.getControllablePowers();
@@ -326,7 +309,7 @@ export class ContentGame extends Content {
}
getMapInfo() {
- return this.props.page.availableMaps[this.props.data.map_name];
+ return this.getPage().availableMaps[this.props.data.map_name];
}
clearScheduleTimeout() {
@@ -343,7 +326,7 @@ export class ContentGame extends Content {
engine.deadline_timer = 0;
this.clearScheduleTimeout();
}
- this.getPage().setTitle(ContentGame.gameTitle(engine));
+ this.getPage().load(`game: ${engine.game_id}`, <ContentGame data={gameReloaded(engine)}/>);
}
reloadDeadlineTimer(networkGame) {
@@ -366,13 +349,17 @@ export class ContentGame extends Content {
}
networkGameIsDisplayed(networkGame) {
- return this.getPage().pageIsGame(networkGame.local);
+ return this.getPage().getName() === `game: ${networkGame.local.game_id}`;
}
notifiedNetworkGame(networkGame, notification) {
if (this.networkGameIsDisplayed(networkGame)) {
const msg = `Game (${networkGame.local.game_id}) received notification ${notification.name}.`;
- this.props.page.loadGame(networkGame.local, {info: msg});
+ this.getPage().load(
+ `game: ${networkGame.local.game_id}`,
+ <ContentGame data={networkGame.local}/>,
+ {info: msg}
+ );
this.reloadDeadlineTimer(networkGame);
}
}
@@ -383,10 +370,11 @@ export class ContentGame extends Content {
|| !networkGame.channel.game_id_to_instances[networkGame.local.game_id].has(networkGame.local.role)
)) {
// This power game is now invalid.
- this.props.page.disconnectGame(networkGame.local.game_id);
+ this.getPage().disconnectGame(networkGame.local.game_id);
if (this.networkGameIsDisplayed(networkGame)) {
- this.props.page.loadGames(null,
- {error: `Player game ${networkGame.local.game_id}/${networkGame.local.role} was kicked. Deadline over?`});
+ const page = this.getPage();
+ page.loadGames(
+ {error: `${networkGame.local.game_id}/${networkGame.local.role} was kicked. Deadline over?`});
}
} else {
this.notifiedNetworkGame(networkGame, notification);
@@ -398,8 +386,10 @@ export class ContentGame extends Content {
.then(allPossibleOrders => {
networkGame.local.setPossibleOrders(allPossibleOrders);
if (this.networkGameIsDisplayed(networkGame)) {
- this.getPage().loadGame(
- networkGame.local, {info: `Game update (${notification.name}) to ${networkGame.local.phase}.`}
+ this.getPage().load(
+ `game: ${networkGame.local.game_id}`,
+ <ContentGame data={networkGame.local}/>,
+ {info: `Game update (${notification.name}) to ${networkGame.local.phase}.`}
);
this.__store_orders(null);
this.setState({orders: null, wait: null, messageHighlights: {}});
@@ -414,8 +404,10 @@ export class ContentGame extends Content {
.then(allPossibleOrders => {
networkGame.local.setPossibleOrders(allPossibleOrders);
if (this.networkGameIsDisplayed(networkGame)) {
- this.getPage().loadGame(
- networkGame.local, {info: `Possible orders re-loaded.`}
+ this.getPage().load(
+ `game: ${networkGame.local.game_id}`,
+ <ContentGame data={networkGame.local}/>,
+ {info: `Possible orders re-loaded.`}
);
this.reloadDeadlineTimer(networkGame);
}
@@ -459,7 +451,7 @@ export class ContentGame extends Content {
}
onChangeCurrentPower(event) {
- this.setState({power: event.target.value});
+ this.setState({power: event.target.value, tabPastMessages: null, tabCurrentMessages: null});
}
onChangeMainTab(tab) {
@@ -482,10 +474,14 @@ export class ContentGame extends Content {
recipient: recipient,
message: body
});
- const page = this.props.page;
+ const page = this.getPage();
networkGame.sendGameMessage({message: message})
.then(() => {
- page.loadGame(engine, {success: `Message sent: ${JSON.stringify(message)}`});
+ page.load(
+ `game: ${engine.game_id}`,
+ <ContentGame data={engine}/>,
+ {success: `Message sent: ${JSON.stringify(message)}`}
+ );
})
.catch(error => page.error(error.toString()));
}
@@ -560,10 +556,10 @@ export class ContentGame extends Content {
Diplog.info('Sending orders for ' + powerName + ': ' + JSON.stringify(localPowerOrders));
this.props.data.client.setOrders({power_name: powerName, orders: localPowerOrders || []})
.then(() => {
- this.props.page.success('Orders sent.');
+ this.getPage().success('Orders sent.');
})
.catch(err => {
- this.props.page.error(err.toString());
+ this.getPage().error(err.toString());
})
.then(() => {
this.reloadServerOrders();
@@ -572,10 +568,11 @@ export class ContentGame extends Content {
}
onProcessGame() {
+ const page = this.getPage();
this.props.data.client.process()
- .then(() => this.props.page.success('Game processed.'))
+ .then(() => page.success('Game processed.'))
.catch(err => {
- this.props.page.error(err.toString());
+ page.error(err.toString());
});
}
@@ -604,7 +601,7 @@ export class ContentGame extends Content {
onOrderBuilding(powerName, path) {
const pathToSave = path.slice(1);
- this.props.page.success(`Building order ${pathToSave.join(' ')} ...`);
+ this.getPage().success(`Building order ${pathToSave.join(' ')} ...`);
this.setState({orderBuildingPath: pathToSave});
}
@@ -632,7 +629,7 @@ export class ContentGame extends Content {
allOrders[powerName] = {};
allOrders[powerName][localOrder.loc] = localOrder;
state.orders = allOrders;
- this.props.page.success(`Built order: ${orderString}`);
+ this.getPage().success(`Built order: ${orderString}`);
this.__store_orders(allOrders);
this.setState(state);
}
@@ -684,10 +681,9 @@ export class ContentGame extends Content {
});
}
- __change_past_phase(newPhaseIndex, subView) {
+ __change_past_phase(newPhaseIndex) {
this.setState({
historyPhaseIndex: newPhaseIndex,
- historySubView: (subView ? subView : 0),
historyCurrentLoc: null,
historyCurrentOrders: null
});
@@ -700,16 +696,6 @@ export class ContentGame extends Content {
onChangePastPhaseIndex(increment) {
const selectObject = document.getElementById('select-past-phase');
if (selectObject) {
- if (!this.state.historyShowOrders) {
- // We must change map sub-view before showed phase index.
- const currentSubView = this.state.historySubView;
- const newSubView = currentSubView + (increment ? 1 : -1);
- if (newSubView === 0 || newSubView === 1) {
- // Sub-view correctly updated. We don't yet change showed phase.
- return this.setState({historySubView: newSubView});
- }
- // Sub-view badly updated (either from 0 to -1, or from 1 to 2). We must change phase.
- }
// Let's simply increase or decrease index of showed past phase.
const index = selectObject.selectedIndex;
const newIndex = index + (increment ? 1 : -1);
@@ -741,7 +727,7 @@ export class ContentGame extends Content {
}
onChangeShowPastOrders(event) {
- this.setState({historyShowOrders: event.target.checked, historySubView: 0});
+ this.setState({historyShowOrders: event.target.checked});
}
renderOrders(engine, currentPowerName) {
@@ -763,7 +749,7 @@ export class ContentGame extends Content {
let protagonist = message.sender;
if (message.recipient === 'GLOBAL')
protagonist = message.recipient;
- this.getPage().loadGame(this.props.data);
+ this.getPage().load(`game: ${this.props.data.game_id}`, <ContentGame data={this.props.data}/>);
if (this.state.messageHighlights.hasOwnProperty(protagonist) && this.state.messageHighlights[protagonist] > 0) {
const messageHighlights = Object.assign({}, this.state.messageHighlights);
--messageHighlights[protagonist];
@@ -779,31 +765,28 @@ export class ContentGame extends Content {
});
}
- renderPastMessages(engine) {
- const messageChannels = engine.getMessageChannels();
- let tabNames = null;
- if (engine.isPlayerGame()) {
- tabNames = [];
- for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role)
- tabNames.push(powerName);
- tabNames.sort();
- tabNames.push('GLOBAL');
- } else {
- tabNames = Object.keys(messageChannels);
- }
+ renderPastMessages(engine, role) {
+ const messageChannels = engine.getMessageChannels(role, true);
+ const tabNames = [];
+ for (let powerName of Object.keys(engine.powers)) if (powerName !== role)
+ tabNames.push(powerName);
+ tabNames.sort();
+ tabNames.push('GLOBAL');
+ const titles = tabNames.map(tabName => (tabName === 'GLOBAL' ? tabName : tabName.substr(0, 3)));
const currentTabId = this.state.tabPastMessages || tabNames[0];
return (
<div className={'panel-messages'} key={'panel-messages'}>
{/* Messages. */}
- <Tabs menu={tabNames} titles={tabNames} onChange={this.onChangeTabPastMessages} active={currentTabId}>
+ <Tabs menu={tabNames} titles={titles} onChange={this.onChangeTabPastMessages} active={currentTabId}>
{tabNames.map(protagonist => (
<Tab key={protagonist} className={'game-messages'} display={currentTabId === protagonist}>
{(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ?
(<div className={'no-game-message'}>No
messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.</div>) :
messageChannels[protagonist].map((message, index) => (
- <MessageView key={index} owner={engine.role} message={message} read={true}/>
+ <MessageView key={index} phase={engine.phase} owner={role} message={message}
+ read={true}/>
))
)}
</Tab>
@@ -813,51 +796,41 @@ export class ContentGame extends Content {
);
}
- renderCurrentMessages(engine) {
- const messageChannels = engine.getMessageChannels();
- let tabNames = null;
- let highlights = null;
- if (engine.isPlayerGame()) {
- tabNames = [];
- for (let powerName of Object.keys(engine.powers)) if (powerName !== engine.role)
- tabNames.push(powerName);
- tabNames.sort();
- tabNames.push('GLOBAL');
- highlights = this.state.messageHighlights;
- } else {
- tabNames = Object.keys(messageChannels);
- let totalHighlights = 0;
- for (let count of Object.values(this.state.messageHighlights))
- totalHighlights += count;
- highlights = {messages: totalHighlights};
- }
- const unreadMarked = new Set();
+ renderCurrentMessages(engine, role) {
+ const messageChannels = engine.getMessageChannels(role, true);
+ const tabNames = [];
+ for (let powerName of Object.keys(engine.powers)) if (powerName !== role)
+ tabNames.push(powerName);
+ tabNames.sort();
+ tabNames.push('GLOBAL');
+ const titles = tabNames.map(tabName => (tabName === 'GLOBAL' ? tabName : tabName.substr(0, 3)));
const currentTabId = this.state.tabCurrentMessages || tabNames[0];
+ const highlights = this.state.messageHighlights;
+ const unreadMarked = new Set();
return (
<div className={'panel-messages'} key={'panel-messages'}>
{/* Messages. */}
- <Tabs menu={tabNames} titles={tabNames} onChange={this.onChangeTabCurrentMessages} active={currentTabId}
+ <Tabs menu={tabNames} titles={titles} onChange={this.onChangeTabCurrentMessages} active={currentTabId}
highlights={highlights}>
{tabNames.map(protagonist => (
- <Tab id={`panel-current-messages-${protagonist}`} key={protagonist} className={'game-messages'}
- display={currentTabId === protagonist}>
+ <Tab key={protagonist} className={'game-messages'} display={currentTabId === protagonist}
+ id={`panel-current-messages-${protagonist}`}>
{(!messageChannels.hasOwnProperty(protagonist) || !messageChannels[protagonist].length ?
(<div className={'no-game-message'}>No
messages{engine.isPlayerGame() ? ` with ${protagonist}` : ''}.</div>) :
(messageChannels[protagonist].map((message, index) => {
let id = null;
if (!message.read && !unreadMarked.has(protagonist)) {
- if (engine.isOmniscientGame() || message.sender !== engine.role) {
+ if (engine.isOmniscientGame() || message.sender !== role) {
unreadMarked.add(protagonist);
id = `${protagonist}-unread`;
}
}
- return <MessageView key={index}
- owner={engine.role}
+ return <MessageView key={index} phase={engine.phase} owner={role}
message={message}
- id={id}
- onClick={this.onClickMessage}/>;
+ read={message.phase !== engine.phase}
+ id={id} onClick={this.onClickMessage}/>;
}))
)}
</Tab>
@@ -873,13 +846,13 @@ export class ContentGame extends Content {
)}
{/* Send form. */}
{engine.isPlayerGame() && (
- <MessageForm sender={engine.role} recipient={currentTabId} onSubmit={form =>
+ <MessageForm sender={role} recipient={currentTabId} onSubmit={form =>
this.sendMessage(engine.client, currentTabId, form.message)}/>)}
</div>
);
}
- renderPastMap(gameEngine, showOrders) {
+ renderMapForResults(gameEngine, showOrders) {
return <Map key={'past-map'}
id={'past-map'}
game={gameEngine}
@@ -891,7 +864,19 @@ export class ContentGame extends Content {
/>;
}
- renderCurrentMap(gameEngine, powerName, orderType, orderPath) {
+ renderMapForMessages(gameEngine, showOrders) {
+ return <Map key={'messages-map'}
+ id={'messages-map'}
+ game={gameEngine}
+ mapInfo={this.getMapInfo(gameEngine.map_name)}
+ onError={this.getPage().error}
+ onHover={showOrders ? this.displayLocationOrders : null}
+ showOrders={Boolean(showOrders)}
+ orders={(gameEngine.order_history.contains(gameEngine.phase) && gameEngine.order_history.get(gameEngine.phase)) || null}
+ />;
+ }
+
+ renderMapForCurrent(gameEngine, powerName, orderType, orderPath) {
const rawOrders = this.__get_orders(gameEngine);
const orders = {};
for (let entry of Object.entries(rawOrders)) {
@@ -915,27 +900,52 @@ export class ContentGame extends Content {
onSelectVia={this.onSelectVia}/>;
}
- renderTabPhaseHistory(toDisplay, initialEngine) {
+ __get_engine_to_display(initialEngine) {
const pastPhases = initialEngine.state_history.values().map(state => state.name);
- if (initialEngine.phase === 'COMPLETED') {
- pastPhases.push('COMPLETED');
- }
+ pastPhases.push(initialEngine.phase);
let phaseIndex = 0;
if (initialEngine.displayed) {
if (this.state.historyPhaseIndex === null || this.state.historyPhaseIndex >= pastPhases.length) {
phaseIndex = pastPhases.length - 1;
+ } else if (this.state.historyPhaseIndex < 0) {
+ phaseIndex = pastPhases.length + this.state.historyPhaseIndex;
} else {
- if (this.state.historyPhaseIndex < 0) {
- phaseIndex = pastPhases.length + this.state.historyPhaseIndex;
- } else {
- phaseIndex = this.state.historyPhaseIndex;
- }
+ phaseIndex = this.state.historyPhaseIndex;
}
}
const engine = (
- phaseIndex === initialEngine.state_history.size() ?
- initialEngine : initialEngine.cloneAt(initialEngine.state_history.keyFromIndex(phaseIndex))
+ pastPhases[phaseIndex] === initialEngine.phase ?
+ initialEngine : initialEngine.cloneAt(pastPhases[phaseIndex])
+ );
+ return {engine, pastPhases, phaseIndex};
+ }
+
+ __form_phases(pastPhases, phaseIndex) {
+ return (
+ <form key={1} className={'form-inline mb-4'}>
+ <Button title={UTILS.html.UNICODE_LEFT_ARROW} onClick={this.onDecrementPastPhase} pickEvent={true}
+ disabled={phaseIndex === 0}/>
+ <div className="form-group mx-1">
+ <select className={'form-control custom-select'}
+ id={'select-past-phase'}
+ value={phaseIndex}
+ onChange={this.onChangePastPhase}>
+ {pastPhases.map((phaseName, index) => <option key={index} value={index}>{phaseName}</option>)}
+ </select>
+ </div>
+ <Button title={UTILS.html.UNICODE_RIGHT_ARROW} onClick={this.onIncrementPastPhase} pickEvent={true}
+ disabled={phaseIndex === pastPhases.length - 1}/>
+ <div className="form-group mx-1">
+ <input className={'form-check-input'} id={'show-orders'} type={'checkbox'}
+ checked={this.state.historyShowOrders} onChange={this.onChangeShowPastOrders}/>
+ <label className={'form-check-label'} htmlFor={'show-orders'}>Show orders</label>
+ </div>
+ </form>
);
+ }
+
+ renderTabResults(toDisplay, initialEngine) {
+ const {engine, pastPhases, phaseIndex} = this.__get_engine_to_display(initialEngine);
let orders = {};
let orderResult = null;
if (engine.order_history.contains(engine.phase))
@@ -971,27 +981,8 @@ export class ContentGame extends Content {
};
const orderView = [
- (<form key={1} className={'form-inline mb-4'}>
- <Button title={UTILS.html.UNICODE_LEFT_ARROW} onClick={this.onDecrementPastPhase} pickEvent={true}
- disabled={phaseIndex === 0}/>
- <div className={'form-group'}>
- <select className={'form-control custom-select'}
- id={'select-past-phase'}
- value={phaseIndex}
- onChange={this.onChangePastPhase}>
- {pastPhases.map((phaseName, index) => <option key={index} value={index}>{phaseName}</option>)}
- </select>
- </div>
- <Button title={UTILS.html.UNICODE_RIGHT_ARROW} onClick={this.onIncrementPastPhase} pickEvent={true}
- disabled={phaseIndex === pastPhases.length - 1}/>
- <div className={'form-group'}>
- <input className={'form-check-input'} id={'show-orders'} type={'checkbox'}
- checked={this.state.historyShowOrders} onChange={this.onChangeShowPastOrders}/>
- <label className={'form-check-label'} htmlFor={'show-orders'}>Show orders</label>
- </div>
- </form>),
- ((this.state.historyShowOrders && (
- (countOrders && (
+ this.__form_phases(pastPhases, phaseIndex),
+ (((countOrders && (
<div key={2} className={'past-orders container'}>
{powerNames.map(powerName => !orders[powerName] || !orders[powerName].length ? '' : (
<div key={powerName} className={'row'}>
@@ -1005,22 +996,30 @@ export class ContentGame extends Content {
))}
</div>
)) || <div key={2} className={'no-orders'}>No orders for this phase!</div>
- )) || '')
+ ))
];
- const messageView = this.renderPastMessages(engine);
- let detailsView = null;
- if (this.state.historyShowOrders && countOrders) {
- detailsView = (
+ return (
+ <Tab id={'tab-phase-history'} display={toDisplay}>
<Row>
- <div className={'col-sm-6'}>{orderView}</div>
- <div className={'col-sm-6'}>{messageView}</div>
+ <div className={'col-xl'}>
+ {this.state.historyCurrentOrders && (
+ <div className={'history-current-orders'}>{this.state.historyCurrentOrders.join(', ')}</div>
+ )}
+ {this.renderMapForResults(engine, this.state.historyShowOrders)}
+ </div>
+ <div className={'col-xl'}>{orderView}</div>
</Row>
- );
- } else {
- detailsView = orderView.slice();
- detailsView.push(messageView);
- }
+ {toDisplay && <HotKey keys={['arrowleft']} onKeysCoincide={this.onDecrementPastPhase}/>}
+ {toDisplay && <HotKey keys={['arrowright']} onKeysCoincide={this.onIncrementPastPhase}/>}
+ {toDisplay && <HotKey keys={['home']} onKeysCoincide={this.displayFirstPastPhase}/>}
+ {toDisplay && <HotKey keys={['end']} onKeysCoincide={this.displayLastPastPhase}/>}
+ </Tab>
+ );
+ }
+
+ renderTabMessages(toDisplay, initialEngine, currentPowerName) {
+ const {engine, pastPhases, phaseIndex} = this.__get_engine_to_display(initialEngine);
return (
<Tab id={'tab-phase-history'} display={toDisplay}>
@@ -1029,9 +1028,16 @@ export class ContentGame extends Content {
{this.state.historyCurrentOrders && (
<div className={'history-current-orders'}>{this.state.historyCurrentOrders.join(', ')}</div>
)}
- {this.renderPastMap(engine, this.state.historyShowOrders || this.state.historySubView)}
+ {this.renderMapForMessages(engine, this.state.historyShowOrders)}
+ </div>
+ <div className={'col-xl'}>
+ {this.__form_phases(pastPhases, phaseIndex)}
+ {pastPhases[phaseIndex] === initialEngine.phase ? (
+ this.renderCurrentMessages(initialEngine, currentPowerName)
+ ) : (
+ this.renderPastMessages(engine, currentPowerName)
+ )}
</div>
- <div className={'col-xl'}>{detailsView}</div>
</Row>
{toDisplay && <HotKey keys={['arrowleft']} onKeysCoincide={this.onDecrementPastPhase}/>}
{toDisplay && <HotKey keys={['arrowright']} onKeysCoincide={this.onIncrementPastPhase}/>}
@@ -1041,7 +1047,7 @@ export class ContentGame extends Content {
);
}
- renderTabCurrentPhase(toDisplay, engine, powerName, orderType, orderPath) {
+ renderTabCurrentPhase(toDisplay, engine, powerName, orderType, orderPath, currentPowerName, currentTabOrderCreation) {
const powerNames = Object.keys(engine.powers);
powerNames.sort();
const orderedPowers = powerNames.map(pn => engine.powers[pn]);
@@ -1049,11 +1055,12 @@ export class ContentGame extends Content {
<Tab id={'tab-current-phase'} display={toDisplay}>
<Row>
<div className={'col-xl'}>
- {this.renderCurrentMap(engine, powerName, orderType, orderPath)}
+ {this.renderMapForCurrent(engine, powerName, orderType, orderPath)}
</div>
<div className={'col-xl'}>
{/* Orders. */}
<div className={'panel-orders mb-4'}>
+ {currentTabOrderCreation ? <div className="mb-4">{currentTabOrderCreation}</div> : ''}
<Bar className={'p-2'}>
<strong className={'mr-4'}>Orders:</strong>
<Button title={'reset'} onClick={this.reloadServerOrders}/>
@@ -1072,16 +1079,29 @@ export class ContentGame extends Content {
wrapper={PowerView.wrap}/>
</div>
</div>
- {/* Messages. */}
- {this.renderCurrentMessages(engine)}
</div>
</Row>
</Tab>
);
}
+ getPage() {
+ return this.context;
+ }
+
render() {
+ this.props.data.displayed = true;
+ const page = this.context;
const engine = this.props.data;
+ const title = ContentGame.gameTitle(engine);
+ const navigation = [
+ ['Help', () => page.loadFancyBox('Help', () => <Help/>)],
+ ['Load a game from disk', page.loadGameFromDisk],
+ ['Save game to disk', () => ContentGame.saveGameToDisk(engine)],
+ [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Games`, () => page.loadGames()],
+ [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Leave game`, () => page.leaveGame(engine.game_id)],
+ [`${UTILS.html.UNICODE_SMALL_LEFT_ARROW} Logout`, page.logout]
+ ];
const phaseType = engine.getPhaseType();
const controllablePowers = engine.getControllablePowers();
if (this.props.data.client)
@@ -1099,12 +1119,14 @@ export class ContentGame extends Content {
if (engine.state_history.size()) {
hasTabPhaseHistory = true;
tabNames.push('phase_history');
- tabTitles.push('Phase history');
+ tabTitles.push('Results');
}
- if (controllablePowers.length && phaseType) {
+ tabNames.push('messages');
+ tabTitles.push('Messages');
+ if (controllablePowers.length && phaseType && !engine.isObserverGame()) {
hasTabCurrentPhase = true;
tabNames.push('current_phase');
- tabTitles.push('Current phase');
+ tabTitles.push('Current');
}
if (!tabNames.length) {
// This should never happen, but let's display this message.
@@ -1135,64 +1157,72 @@ export class ContentGame extends Content {
buildCount = engine.getBuildsCount(currentPowerName);
}
- return (
- <main>
- {(hasTabCurrentPhase && (
- <div className={'row align-items-center mb-3'}>
- <div className={'col-sm-2'}>
- {(controllablePowers.length === 1 &&
- <div className={'power-name'}>{controllablePowers[0]}</div>) || (
- <select className={'form-control custom-select'} id={'current-power'}
- value={currentPowerName} onChange={this.onChangeCurrentPower}>
- {controllablePowers.map(
- powerName => <option key={powerName} value={powerName}>{powerName}</option>)}
- </select>
- )}
- </div>
- <div className={'col-sm-10'}>
- <PowerActionsForm orderType={orderBuildingType}
- orderTypes={allowedPowerOrderTypes}
- onChange={this.onChangeOrderType}
- onNoOrders={() => this.onSetNoOrders(currentPowerName)}
- onSetWaitFlag={() => this.setWaitFlag(!currentPower.wait)}
- onVote={this.vote}
- role={engine.role}
- power={currentPower}/>
- </div>
- </div>
- )) || ''}
- {(hasTabCurrentPhase && (
- <div>
- {(allowedPowerOrderTypes.length && (
- <span>
+ const navAfterTitle = (
+ (controllablePowers.length === 1 &&
+ <span className="power-name">{controllablePowers[0]}</span>) || (
+ <form className="form-inline form-current-power">
+ <select className="form-control custom-select custom-control-inline" id="current-power"
+ value={currentPowerName} onChange={this.onChangeCurrentPower}>
+ {controllablePowers.map(
+ powerName => <option key={powerName} value={powerName}>{powerName}</option>)}
+ </select>
+ </form>
+ )
+ );
+
+ const currentTabOrderCreation = hasTabCurrentPhase && (
+ <div>
+ <PowerActionsForm orderType={orderBuildingType}
+ orderTypes={allowedPowerOrderTypes}
+ onChange={this.onChangeOrderType}
+ onNoOrders={() => this.onSetNoOrders(currentPowerName)}
+ onSetWaitFlag={() => this.setWaitFlag(!currentPower.wait)}
+ onVote={this.vote}
+ role={engine.role}
+ power={currentPower}/>
+ {(allowedPowerOrderTypes.length && (
+ <span>
<strong>Orderable locations</strong>: {orderTypeToLocs[orderBuildingType].join(', ')}
</span>
- ))
- || (<strong>&nbsp;No orderable location.</strong>)}
- {phaseType === 'A' && (
- (buildCount === null && (
- <strong>&nbsp;(unknown build count)</strong>
- ))
- || (buildCount === 0 ? (
- <strong>&nbsp;(nothing to build or disband)</strong>
- ) : (buildCount > 0 ? (
- <strong>&nbsp;({buildCount} unit{buildCount > 1 && 's'} may be built)</strong>
- ) : (
- <strong>&nbsp;({-buildCount} unit{buildCount < -1 && 's'} to disband)</strong>
- )))
- )}
- </div>
- )) || ''}
+ ))
+ || (<strong>&nbsp;No orderable location.</strong>)}
+ {phaseType === 'A' && (
+ (buildCount === null && (
+ <strong>&nbsp;(unknown build count)</strong>
+ ))
+ || (buildCount === 0 ? (
+ <strong>&nbsp;(nothing to build or disband)</strong>
+ ) : (buildCount > 0 ? (
+ <strong>&nbsp;({buildCount} unit{buildCount > 1 && 's'} may be built)</strong>
+ ) : (
+ <strong>&nbsp;({-buildCount} unit{buildCount < -1 && 's'} to disband)</strong>
+ )))
+ )}
+ </div>
+ );
+
+ return (
+ <main>
+ <Helmet>
+ <title>{title} | Diplomacy</title>
+ </Helmet>
+ <Navigation title={title}
+ afterTitle={navAfterTitle}
+ username={page.channel.username}
+ navigation={navigation}/>
<Tabs menu={tabNames} titles={tabTitles} onChange={this.onChangeMainTab} active={mainTab}>
{/* Tab Phase history. */}
- {(hasTabPhaseHistory && this.renderTabPhaseHistory(mainTab === 'phase_history', engine)) || ''}
+ {(hasTabPhaseHistory && this.renderTabResults(mainTab === 'phase_history', engine)) || ''}
+ {this.renderTabMessages(mainTab === 'messages', engine, currentPowerName)}
{/* Tab Current phase. */}
{(hasTabCurrentPhase && this.renderTabCurrentPhase(
mainTab === 'current_phase',
engine,
currentPowerName,
orderBuildingType,
- this.state.orderBuildingPath
+ this.state.orderBuildingPath,
+ currentPowerName,
+ currentTabOrderCreation
)) || ''}
</Tabs>
{this.state.fancy_title && (
@@ -1204,7 +1234,7 @@ export class ContentGame extends Content {
}
componentDidMount() {
- super.componentDidMount();
+ window.scrollTo(0, 0);
if (this.props.data.client)
this.reloadDeadlineTimer(this.props.data.client);
this.props.data.displayed = true;
@@ -1233,3 +1263,8 @@ export class ContentGame extends Content {
}
}
+
+ContentGame.contextType = PageContext;
+ContentGame.propTypes = {
+ data: PropTypes.object.isRequired
+};
diff --git a/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx b/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx
index 6a62d71..51ad998 100644
--- a/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx
+++ b/diplomacy/web/src/gui/diplomacy/contents/content_games.jsx
@@ -15,13 +15,18 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {Content} from "../../core/content";
-import {Tab, Tabs} from "../../core/tabs";
+import {Tabs} from "../../core/tabs";
import {Table} from "../../core/table";
import {FindForm} from "../forms/find_form";
import {CreateForm} from "../forms/create_form";
import {InlineGameView} from "../utils/inline_game_view";
import {STRINGS} from "../../../diplomacy/utils/strings";
+import {Helmet} from "react-helmet";
+import {Navigation} from "../widgets/navigation";
+import {PageContext} from "../widgets/page_context";
+import {ContentGame} from "./content_game";
+import PropTypes from 'prop-types';
+import {Tab} from "../../core/tab";
const TABLE_LOCAL_GAMES = {
game_id: ['Game ID', 0],
@@ -35,7 +40,7 @@ const TABLE_LOCAL_GAMES = {
my_games: ['My Games', 8],
};
-export class ContentGames extends Content {
+export class ContentGames extends React.Component {
constructor(props) {
super(props);
@@ -46,15 +51,8 @@ export class ContentGames extends Content {
this.wrapGameData = this.wrapGameData.bind(this);
}
- static builder(page, data) {
- return {
- title: 'Games',
- navigation: [
- ['load a game from disk', page.loadGameFromDisk],
- ['logout', page.logout]
- ],
- component: <ContentGames page={page} data={data}/>
- };
+ getPage() {
+ return this.context;
}
onFind(form) {
@@ -65,6 +63,7 @@ export class ContentGames extends Content {
.then((data) => {
this.getPage().success('Found ' + data.length + ' data.');
this.getPage().addGamesFound(data);
+ this.getPage().loadGames();
})
.catch((error) => {
this.getPage().error('Error when looking for distant games: ' + error);
@@ -98,7 +97,11 @@ export class ContentGames extends Content {
})
.then(allPossibleOrders => {
networkGame.local.setPossibleOrders(allPossibleOrders);
- this.getPage().loadGame(networkGame.local, {success: 'Game created.'});
+ this.getPage().load(
+ `game: ${networkGame.local.game_id}`,
+ <ContentGame data={networkGame.local}/>,
+ {success: 'Game created.'}
+ );
})
.catch((error) => {
this.getPage().error('Error when creating a game: ' + error);
@@ -114,27 +117,55 @@ export class ContentGames extends Content {
}
render() {
- const myGames = this.getPage().getMyGames();
+ const title = 'Games';
+ const page = this.getPage();
+ const navigation = [
+ ['load a game from disk', page.loadGameFromDisk],
+ ['logout', page.logout]
+ ];
+ const myGames = this.props.myGames;
+ const gamesFound = this.props.gamesFound;
+ myGames.sort((a, b) => b.timestamp_created - a.timestamp_created);
+ gamesFound.sort((a, b) => b.timestamp_created - a.timestamp_created);
const tab = this.state.tab ? this.state.tab : (myGames.length ? 'my-games' : 'find');
return (
<main>
+ <Helmet>
+ <title>{title} | Diplomacy</title>
+ </Helmet>
+ <Navigation title={title} username={page.channel.username} navigation={navigation}/>
<Tabs menu={['create', 'find', 'my-games']} titles={['Create', 'Find', 'My Games']}
onChange={this.changeTab} active={tab}>
- <Tab id="tab-games-create" display={tab === 'create'}>
- <CreateForm onSubmit={this.onCreate}/>
- </Tab>
- <Tab id="tab-games-find" display={tab === 'find'}>
- <FindForm onSubmit={this.onFind}/>
- <Table className={"table table-striped"} caption={"Games"} columns={TABLE_LOCAL_GAMES}
- data={this.getPage().getGamesFound()} wrapper={this.wrapGameData}/>
- </Tab>
- <Tab id={'tab-my-games'} display={tab === 'my-games'}>
- <Table className={"table table-striped"} caption={"My games"} columns={TABLE_LOCAL_GAMES}
- data={myGames} wrapper={this.wrapGameData}/>
- </Tab>
+ {tab === 'create' ? (
+ <Tab id="tab-games-create" display={true}>
+ <CreateForm onSubmit={this.onCreate}/>
+ </Tab>
+ ) : ''}
+ {tab === 'find' ? (
+ <Tab id="tab-games-find" display={true}>
+ <FindForm onSubmit={this.onFind}/>
+ <Table className={"table table-striped"} caption={"Games"} columns={TABLE_LOCAL_GAMES}
+ data={gamesFound} wrapper={this.wrapGameData}/>
+ </Tab>
+ ) : ''}
+ {tab === 'my-games' ? (
+ <Tab id={'tab-my-games'} display={true}>
+ <Table className={"table table-striped"} caption={"My games"} columns={TABLE_LOCAL_GAMES}
+ data={myGames} wrapper={this.wrapGameData}/>
+ </Tab>
+ ) : ''}
</Tabs>
</main>
);
}
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
}
+
+ContentGames.contextType = PageContext;
+ContentGames.propTypes = {
+ gamesFound: PropTypes.array.isRequired,
+ myGames: PropTypes.array.isRequired
+};
diff --git a/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx
index 0447280..5b3ec13 100644
--- a/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx
+++ b/diplomacy/web/src/gui/diplomacy/forms/join_form.jsx
@@ -49,20 +49,22 @@ export class JoinForm extends React.Component {
const onSubmit = Forms.createOnSubmitCallback(this, this.props.onSubmit);
return (
<form className={'form-inline'}>
- <div className={'form-group'}>
+ <div className={'form-group mr-2'}>
{Forms.createLabel(this.getPowerNameID(), 'Power:')}
<select id={this.getPowerNameID()} className={'from-control custom-select ml-2'}
value={Forms.getValue(this.state, this.getPowerNameID())} onChange={onChange}>
{Forms.createSelectOptions(STRINGS.ALL_POWER_NAMES, true)}
</select>
</div>
- <div className={'form-group mx-2'}>
- {Forms.createLabel(this.getPasswordID(), '', 'sr-only')}
- <input id={this.getPasswordID()} type={'password'} className={'form-control'}
- placeholder={'registration password'}
- value={Forms.getValue(this.state, this.getPasswordID())}
- onChange={onChange}/>
- </div>
+ {this.props.password_required ? (
+ <div className={'form-group mr-2'}>
+ {Forms.createLabel(this.getPasswordID(), '', 'sr-only')}
+ <input id={this.getPasswordID()} type={'password'} className={'form-control'}
+ placeholder={'registration password'}
+ value={Forms.getValue(this.state, this.getPasswordID())}
+ onChange={onChange}/>
+ </div>
+ ) : ''}
{Forms.createSubmit('join', false, onSubmit)}
</form>
);
@@ -71,6 +73,7 @@ export class JoinForm extends React.Component {
JoinForm.propTypes = {
game_id: PropTypes.string.isRequired,
+ password_required: PropTypes.bool.isRequired,
powers: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
onSubmit: PropTypes.func
diff --git a/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx
index 33bd763..2f7c1f5 100644
--- a/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx
+++ b/diplomacy/web/src/gui/diplomacy/forms/power_actions_form.jsx
@@ -48,7 +48,6 @@ export class PowerActionsForm extends React.Component {
const votes = [];
if (this.props.orderTypes.length) {
title = 'Create order:';
- header.push(<strong key={'title'} className={titleClass}>{title}</strong>);
header.push(...this.props.orderTypes.map((orderLetter, index) => (
<div key={index} className={'form-check-inline'}>
{Forms.createRadio('order_type', orderLetter, ORDER_BUILDER[orderLetter].name, this.props.orderType, onChange)}
@@ -58,10 +57,8 @@ export class PowerActionsForm extends React.Component {
} else if (this.props.power.order_is_set) {
title = 'Unorderable power (already locked on server).';
titleClass += ' neutral';
- header.push(<strong key={'title'} className={titleClass}>{title}</strong>);
} else {
title = 'No orders available for this power.';
- header.push(<strong key={'title'} className={titleClass}>{title}</strong>);
}
if (!this.props.power.order_is_set) {
header.push(Forms.createButton('pass', this.props.onNoOrders));
@@ -90,19 +87,23 @@ export class PowerActionsForm extends React.Component {
}
}
return (
- <form className={'form-inline power-actions-form'}>
- {header}
- {Forms.createButton(
- (this.props.power.wait ? 'no wait' : 'wait'),
- this.props.onSetWaitFlag,
- (this.props.power.wait ? 'success' : 'danger')
- )}
- {votes}
- <HotKey keys={['escape']} onKeysCoincide={onReset}/>
- {this.props.orderTypes.map((letter, index) => (
- <HotKey key={index} keys={[letter.toLowerCase()]} onKeysCoincide={() => onSetOrderType(letter)}/>
- ))}
- </form>
+ <div>
+ <div><strong key={'title'} className={titleClass}>{title}</strong></div>
+ <form className={'form-inline power-actions-form'}>
+ {header}
+ {Forms.createButton(
+ (this.props.power.wait ? 'no wait' : 'wait'),
+ this.props.onSetWaitFlag,
+ (this.props.power.wait ? 'success' : 'danger')
+ )}
+ {votes}
+ <HotKey keys={['escape']} onKeysCoincide={onReset}/>
+ {this.props.orderTypes.map((letter, index) => (
+ <HotKey key={index} keys={[letter.toLowerCase()]}
+ onKeysCoincide={() => onSetOrderType(letter)}/>
+ ))}
+ </form>
+ </div>
);
}
}
diff --git a/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx
index 3c55e49..6b966d0 100644
--- a/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx
+++ b/diplomacy/web/src/gui/diplomacy/forms/select_location_form.jsx
@@ -16,7 +16,7 @@
// ==============================================================================
import React from "react";
import PropTypes from "prop-types";
-import {Button} from "../../core/widgets";
+import {Button} from "../../core/button";
export class SelectLocationForm extends React.Component {
render() {
diff --git a/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx b/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx
index cc62fe2..51f3306 100644
--- a/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx
+++ b/diplomacy/web/src/gui/diplomacy/forms/select_via_form.jsx
@@ -16,7 +16,7 @@
// ==============================================================================
import React from "react";
import PropTypes from "prop-types";
-import {Button} from "../../core/widgets";
+import {Button} from "../../core/button";
export class SelectViaForm extends React.Component {
render() {
diff --git a/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx b/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx
index 0ada4c9..3de649c 100644
--- a/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx
+++ b/diplomacy/web/src/gui/diplomacy/utils/inline_game_view.jsx
@@ -15,9 +15,10 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {Button} from "../../core/widgets";
import {JoinForm} from "../forms/join_form";
import {STRINGS} from "../../../diplomacy/utils/strings";
+import {ContentGame} from "../contents/content_game";
+import {Button} from "../../core/button";
export class InlineGameView {
constructor(page, gameData) {
@@ -46,7 +47,11 @@ export class InlineGameView {
})
.then(allPossibleOrders => {
this.game.setPossibleOrders(allPossibleOrders);
- this.page.loadGame(this.game, {success: 'Game joined.'});
+ this.page.load(
+ `game: ${this.game.game_id}`,
+ <ContentGame data={this.game}/>,
+ {success: 'Game joined.'}
+ );
})
.catch((error) => {
this.page.error('Error when joining game ' + this.game.game_id + ': ' + error);
@@ -54,7 +59,7 @@ export class InlineGameView {
}
showGame() {
- this.page.loadGame(this.game);
+ this.page.load(`game: ${this.game.game_id}`, <ContentGame data={this.game}/>);
}
getJoinUI() {
@@ -70,6 +75,7 @@ export class InlineGameView {
} else {
// Game not yet joined.
return <JoinForm key={this.game.game_id} game_id={this.game.game_id} powers={this.game.controlled_powers}
+ password_required={this.game.registration_password}
onSubmit={this.joinGame}/>;
}
}
@@ -124,6 +130,14 @@ export class InlineGameView {
return this.getJoinUI();
if (name === 'my_games')
return this.getMyGamesButton();
+ if (name === 'game_id') {
+ const date = new Date(this.game.timestamp_created / 1000);
+ const dateString = `${date.toLocaleDateString()} - ${date.toLocaleTimeString()}`;
+ return <div>
+ <div><strong>{this.game.game_id}</strong></div>
+ <div>({dateString})</div>
+ </div>;
+ }
return this.game[name];
}
}
diff --git a/diplomacy/web/src/gui/diplomacy/utils/load_game_from_disk.js b/diplomacy/web/src/gui/diplomacy/utils/load_game_from_disk.js
new file mode 100644
index 0000000..1e13f4f
--- /dev/null
+++ b/diplomacy/web/src/gui/diplomacy/utils/load_game_from_disk.js
@@ -0,0 +1,83 @@
+import $ from "jquery";
+import {STRINGS} from "../../../diplomacy/utils/strings";
+import {Game} from "../../../diplomacy/engine/game";
+
+export function loadGameFromDisk(onLoad, onError) {
+ const input = $(document.createElement('input'));
+ input.attr("type", "file");
+ input.trigger('click');
+ input.change(event => {
+ const file = event.target.files[0];
+ if (!file.name.match(/\.json$/i)) {
+ onError(`Invalid JSON filename ${file.name}`);
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = () => {
+ const savedData = JSON.parse(reader.result);
+ const gameObject = {};
+ gameObject.game_id = `(local) ${savedData.id}`;
+ gameObject.map_name = savedData.map;
+ gameObject.rules = savedData.rules;
+ gameObject.state_history = {};
+ gameObject.message_history = {};
+ gameObject.order_history = {};
+ gameObject.result_history = {};
+
+ // Load all saved phases (expect the latest one) to history fields.
+ for (let i = 0; i < savedData.phases.length - 1; ++i) {
+ const savedPhase = savedData.phases[i];
+ const gameState = savedPhase.state;
+ const phaseOrders = savedPhase.orders || {};
+ const phaseResults = savedPhase.results || {};
+ const phaseMessages = {};
+ if (savedPhase.messages) {
+ for (let message of savedPhase.messages) {
+ phaseMessages[message.time_sent] = message;
+ }
+ }
+ if (!gameState.name)
+ gameState.name = savedPhase.name;
+ gameObject.state_history[gameState.name] = gameState;
+ gameObject.message_history[gameState.name] = phaseMessages;
+ gameObject.order_history[gameState.name] = phaseOrders;
+ gameObject.result_history[gameState.name] = phaseResults;
+ }
+
+ // Load latest phase separately and use it later to define the current game phase.
+ const latestPhase = savedData.phases[savedData.phases.length - 1];
+ const latestGameState = latestPhase.state;
+ const latestPhaseOrders = latestPhase.orders || {};
+ const latestPhaseResults = latestPhase.results || {};
+ const latestPhaseMessages = {};
+ if (latestPhase.messages) {
+ for (let message of latestPhase.messages) {
+ latestPhaseMessages[message.time_sent] = message;
+ }
+ }
+ if (!latestGameState.name)
+ latestGameState.name = latestPhase.name;
+ // TODO: NB: What is latest phase in loaded JSON contains order results? Not sure if it is well handled.
+ gameObject.result_history[latestGameState.name] = latestPhaseResults;
+
+ gameObject.messages = [];
+ gameObject.role = STRINGS.OBSERVER_TYPE;
+ gameObject.status = STRINGS.COMPLETED;
+ gameObject.timestamp_created = 0;
+ gameObject.deadline = 0;
+ gameObject.n_controls = 0;
+ gameObject.registration_password = '';
+ const game = new Game(gameObject);
+
+ // Set game current phase and state using latest phase found in JSON file.
+ game.setPhaseData({
+ name: latestGameState.name,
+ state: latestGameState,
+ orders: latestPhaseOrders,
+ messages: latestPhaseMessages
+ });
+ onLoad(game);
+ };
+ reader.readAsText(file);
+ });
+}
diff --git a/diplomacy/web/src/gui/diplomacy/widgets/help.jsx b/diplomacy/web/src/gui/diplomacy/widgets/help.jsx
new file mode 100644
index 0000000..1ec1a54
--- /dev/null
+++ b/diplomacy/web/src/gui/diplomacy/widgets/help.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+export function Help() {
+ return (
+ <div>
+ <p>When building an order, press <strong>ESC</strong> to reset build.</p>
+ <p>Press letter associated to an order type to start building an order of this type.
+ <br/> Order type letter is indicated in order type name after order type radio button.
+ </p>
+ <p>In Phase History tab, use keyboard left and right arrows to navigate in past phases.</p>
+ </div>
+ );
+}
diff --git a/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx b/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx
index 045a108..46153b8 100644
--- a/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx
+++ b/diplomacy/web/src/gui/diplomacy/widgets/message_view.jsx
@@ -15,7 +15,6 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {UTILS} from "../../../diplomacy/utils/utils";
import PropTypes from 'prop-types';
export class MessageView extends React.Component {
@@ -24,9 +23,13 @@ export class MessageView extends React.Component {
const message = this.props.message;
const owner = this.props.owner;
const id = this.props.id ? {id: this.props.id} : {};
- const messagesLines = message.message.replace('\r\n', '\n').replace('\r', '\n').split('\n');
+ const messagesLines = message.message.replace('\r\n', '\n')
+ .replace('\r', '\n')
+ .replace('<br>', '\n')
+ .replace('<br/>', '\n')
+ .split('\n');
let onClick = null;
- const classNames = ['game-message'];
+ const classNames = ['game-message', 'row'];
if (owner === message.sender)
classNames.push('message-sender');
else {
@@ -36,12 +39,18 @@ export class MessageView extends React.Component {
onClick = this.props.onClick ? {onClick: () => this.props.onClick(message)} : {};
}
return (
- <div className={'game-message-wrapper'} {...id}>
+ <div className={'game-message-wrapper' + (
+ this.props.phase && this.props.phase !== message.phase ? ' other-phase' : ' new-phase')}
+ {...id}>
<div className={classNames.join(' ')} {...onClick}>
- <div className={'message-header'}>
- {message.sender} {UTILS.html.UNICODE_SMALL_RIGHT_ARROW} {message.recipient}
+ <div className="message-header col-md-auto text-md-right text-center">
+ {message.phase}
+ </div>
+ <div className="message-content col-md">
+ {messagesLines.map((line, lineIndex) => <div key={lineIndex}>{
+ line.replace(/(<([^>]+)>)/ig,"")
+ }</div>)}
</div>
- <div className={'message-content'}>{messagesLines.map((line, lineIndex) => <div key={lineIndex}>{line}</div>)}</div>
</div>
</div>
);
@@ -50,6 +59,7 @@ export class MessageView extends React.Component {
MessageView.propTypes = {
message: PropTypes.object,
+ phase: PropTypes.string,
owner: PropTypes.string,
onClick: PropTypes.func,
id: PropTypes.string,
diff --git a/diplomacy/web/src/gui/diplomacy/widgets/navigation.jsx b/diplomacy/web/src/gui/diplomacy/widgets/navigation.jsx
new file mode 100644
index 0000000..5d961bc
--- /dev/null
+++ b/diplomacy/web/src/gui/diplomacy/widgets/navigation.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import Octicon, {Person} from "@githubprimer/octicons-react";
+import PropTypes from "prop-types";
+
+export class Navigation extends React.Component {
+ render() {
+ const hasNavigation = this.props.navigation && this.props.navigation.length;
+ if (hasNavigation) {
+ return (
+ <div className={'title row'}>
+ <div className={'col align-self-center'}>
+ <strong>{this.props.title}</strong>
+ {this.props.afterTitle ? this.props.afterTitle : ''}
+ </div>
+ <div className={'col-sm-1'}>
+ {(!hasNavigation && (
+ <div className={'float-right'}>
+ <strong>
+ <u className={'mr-2'}>{this.props.username}</u>
+ <Octicon icon={Person}/>
+ </strong>
+ </div>
+ )) || (
+ <div className="dropdown float-right">
+ <button className="btn btn-secondary dropdown-toggle" type="button"
+ id="dropdownMenuButton" data-toggle="dropdown"
+ aria-haspopup="true" aria-expanded="false">
+ {(this.props.username && (
+ <span>
+ <u className={'mr-2'}>{this.props.username}</u>
+ <Octicon icon={Person}/>
+ </span>
+ )) || 'Menu'}
+ </button>
+ <div className="dropdown-menu dropdown-menu-right"
+ aria-labelledby="dropdownMenuButton">
+ {this.props.navigation.map((nav, index) => {
+ const navTitle = nav[0];
+ const navAction = nav[1];
+ return <span key={index} className="dropdown-item"
+ onClick={navAction}>{navTitle}</span>;
+ })}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+ return (
+ <div className={'title'}><strong>{this.props.title}</strong></div>
+ );
+ }
+}
+
+Navigation.propTypes = {
+ title: PropTypes.string.isRequired,
+ afterTitle: PropTypes.object,
+ navigation: PropTypes.array,
+ username: PropTypes.string,
+};
diff --git a/diplomacy/web/src/gui/diplomacy/widgets/page_context.jsx b/diplomacy/web/src/gui/diplomacy/widgets/page_context.jsx
new file mode 100644
index 0000000..cfb8252
--- /dev/null
+++ b/diplomacy/web/src/gui/diplomacy/widgets/page_context.jsx
@@ -0,0 +1,3 @@
+import React from "react";
+
+export const PageContext = React.createContext(null);
diff --git a/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx b/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx
index 28a5421..4ed4d8a 100644
--- a/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx
+++ b/diplomacy/web/src/gui/diplomacy/widgets/power_order.jsx
@@ -15,8 +15,8 @@
// with this program. If not, see <https://www.gnu.org/licenses/>.
// ==============================================================================
import React from "react";
-import {Button} from "../../core/widgets";
import PropTypes from 'prop-types';
+import {Button} from "../../core/button";
export class PowerOrder extends React.Component {
render() {
diff --git a/diplomacy/web/src/index.css b/diplomacy/web/src/index.css
index f33b116..f270135 100644
--- a/diplomacy/web/src/index.css
+++ b/diplomacy/web/src/index.css
@@ -1,7 +1,7 @@
/** Bootstrap. **/
/** Common. **/
-a.dropdown-item {
+span.dropdown-item {
cursor: pointer;
}
@@ -46,7 +46,7 @@ a.dropdown-item {
color: green;
}
-#past-map svg, #current-map svg {
+#past-map svg, #current-map svg, #messages-map svg {
display: block;
width: 100%;
height: auto;
@@ -94,10 +94,25 @@ main {
text-align: center;
}
+span.power-name {
+ display: inline-block;
+ margin-left: 1rem;
+ margin-right: 1rem;
+ width: 10rem;
+}
+
#current-power {
color: red;
font-weight: bold;
text-align: center;
+ display: inline-block;
+ margin-left: 1rem;
+ margin-right: 1rem;
+ width: 10rem;
+}
+
+.form-current-power {
+ display: inline-block;
}
.page-messages {
@@ -173,9 +188,10 @@ main {
display: inline-block;
}
-.page > .title {
+.page > main > .title {
border-bottom: 1px solid silver;
- padding: 10px;
+ padding-bottom: 10px;
+ margin-bottom: 10px;
}
.left {
@@ -341,19 +357,18 @@ main {
}
.game-message {
- padding: 10px;
- width: 75%;
- border-width: 4px;
+ width: 90%;
+ border-width: 2px;
border-style: solid;
- border-radius: 10px;
+ margin: 0;
}
.game-message .message-header {
font-weight: bold;
+ border-right: inherit;
}
.game-message.message-recipient {
- float: left;
border-color: rgb(240, 200, 200);
background-color: rgb(255, 220, 220);
cursor: pointer;
@@ -366,18 +381,26 @@ main {
}
.game-message.message-sender {
- float: right;
border-color: rgb(200, 200, 240);
background-color: rgb(220, 220, 255);
+ float: right;
}
.game-message-wrapper {
- overflow: auto;
clear: both;
+ overflow: auto;
+}
+
+.game-message-wrapper.other-phase {
+ color: rgb(140, 140, 140);
}
.game-message-wrapper + .game-message-wrapper {
- margin-top: 10px;
+ padding-top: 5px;
+}
+
+.game-message-wrapper.other-phase + .game-message-wrapper.new-phase {
+ margin-top: 5px;
}
.button-server {