diff options
Diffstat (limited to 'diplomacy/web')
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> No orderable location.</strong>)} - {phaseType === 'A' && ( - (buildCount === null && ( - <strong> (unknown build count)</strong> - )) - || (buildCount === 0 ? ( - <strong> (nothing to build or disband)</strong> - ) : (buildCount > 0 ? ( - <strong> ({buildCount} unit{buildCount > 1 && 's'} may be built)</strong> - ) : ( - <strong> ({-buildCount} unit{buildCount < -1 && 's'} to disband)</strong> - ))) - )} - </div> - )) || ''} + )) + || (<strong> No orderable location.</strong>)} + {phaseType === 'A' && ( + (buildCount === null && ( + <strong> (unknown build count)</strong> + )) + || (buildCount === 0 ? ( + <strong> (nothing to build or disband)</strong> + ) : (buildCount > 0 ? ( + <strong> ({buildCount} unit{buildCount > 1 && 's'} may be built)</strong> + ) : ( + <strong> ({-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 { |