aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/web
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
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')
-rw-r--r--diplomacy/web/package-lock.json545
-rw-r--r--diplomacy/web/package.json2
-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
27 files changed, 1098 insertions, 900 deletions
diff --git a/diplomacy/web/package-lock.json b/diplomacy/web/package-lock.json
index 86b1024..eddf412 100644
--- a/diplomacy/web/package-lock.json
+++ b/diplomacy/web/package-lock.json
@@ -268,9 +268,9 @@
}
},
"@babel/parser": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz",
- "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w=="
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz",
+ "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew=="
},
"@babel/plugin-proposal-async-generator-functions": {
"version": "7.2.0",
@@ -583,11 +583,11 @@
}
},
"@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.4.tgz",
- "integrity": "sha512-Ki+Y9nXBlKfhD+LXaRS7v95TtTGYRAf9Y1rTDiE75zf8YQz4GDaWRXosMfJBXxnk88mGFjWdCRIeqDbon7spYA==",
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz",
+ "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==",
"requires": {
- "regexp-tree": "^0.1.0"
+ "regexp-tree": "^0.1.6"
}
},
"@babel/plugin-transform-new-target": {
@@ -671,11 +671,11 @@
}
},
"@babel/plugin-transform-regenerator": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.4.tgz",
- "integrity": "sha512-Zz3w+pX1SI0KMIiqshFZkwnVGUhDZzpX2vtPzfJBKQQq8WsP/Xy9DNdELWivxcKOCX/Pywge4SiEaPaLtoDT4g==",
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz",
+ "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==",
"requires": {
- "regenerator-transform": "^0.13.4"
+ "regenerator-transform": "^0.14.0"
}
},
"@babel/plugin-transform-reserved-words": {
@@ -747,9 +747,9 @@
}
},
"@babel/plugin-transform-typescript": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.4.tgz",
- "integrity": "sha512-rwDvjaMTx09WC0rXGBRlYSSkEHOKRrecY6hEr3SVIPKII8DVWXtapNAfAyMC0dovuO+zYArcAuKeu3q9DNRfzA==",
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.5.tgz",
+ "integrity": "sha512-RPB/YeGr4ZrFKNwfuQRlMf2lxoCUaU01MTw39/OFE/RiL8HDjtn68BwEPft1P7JN4akyEmjGWAMNldOV7o9V2g==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-syntax-typescript": "^7.2.0"
@@ -766,9 +766,9 @@
}
},
"@babel/preset-env": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.4.4.tgz",
- "integrity": "sha512-FU1H+ACWqZZqfw1x2G1tgtSSYSfxJLkpaUQL37CenULFARDo+h4xJoVHzRoHbK+85ViLciuI7ME4WTIhFRBBlw==",
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.4.5.tgz",
+ "integrity": "sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/helper-plugin-utils": "^7.0.0",
@@ -799,12 +799,12 @@
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@babel/plugin-transform-modules-systemjs": "^7.4.4",
"@babel/plugin-transform-modules-umd": "^7.2.0",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.4",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5",
"@babel/plugin-transform-new-target": "^7.4.4",
"@babel/plugin-transform-object-super": "^7.2.0",
"@babel/plugin-transform-parameters": "^7.4.4",
"@babel/plugin-transform-property-literals": "^7.2.0",
- "@babel/plugin-transform-regenerator": "^7.4.4",
+ "@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/plugin-transform-reserved-words": "^7.2.0",
"@babel/plugin-transform-shorthand-properties": "^7.2.0",
"@babel/plugin-transform-spread": "^7.2.0",
@@ -813,8 +813,8 @@
"@babel/plugin-transform-typeof-symbol": "^7.2.0",
"@babel/plugin-transform-unicode-regex": "^7.4.4",
"@babel/types": "^7.4.4",
- "browserslist": "^4.5.2",
- "core-js-compat": "^3.0.0",
+ "browserslist": "^4.6.0",
+ "core-js-compat": "^3.1.1",
"invariant": "^2.2.2",
"js-levenshtein": "^1.1.3",
"semver": "^5.5.0"
@@ -867,15 +867,15 @@
}
},
"@babel/traverse": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz",
- "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==",
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz",
+ "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==",
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/generator": "^7.4.4",
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-split-export-declaration": "^7.4.4",
- "@babel/parser": "^7.4.4",
+ "@babel/parser": "^7.4.5",
"@babel/types": "^7.4.4",
"debug": "^4.1.0",
"globals": "^11.1.0",
@@ -925,9 +925,9 @@
"integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw=="
},
"@hapi/hoek": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.1.tgz",
- "integrity": "sha512-+ryw4GU9pjr1uT6lBuErHJg3NYqzwJTvZ75nKuJijEzpd00Uqi6oiawTGDDf5Hl0zWmI7qHfOtaqB0kpQZJQzA=="
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz",
+ "integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A=="
},
"@hapi/joi": {
"version": "15.0.3",
@@ -1191,9 +1191,9 @@
"integrity": "sha512-U9m870Kqm0ko8beHawRXLGLvSi/ZMrl89gJ5BNcT452fAjtF2p4uRzXkdzvGJJJYBgx7BmqlDjBN/eCp5AAX2w=="
},
"@svgr/babel-plugin-svg-dynamic-title": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.2.0.tgz",
- "integrity": "sha512-gH2qItapwCUp6CCqbxvzBbc4dh4OyxdYKsW3EOkYexr0XUmQL0ScbdNh6DexkZ01T+sdClniIbnCObsXcnx3sQ=="
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.3.0.tgz",
+ "integrity": "sha512-3eI17Pb3jlg3oqV4Tie069n1SelYKBUpI90txDcnBWk4EGFW+YQGyQjy6iuJAReH0RnpUJ9jUExrt/xniGvhqw=="
},
"@svgr/babel-plugin-svg-em-dimensions": {
"version": "4.2.0",
@@ -1211,26 +1211,26 @@
"integrity": "sha512-hYfYuZhQPCBVotABsXKSCfel2slf/yvJY8heTVX1PCTaq/IgASq1IyxPPKJ0chWREEKewIU/JMSsIGBtK1KKxw=="
},
"@svgr/babel-preset": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-4.2.0.tgz",
- "integrity": "sha512-iLetHpRCQXfK47voAs5/uxd736cCyocEdorisjAveZo8ShxJ/ivSZgstBmucI1c8HyMF5tOrilJLoFbhpkPiKw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-4.3.0.tgz",
+ "integrity": "sha512-Lgy1RJiZumGtv6yJroOxzFuL64kG/eIcivJQ7y9ljVWL+0QXvFz4ix1xMrmjMD+rpJWwj50ayCIcFelevG/XXg==",
"requires": {
"@svgr/babel-plugin-add-jsx-attribute": "^4.2.0",
"@svgr/babel-plugin-remove-jsx-attribute": "^4.2.0",
"@svgr/babel-plugin-remove-jsx-empty-expression": "^4.2.0",
"@svgr/babel-plugin-replace-jsx-attribute-value": "^4.2.0",
- "@svgr/babel-plugin-svg-dynamic-title": "^4.2.0",
+ "@svgr/babel-plugin-svg-dynamic-title": "^4.3.0",
"@svgr/babel-plugin-svg-em-dimensions": "^4.2.0",
"@svgr/babel-plugin-transform-react-native-svg": "^4.2.0",
"@svgr/babel-plugin-transform-svg-component": "^4.2.0"
}
},
"@svgr/core": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@svgr/core/-/core-4.2.0.tgz",
- "integrity": "sha512-nvzXaf2VavqjMCTTfsZfjL4o9035KedALkMzk82qOlHOwBb8JT+9+zYDgBl0oOunbVF94WTLnvGunEg0csNP3Q==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-4.3.0.tgz",
+ "integrity": "sha512-Ycu1qrF5opBgKXI0eQg3ROzupalCZnSDETKCK/3MKN4/9IEmt3jPX/bbBjftklnRW+qqsCEpO0y/X9BTRw2WBg==",
"requires": {
- "@svgr/plugin-jsx": "^4.2.0",
+ "@svgr/plugin-jsx": "^4.3.0",
"camelcase": "^5.3.1",
"cosmiconfig": "^5.2.0"
}
@@ -1244,12 +1244,12 @@
}
},
"@svgr/plugin-jsx": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.2.0.tgz",
- "integrity": "sha512-AM1YokmZITgveY9bulLVquqNmwiFo2Px2HL+IlnTCR01YvWDfRL5QKdnF7VjRaS5MNP938mmqvL0/8oz3zQMkg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.3.0.tgz",
+ "integrity": "sha512-0ab8zJdSOTqPfjZtl89cjq2IOmXXUYV3Fs7grLT9ur1Al3+x3DSp2+/obrYKUGbQUnLq96RMjSZ7Icd+13vwlQ==",
"requires": {
"@babel/core": "^7.4.3",
- "@svgr/babel-preset": "^4.2.0",
+ "@svgr/babel-preset": "^4.3.0",
"@svgr/hast-util-to-babel-ast": "^4.2.0",
"rehype-parse": "^6.0.0",
"unified": "^7.1.0",
@@ -1341,9 +1341,9 @@
}
},
"@types/node": {
- "version": "12.0.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
- "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA=="
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.4.tgz",
+ "integrity": "sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw=="
},
"@types/q": {
"version": "1.5.2",
@@ -2224,9 +2224,9 @@
},
"dependencies": {
"core-js": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
- "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
+ "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
"regenerator-runtime": {
"version": "0.11.1",
@@ -2334,9 +2334,9 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw=="
},
"bluebird": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
- "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
+ "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
},
"bn.js": {
"version": "4.11.8",
@@ -2344,22 +2344,27 @@
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"body-parser": {
- "version": "1.18.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
- "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
- "bytes": "3.0.0",
+ "bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
- "http-errors": "~1.6.3",
- "iconv-lite": "0.4.23",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
- "qs": "6.5.2",
- "raw-body": "2.3.3",
- "type-is": "~1.6.16"
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
},
"dependencies": {
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -2368,18 +2373,15 @@
"ms": "2.0.0"
}
},
- "iconv-lite": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
- "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@@ -2533,13 +2535,13 @@
}
},
"browserslist": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.0.tgz",
- "integrity": "sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg==",
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.1.tgz",
+ "integrity": "sha512-1MC18ooMPRG2UuVFJTHFIAkk6mpByJfxCrnUyvSlu/hyQSFHMrlhM02SzNuCV+quTP4CKmqtOMAIjrifrpBJXQ==",
"requires": {
- "caniuse-lite": "^1.0.30000967",
- "electron-to-chromium": "^1.3.133",
- "node-releases": "^1.1.19"
+ "caniuse-lite": "^1.0.30000971",
+ "electron-to-chromium": "^1.3.137",
+ "node-releases": "^1.1.21"
}
},
"bser": {
@@ -2674,9 +2676,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30000967",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000967.tgz",
- "integrity": "sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ=="
+ "version": "1.0.30000971",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000971.tgz",
+ "integrity": "sha512-TQFYFhRS0O5rdsmSbF1Wn+16latXYsQJat66f7S7lizXW1PVpWJeZw9wqqVLIjuxDRz7s7xRUj13QCfd8hKn6g=="
},
"capture-exit": {
"version": "2.0.0",
@@ -3229,9 +3231,9 @@
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
},
"chrome-trace-event": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
- "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
+ "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==",
"requires": {
"tslib": "^1.9.0"
}
@@ -3496,9 +3498,12 @@
"integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo="
},
"content-disposition": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
- "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
},
"content-type": {
"version": "1.0.4",
@@ -3514,9 +3519,9 @@
}
},
"cookie": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
- "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -3547,20 +3552,26 @@
"integrity": "sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew=="
},
"core-js-compat": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.0.1.tgz",
- "integrity": "sha512-2pC3e+Ht/1/gD7Sim/sqzvRplMiRnFQVlPpDVaHtY9l7zZP7knamr3VRD6NyGfHd84MrDC0tAM9ulNxYMW0T3g==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.3.tgz",
+ "integrity": "sha512-EP018pVhgwsKHz3YoN1hTq49aRe+h017Kjz0NQz3nXV0cCRMvH3fLQl+vEPGr4r4J5sk4sU3tUC7U1aqTCeJeA==",
"requires": {
- "browserslist": "^4.5.4",
- "core-js": "3.0.1",
- "core-js-pure": "3.0.1",
- "semver": "^6.0.0"
+ "browserslist": "^4.6.0",
+ "core-js-pure": "3.1.3",
+ "semver": "^6.1.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz",
+ "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ=="
+ }
}
},
"core-js-pure": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.0.1.tgz",
- "integrity": "sha512-mSxeQ6IghKW3MoyF4cz19GJ1cMm7761ON+WObSyLfTu/Jn3x7w4NwNFnrZxgl4MTSvYYepVLNuRtlB4loMwJ5g=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.3.tgz",
+ "integrity": "sha512-k3JWTrcQBKqjkjI0bkfXS0lbpWPxYuHWfMMjC1VDmzU4Q58IwSbuXSo99YO/hUHlw/EB4AlfA2PVxOGkrIq6dA=="
},
"core-util-is": {
"version": "1.0.2",
@@ -4262,9 +4273,9 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-to-chromium": {
- "version": "1.3.134",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.134.tgz",
- "integrity": "sha512-C3uK2SrtWg/gSWaluLHWSHjyebVZCe4ZC0NVgTAoTq8tCR9FareRK5T7R7AS/nPZShtlEcjVMX1kQ8wi4nU68w=="
+ "version": "1.3.142",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.142.tgz",
+ "integrity": "sha512-GLOB/wAA2g9l5Hwg1XrPqd6br2WNOPIY8xl/q+g5zZdv3b5fB69oFOooxKxc0DfDfDS1RqaF6hKjwt6v4fuFUw=="
},
"elliptic": {
"version": "6.4.1",
@@ -4882,6 +4893,11 @@
"strip-eof": "^1.0.0"
}
},
+ "exenv": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
+ "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
+ },
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -4946,38 +4962,38 @@
}
},
"express": {
- "version": "4.16.4",
- "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
- "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
- "accepts": "~1.3.5",
+ "accepts": "~1.3.7",
"array-flatten": "1.1.1",
- "body-parser": "1.18.3",
- "content-disposition": "0.5.2",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
"content-type": "~1.0.4",
- "cookie": "0.3.1",
+ "cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
- "finalhandler": "1.1.1",
+ "finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
- "parseurl": "~1.3.2",
+ "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.4",
- "qs": "6.5.2",
- "range-parser": "~1.2.0",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
- "send": "0.16.2",
- "serve-static": "1.13.2",
- "setprototypeof": "1.1.0",
- "statuses": "~1.4.0",
- "type-is": "~1.6.16",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
@@ -4999,6 +5015,11 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@@ -5116,9 +5137,9 @@
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"fast-glob": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz",
- "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==",
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
+ "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==",
"requires": {
"@mrmlnc/readdir-enhanced": "^2.2.1",
"@nodelib/fs.stat": "^1.1.2",
@@ -5240,16 +5261,16 @@
}
},
"finalhandler": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
- "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
- "parseurl": "~1.3.2",
- "statuses": "~1.4.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
@@ -5723,9 +5744,9 @@
}
},
"hast-util-from-parse5": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.0.tgz",
- "integrity": "sha512-A7ev5OseS/J15214cvDdcI62uwovJO2PB60Xhnq7kaxvvQRFDEccuqbkrFXU03GPBGopdPqlpQBRqIcDS/Fjbg==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.1.tgz",
+ "integrity": "sha512-UfPzdl6fbxGAxqGYNThRUhRlDYY7sXu6XU9nQeX4fFZtV+IHbyEJtd+DUuwOqNV4z3K05E/1rIkoVr/JHmeWWA==",
"requires": {
"ccount": "^1.0.3",
"hastscript": "^5.0.0",
@@ -5742,14 +5763,14 @@
}
},
"hast-util-parse-selector": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz",
- "integrity": "sha512-Xyh0v+nHmQvrOqop2Jqd8gOdyQtE8sIP9IQf7mlVDqp924W4w/8Liuguk2L2qei9hARnQSG2m+wAOCxM7npJVw=="
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.2.tgz",
+ "integrity": "sha512-jIMtnzrLTjzqgVEQqPEmwEZV+ea4zHRFTP8Z2Utw0I5HuBOXHzUPPQWr6ouJdJqDKLbFU/OEiYwZ79LalZkmmw=="
},
"hastscript": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.0.0.tgz",
- "integrity": "sha512-xJtuJ8D42Xtq5yJrnDg/KAIxl2cXBXKoiIJwmWX9XMf8113qHTGl/Bf7jEsxmENJ4w6q4Tfl8s/Y6mEZo8x8qw==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.0.tgz",
+ "integrity": "sha512-7mOQX5VfVs/gmrOGlN8/EDfp1GqV6P3gTNVt+KnX4gbYhpASTM8bklFdFQCbFRAadURXAmw0R1QQdBdqp7jswQ==",
"requires": {
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.2.0",
@@ -5869,9 +5890,9 @@
},
"dependencies": {
"readable-stream": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
- "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+ "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -5886,14 +5907,15 @@
"integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc="
},
"http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
- "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
}
},
"http-parser-js": {
@@ -5961,9 +5983,9 @@
"integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0="
},
"icss-utils": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.0.tgz",
- "integrity": "sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+ "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
"requires": {
"postcss": "^7.0.14"
}
@@ -7609,9 +7631,9 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"json3": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
- "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE="
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
+ "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA=="
},
"json5": {
"version": "2.1.0",
@@ -7863,9 +7885,9 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
},
"loglevel": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
- "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po="
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.2.tgz",
+ "integrity": "sha512-Jt2MHrCNdtIe1W6co3tF5KXGRkzF+TYffiQstfXa04mrss9IKXzAAXYWak8LbZseAQY03sH2GzMCMU0ZOUc9bg=="
},
"loose-envify": {
"version": "1.4.0",
@@ -8023,9 +8045,9 @@
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"microevent.ts": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.0.tgz",
- "integrity": "sha1-OQdIuKUVCD5rY81REqPxjC/g66g="
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz",
+ "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g=="
},
"micromatch": {
"version": "3.1.10",
@@ -8224,9 +8246,9 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"nan": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
- "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"optional": true
},
"nanomatch": {
@@ -8363,9 +8385,9 @@
}
},
"node-releases": {
- "version": "1.1.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.19.tgz",
- "integrity": "sha512-SH/B4WwovHbulIALsQllAVwqZZD1kPmKCqrhGfR29dXjLAVZMHvBjD3S6nL9D/J9QkmZ1R92/0wCMDKXUUvyyA==",
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.22.tgz",
+ "integrity": "sha512-O6XpteBuntW1j86mw6LlovBIwTe+sO2+7vi9avQffNeIW4upgnaCVm6xrBWH+KATz7mNNRNNeEpuWB7dT6Cr3w==",
"requires": {
"semver": "^5.3.0"
},
@@ -9885,9 +9907,9 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM="
},
"prompts": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.0.4.tgz",
- "integrity": "sha512-HTzM3UWp/99A0gk51gAegwo1QRYA7xjcZufMNe33rCclFszUYAuHe1fIN/3ZmiHeGPkUsNaRyQm1hHOfM0PKxA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz",
+ "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==",
"requires": {
"kleur": "^3.0.2",
"sisteransi": "^1.0.0"
@@ -9933,9 +9955,9 @@
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
},
"psl": {
- "version": "1.1.31",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
- "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
+ "version": "1.1.32",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
+ "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g=="
},
"public-encrypt": {
"version": "4.0.3",
@@ -10041,23 +10063,20 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
- "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
- "bytes": "3.0.0",
- "http-errors": "1.6.3",
- "iconv-lite": "0.4.23",
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
- "iconv-lite": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
- "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
}
}
},
@@ -10178,6 +10197,22 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
},
+ "react-fast-compare": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
+ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
+ },
+ "react-helmet": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz",
+ "integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==",
+ "requires": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.5.4",
+ "react-fast-compare": "^2.0.2",
+ "react-side-effect": "^1.1.0"
+ }
+ },
"react-inlinesvg": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/react-inlinesvg/-/react-inlinesvg-0.8.4.tgz",
@@ -10323,6 +10358,15 @@
}
}
},
+ "react-side-effect": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.5.tgz",
+ "integrity": "sha512-Z2ZJE4p/jIfvUpiUMRydEVpQRf2f8GMHczT6qLcARmX7QRb28JDBTpnM2g/i5y/p7ZDEXYGHWg0RbhikE+hJRw==",
+ "requires": {
+ "exenv": "^1.2.1",
+ "shallowequal": "^1.0.1"
+ }
+ },
"read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -10401,9 +10445,9 @@
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
},
"regenerator-transform": {
- "version": "0.13.4",
- "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.4.tgz",
- "integrity": "sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A==",
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz",
+ "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==",
"requires": {
"private": "^0.1.6"
}
@@ -10418,9 +10462,9 @@
}
},
"regexp-tree": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.6.tgz",
- "integrity": "sha512-LFrA98Dw/heXqDojz7qKFdygZmFoiVlvE1Zp7Cq2cvF+ZA+03Gmhy0k0PQlsC1jvHPiTUSs+pDHEuSWv6+6D7w=="
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.10.tgz",
+ "integrity": "sha512-K1qVSbcedffwuIslMwpe6vGlj+ZXRnGkvjAtFHfDZZZuEdA/h0dxljAPu9vhUo6Rrx2U2AwJ+nSQ6hK+lrP5MQ=="
},
"regexpp": {
"version": "2.0.1",
@@ -10885,9 +10929,9 @@
"integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ=="
},
"send": {
- "version": "0.16.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
- "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -10896,12 +10940,12 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
- "http-errors": "~1.6.2",
- "mime": "1.4.1",
- "ms": "2.0.0",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
"on-finished": "~2.3.0",
- "range-parser": "~1.2.0",
- "statuses": "~1.4.0"
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
},
"dependencies": {
"debug": {
@@ -10910,17 +10954,19 @@
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
}
},
"mime": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
- "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
}
}
},
@@ -10951,22 +10997,38 @@
"ms": "2.0.0"
}
},
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
}
}
},
"serve-static": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
- "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
- "parseurl": "~1.3.2",
- "send": "0.16.2"
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
}
},
"set-blocking": {
@@ -11001,9 +11063,9 @@
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
- "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"sha.js": {
"version": "2.4.11",
@@ -11045,6 +11107,11 @@
}
}
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -11363,9 +11430,9 @@
},
"dependencies": {
"readable-stream": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
- "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
+ "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -11441,9 +11508,9 @@
}
},
"statuses": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
- "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stealthy-require": {
"version": "1.1.1",
@@ -11816,6 +11883,11 @@
"repeat-string": "^1.6.1"
}
},
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@@ -11854,9 +11926,9 @@
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
},
"tsutils": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.10.0.tgz",
- "integrity": "sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==",
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.13.0.tgz",
+ "integrity": "sha512-wRtEjVU8Su72sDIDoqno5Scwt8x4eaF0teKO3m4hu8K1QFPnIZMM88CLafs2tapUeWnY9SwwO3bWeOt2uauBcg==",
"requires": {
"tslib": "^1.8.1"
}
@@ -12238,11 +12310,11 @@
}
},
"vfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.0.0.tgz",
- "integrity": "sha512-WMNeHy5djSl895BqE86D7WqA0Ie5fAIeGCa7V1EqiXyJg5LaGch2SUaZueok5abYQGH6mXEAsZ45jkoILIOlyA==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.0.1.tgz",
+ "integrity": "sha512-lRHFCuC4SQBFr7Uq91oJDJxlnftoTLQ7eKIpMdubhYcVMho4781a8MWXLy3qZrZ0/STD1kRiKc0cQOHm4OkPeA==",
"requires": {
- "@types/unist": "^2.0.2",
+ "@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
"replace-ext": "1.0.0",
"unist-util-stringify-position": "^2.0.0",
@@ -12250,27 +12322,20 @@
},
"dependencies": {
"unist-util-stringify-position": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.0.tgz",
- "integrity": "sha512-Uz5negUTrf9zm2ZT2Z9kdOL7Mr7FJLyq3ByqagUi7QZRVK1HnspVazvSqwHt73jj7APHtpuJ4K110Jm8O6/elw==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.1.tgz",
+ "integrity": "sha512-Zqlf6+FRI39Bah8Q6ZnNGrEHUhwJOkHde2MHVk96lLyftfJJckaPslKgzhVcviXj8KcE9UJM9F+a4JEiBUTYgA==",
"requires": {
"@types/unist": "^2.0.2"
}
},
"vfile-message": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.0.tgz",
- "integrity": "sha512-YS6qg6UpBfIeiO+6XlhPOuJaoLvt1Y9g2cmlwqhBOOU0XRV8j5RLeoz72t6PWLvNXq3EBG1fQ05wNPrUoz0deQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.1.tgz",
+ "integrity": "sha512-KtasSV+uVU7RWhUn4Lw+wW1Zl/nW8JWx7JCPps10Y9JRRIDeDXf8wfBLoOSsJLyo27DqMyAi54C6Jf/d6Kr2Bw==",
"requires": {
"@types/unist": "^2.0.2",
- "unist-util-stringify-position": "^1.1.1"
- },
- "dependencies": {
- "unist-util-stringify-position": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
- "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ=="
- }
+ "unist-util-stringify-position": "^2.0.0"
}
}
}
@@ -12762,11 +12827,11 @@
}
},
"worker-rpc": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.0.tgz",
- "integrity": "sha1-XxJY3KPWF80YyoZYf4oFrA7r2DQ=",
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz",
+ "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==",
"requires": {
- "microevent.ts": "~0.1.0"
+ "microevent.ts": "~0.1.1"
}
},
"wrap-ansi": {
diff --git a/diplomacy/web/package.json b/diplomacy/web/package.json
index bc6ab38..241632c 100644
--- a/diplomacy/web/package.json
+++ b/diplomacy/web/package.json
@@ -1,6 +1,7 @@
{
"name": "web",
"version": "0.1.0",
+ "homepage": ".",
"private": true,
"dependencies": {
"@githubprimer/octicons-react": "^8.5.0",
@@ -11,6 +12,7 @@
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
+ "react-helmet": "^5.2.1",
"react-inlinesvg": "^0.8.4",
"react-scripts": "^3.0.1",
"react-scrollchor": "^6.0.0",
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 {