// ==============================================================================
// 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 .
// ==============================================================================
/** Main class to use to create app GUI. **/
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
};
export class Page extends React.Component {
constructor(props) {
super(props);
this.connection = null;
this.channel = null;
this.availableMaps = null;
this.state = {
// fancybox,
fancyTitle: null,
onFancyBox: null,
// Page messages
error: null,
info: null,
success: null,
title: null,
// Page content parameters
contentName: 'connection',
contentData: 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.unloadFancyBox = this.unloadFancyBox.bind(this);
}
static wrapMessage(message) {
return message ? `(${UTILS.date()}) ${message}` : '';
}
static __sort_games(games) {
// Sort games with not-joined games first, else compare game ID.
games.sort((a, b) => (((a.role ? 1 : 0) - (b.role ? 1 : 0)) || a.game_id.localeCompare(b.game_id)));
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);
}
//// Methods to load a global fancybox.
loadFancyBox(title, callback) {
this.setState({fancyTitle: title, onFancyBox: callback});
}
unloadFancyBox() {
this.setState({fancyTitle: null, onFancyBox: null});
}
//// 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);
}
loadGames(contentData, messages) {
this.loadPage('games', contentData, messages);
}
loadGame(gameInfo, messages) {
this.loadPage('game', gameInfo, messages);
}
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);
}
});
}
//// Methods to sign out channel and go back to connection page.
__disconnect() {
// Clear local data and go back to connection page.
this.connection.close();
this.connection = null;
this.channel = null;
this.availableMaps = null;
const message = Page.wrapMessage(`Disconnected from channel and server.`);
Diplog.success(message);
this.setState(this.copyState({
error: null,
info: null,
success: message,
contentName: 'connection',
contentData: null,
// When disconnected, remove all games previously loaded.
games: {},
myGames: {}
}));
}
logout() {
// Disconnect channel and go back to connection page.
if (this.channel) {
this.channel.logout()
.then(() => this.__disconnect())
.catch(error => this.error(`Error while disconnecting: ${error.toString()}.`));
} else {
this.__disconnect();
}
}
//// 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);
this.setState({error: message});
}
info(message) {
message = Page.wrapMessage(message);
Diplog.info(message);
this.setState({info: message});
}
success(message) {
message = Page.wrapMessage(message);
Diplog.success(message);
this.setState({success: message});
}
warn(message) {
this.info(message);
}
//// Methods to manage games.
updateMyGames(gamesToAdd) {
// Update state myGames with given games. This method does not update local storage.
const myGames = Object.assign({}, this.state.myGames);
let gamesFound = null;
for (let gameToAdd of gamesToAdd) {
myGames[gameToAdd.game_id] = gameToAdd;
if (this.state.games.hasOwnProperty(gameToAdd.game_id)) {
if (!gamesFound)
gamesFound = Object.assign({}, this.state.games);
gamesFound[gameToAdd.game_id] = gameToAdd;
}
}
if (!gamesFound)
gamesFound = this.state.games;
this.setState({myGames: myGames, games: gamesFound});
}
getMyGames() {
return Page.__sort_games(Object.values(this.state.myGames));
}
getGamesFound() {
return Page.__sort_games(Object.values(this.state.games));
}
addGamesFound(gamesToAdd) {
const gamesFound = {};
for (let game of gamesToAdd) {
gamesFound[game.game_id] = (
this.state.myGames.hasOwnProperty(game.game_id) ?
this.state.myGames[game.game_id] : game
);
}
this.setState({games: gamesFound});
}
leaveGame(gameID) {
if (this.state.myGames.hasOwnProperty(gameID)) {
const game = this.state.myGames[gameID];
if (game.client) {
game.client.leave()
.then(() => {
this.disconnectGame(gameID);
this.loadGames(null, {info: `Game ${gameID} left.`});
})
.catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`));
}
}
}
disconnectGame(gameID) {
if (this.state.myGames.hasOwnProperty(gameID)) {
const game = this.state.myGames[gameID];
if (game.client)
game.client.clearAllCallbacks();
this.channel.getGamesInfo({games: [gameID]})
.then(gamesInfo => {
this.updateMyGames(gamesInfo);
})
.catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`));
}
}
addToMyGames(game) {
// Update state myGames with given game **and** update local storage.
const myGames = Object.assign({}, this.state.myGames);
const gamesFound = this.state.games.hasOwnProperty(game.game_id) ? Object.assign({}, this.state.games) : this.state.games;
myGames[game.game_id] = game;
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});
}
removeFromMyGames(gameID) {
if (this.state.myGames.hasOwnProperty(gameID)) {
const games = Object.assign({}, this.state.myGames);
delete games[gameID];
DipStorage.removeUserGame(this.channel.username, gameID);
this.setState({myGames: games});
}
}
hasMyGame(gameID) {
return this.state.myGames.hasOwnProperty(gameID);
}
//// 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 (
this.success()}>
{successMessage}
this.info()}>
{infoMessage}
this.error()}>
{errorMessage}
{((hasNavigation || this.channel) && (
{title}
{(!hasNavigation && (
{this.channel.username}
)) || (
{content.navigation.map((nav, index) => {
const navTitle = nav[0];
const navAction = nav[1];
return
{navTitle};
})}
)}
)) || (
{title}
)}
{content.component}
{this.state.onFancyBox && (
{this.state.onFancyBox()}
)}
);
}
}