From 5c71a0f73717bffefb5e23a9e100adb62fc54a61 Mon Sep 17 00:00:00 2001
From: Philip Paquette <pcpaquette@gmail.com>
Date: Sun, 7 Jul 2019 09:23:59 -0400
Subject: 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.
---
 diplomacy/web/src/gui/core/page.jsx | 277 ++++++++++--------------------------
 1 file changed, 75 insertions(+), 202 deletions(-)

(limited to 'diplomacy/web/src/gui/core/page.jsx')

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>
         );
     }
 }
-- 
cgit v1.2.3

