diff --git a/.env.dev b/.env.dev index 78c0e23000..280de1027a 100644 --- a/.env.dev +++ b/.env.dev @@ -8,9 +8,9 @@ NEXT_PUBLIC_SITE_DOMAIN=https://web-develop.matters.news NEXT_PUBLIC_ASSET_DOMAIN=https://assets-develop.matters.news -NEXT_PUBLIC_API_URL=https://server-stage.matters.news/graphql -NEXT_PUBLIC_WS_URL=wss://server-stage.matters.news/graphql -NEXT_PUBLIC_OAUTH_URL=https://server-stage.matters.news/oauth +NEXT_PUBLIC_API_URL=https://server-develop.matters.news/graphql +NEXT_PUBLIC_WS_URL=wss://server-develop.matters.news/graphql +NEXT_PUBLIC_OAUTH_URL=https://server-develop.matters.news/oauth NEXT_PUBLIC_SEGMENT_KEY=3gE20MjzN9qncFqlKV0pDvNO7Cp2gWU3 NEXT_PUBLIC_FB_APP_ID=823885921293850 NEXT_PUBLIC_SENTRY_DSN=https://409be482d1da4670879048d7d943c38e@sentry.matters.one/2 diff --git a/package-lock.json b/package-lock.json index 96e4d99d0d..5cfa266b92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "3.9.0", + "version": "3.10.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -58,17 +58,27 @@ } }, "@apollo/federation": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.16.2.tgz", - "integrity": "sha512-pjTkcl1KGxLZOPpVyTygZNuLxZJCCMvGVonPJMoFzQYt63/o0DwpwbcNlbvpdryWjjFgvi5diqJxRFGuxndEPA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@apollo/federation/-/federation-0.17.0.tgz", + "integrity": "sha512-vSW/M8+SGdu5xALsA/RL37GgB+wNFZpXCyPAcg3b68c8x7uoQHgYwqwUu7D+GnAGeOpDUrNnFPdKAYW7elYkyQ==", "dev": true, "requires": { "apollo-graphql": "^0.4.0", - "apollo-server-env": "^2.4.4", + "apollo-server-env": "^2.4.5", "core-js": "^3.4.0", "lodash.xorby": "^4.7.0" }, "dependencies": { + "apollo-graphql": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.4.5.tgz", + "integrity": "sha512-0qa7UOoq7E71kBYE7idi6mNQhHLVdMEDInWk6TNw3KsSWZE2/I68gARP84Mj+paFTO5NYuw1Dht66PVX76Cc2w==", + "dev": true, + "requires": { + "apollo-env": "^0.6.5", + "lodash.sortby": "^4.7.0" + } + }, "core-js": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", @@ -1727,9 +1737,9 @@ "dev": true }, "@endemolshinegroup/cosmiconfig-typescript-loader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.1.tgz", - "integrity": "sha512-bhUR9035PbgL6A/nfLayjoqKo4W7hCtzxqVxq2cgDB+Ndpsa3dGIr71/ymgY3vCTCQaufkFxAcEeoECyJ498CA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.2.tgz", + "integrity": "sha512-ZHkXKq2XFFmAUdmSZrmqUSIrRM4O9gtkdpxMmV+LQl7kScUnbo6pMnXu6+FTDgZ12aW6SDoZoOJfS56WD+Eu6A==", "dev": true, "requires": { "lodash.get": "^4", @@ -1739,14 +1749,14 @@ } }, "@firebase/analytics": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.3.8.tgz", - "integrity": "sha512-HpNRBJHnrGq5jtVTNRgA8Ozng2ilt0pkej8D5EvXoaylu80U+ICKLBlIT8TdUSEfkXC/RPjvLXg6vn/sq/CyqA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.4.0.tgz", + "integrity": "sha512-8DC2OBXGYxeeRxCh6eFnrrswNcKm2WsD8EeqGcl0F1P7J0bJ4Q+WpP3DvxofQZ/PtVHdAhzmfmt9r6Xa9mHnrQ==", "requires": { "@firebase/analytics-types": "0.3.1", - "@firebase/component": "0.1.15", - "@firebase/installations": "0.4.13", - "@firebase/logger": "0.2.5", + "@firebase/component": "0.1.16", + "@firebase/installations": "0.4.14", + "@firebase/logger": "0.2.6", "@firebase/util": "0.2.50", "tslib": "^1.11.1" } @@ -1757,13 +1767,13 @@ "integrity": "sha512-63vVJ5NIBh/JF8l9LuPrQYSzFimk7zYHySQB4Dk9rVdJ8kV/vGQoVTvRu1UW05sEc2Ug5PqtEChtTHU+9hvPcA==" }, "@firebase/app": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.7.tgz", - "integrity": "sha512-6NpIZ3iMrCR2XOShK5oi3YYB0GXX5yxVD8p3+2N+X4CF5cERyIrDRf8+YXOFgr+bDHSbVcIyzpWv6ijhg4MJlw==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.8.tgz", + "integrity": "sha512-Tm7Pi6Dtpx4FFKcpm0jcrZ/qI9oREBxmP3pWlw1jgDW4syRJHmN9/5DYvfFk6FAhj3FrY8E/6F+ngWJfqONotQ==", "requires": { "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.15", - "@firebase/logger": "0.2.5", + "@firebase/component": "0.1.16", + "@firebase/logger": "0.2.6", "@firebase/util": "0.2.50", "dom-storage": "2.1.0", "tslib": "^1.11.1", @@ -1776,9 +1786,9 @@ "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.14.7.tgz", - "integrity": "sha512-NTQY9luV70XUA6zGYOWloDSaOT+l0/R4u3W7ptqVCfZNc4DAt7euUkTbj7SDD14902sHF54j+tk5kmpEmMd0jA==", + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.14.9.tgz", + "integrity": "sha512-PxYa2r5qUEdheXTvqROFrMstK8W4uPiP7NVfp+2Bec+AjY5PxZapCx/YFDLkU0D7YBI82H74PtZrzdJZw7TJ4w==", "requires": { "@firebase/auth-types": "0.10.1" } @@ -1794,23 +1804,23 @@ "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==" }, "@firebase/component": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.15.tgz", - "integrity": "sha512-HqFb1qQl1vtlUMIzPM15plNz27jqM8DWjuQQuGeDfG+4iRRflwKfgNw1BOyoP4kQ8vOBCL7t/71yPXSomNdJdQ==", + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.16.tgz", + "integrity": "sha512-FvffvFN0LWgv1H/FIyruTECOL69Dhy+JfwoTq+mV39V8Mz9lNpo41etonL5AOr7KmXxYJVbNwkx0L9Ei88i7JA==", "requires": { "@firebase/util": "0.2.50", "tslib": "^1.11.1" } }, "@firebase/database": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.6.tgz", - "integrity": "sha512-TqUJOaCATF/h3wpqhPT9Fz1nZI6gBv/M2pHZztUjX4A9o9Bq93NyqUurYiZnGB7zpSkEADFCVT4f0VBrWdHlNw==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.8.tgz", + "integrity": "sha512-Psibz/LD9WBvZRS7A/kkYd5i5l6tBw49adSFmCM2ZJlKE9fxZhxay02AerwfXHiq3gPKVeqXUjBIRuHOWdEXmw==", "requires": { "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.15", + "@firebase/component": "0.1.16", "@firebase/database-types": "0.5.1", - "@firebase/logger": "0.2.5", + "@firebase/logger": "0.2.6", "@firebase/util": "0.2.50", "faye-websocket": "0.11.3", "tslib": "^1.11.1" @@ -1825,13 +1835,13 @@ } }, "@firebase/firestore": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-1.15.5.tgz", - "integrity": "sha512-unkRIC2hL2Ge5er/Hj43aUYiEKlW5bpju8TnIaF33avg/wZpSsmtVrMlAQVkBWFhvWeYpJSr2QOzNLa1bQvuCA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-1.16.1.tgz", + "integrity": "sha512-TGtvNIGHMEFFEuOSsRswou576GPZY39vXIsenn0B1Dqz9ACpyDtvAT9YdbG38srlPq7ZKwsP5x04LB43zZ6eAg==", "requires": { - "@firebase/component": "0.1.15", - "@firebase/firestore-types": "1.11.0", - "@firebase/logger": "0.2.5", + "@firebase/component": "0.1.16", + "@firebase/firestore-types": "1.12.0", + "@firebase/logger": "0.2.6", "@firebase/util": "0.2.50", "@firebase/webchannel-wrapper": "0.2.41", "@grpc/grpc-js": "^1.0.0", @@ -1840,16 +1850,16 @@ } }, "@firebase/firestore-types": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-1.11.0.tgz", - "integrity": "sha512-hD7+cmMUvT5OJeWVrcRkE87PPuj/0/Wic6bntCopJE1WIX/Dm117AUkHgKd3S7Ici6DLp4bdlx1MjjwWL5942w==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-1.12.0.tgz", + "integrity": "sha512-OqNxVb63wPZdUc7YnpacAW1WNIMSKERSewCRi+unCQ0YI0KNfrDSypyGCyel+S3GdOtKMk9KnvDknaGbnaFX4g==" }, "@firebase/functions": { - "version": "0.4.47", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.4.47.tgz", - "integrity": "sha512-wiyMezW1EYq80Uk15M4poapCG10PjN5UJEY0jJr7DhCnDAoADMGlsIYFYio60+biGreij5/hpOybw5mU9WpXUw==", + "version": "0.4.48", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.4.48.tgz", + "integrity": "sha512-BwI/JzO/f/nquKG1IS3VqmwMaKEhvM58/08vTnp46krHBsOYqsdD9T2amz+HXGT9fe2HhDsUhgFE8D00S0vqbg==", "requires": { - "@firebase/component": "0.1.15", + "@firebase/component": "0.1.16", "@firebase/functions-types": "0.3.17", "@firebase/messaging-types": "0.4.5", "isomorphic-fetch": "2.2.1", @@ -1862,11 +1872,11 @@ "integrity": "sha512-DGR4i3VI55KnYk4IxrIw7+VG7Q3gA65azHnZxo98Il8IvYLr2UTBlSh72dTLlDf25NW51HqvJgYJDKvSaAeyHQ==" }, "@firebase/installations": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.4.13.tgz", - "integrity": "sha512-Sic7BtWgdUwk+Z1C4L49Edkhzaol/ijEIdv0pkHfjedIPirIU2V8CJ5qykx2y4aTiyVbdFqfjIpp1c6A6W3GBA==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.4.14.tgz", + "integrity": "sha512-hQPsaU7wdTq3CFMtFQwZy6LgdXZAkXoUToV4O+ekPbjM65QzaGVogJVU8O2H6ADXoq37SarcUXKe86pcUWdFLA==", "requires": { - "@firebase/component": "0.1.15", + "@firebase/component": "0.1.16", "@firebase/installations-types": "0.3.4", "@firebase/util": "0.2.50", "idb": "3.0.2", @@ -1879,17 +1889,17 @@ "integrity": "sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q==" }, "@firebase/logger": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.5.tgz", - "integrity": "sha512-qqw3m0tWs/qrg7axTZG/QZq24DIMdSY6dGoWuBn08ddq7+GLF5HiqkRj71XznYeUUbfRq5W9C/PSFnN4JxX+WA==" + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" }, "@firebase/messaging": { - "version": "0.6.19", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.6.19.tgz", - "integrity": "sha512-PhqK69m70G+GGgvbdnGz2+PyoqfmR5b+nouj1JV+HgyBCjMAhF8rDYQzCWWgy4HaWbLoS/xW6AZUKG20Kv2H1A==", + "version": "0.6.20", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.6.20.tgz", + "integrity": "sha512-1MqyljXnbFBeHYhL6QInVM9aO5MW820yhNmOIVxk58wNXq4tOQLzqnKuvlgZ+ttgqlDzrIYiVf3EOHh5DptttQ==", "requires": { - "@firebase/component": "0.1.15", - "@firebase/installations": "0.4.13", + "@firebase/component": "0.1.16", + "@firebase/installations": "0.4.14", "@firebase/messaging-types": "0.4.5", "@firebase/util": "0.2.50", "idb": "3.0.2", @@ -1902,13 +1912,13 @@ "integrity": "sha512-sux4fgqr/0KyIxqzHlatI04Ajs5rc3WM+WmtCpxrKP1E5Bke8xu/0M+2oy4lK/sQ7nov9z15n3iltAHCgTRU3Q==" }, "@firebase/performance": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.3.8.tgz", - "integrity": "sha512-jODXrtFLyfnRiBehHuMBmsBtMv38U9sTictRxJSz+9JahvWYm1AF0YDzPlfeyYj+kxM6+S5wdQxUaPVdcWAvWg==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.3.9.tgz", + "integrity": "sha512-Fj22DZXRhhKv1OSUzDxX7AqpJUcDld6tzXK1yxOC8e3v1DFPQMQdM9FoG1m1b/Vrqa6pCCqnqG6gh6VPnEcAzQ==", "requires": { - "@firebase/component": "0.1.15", - "@firebase/installations": "0.4.13", - "@firebase/logger": "0.2.5", + "@firebase/component": "0.1.16", + "@firebase/installations": "0.4.14", + "@firebase/logger": "0.2.6", "@firebase/performance-types": "0.0.13", "@firebase/util": "0.2.50", "tslib": "^1.11.1" @@ -1942,13 +1952,13 @@ } }, "@firebase/remote-config": { - "version": "0.1.24", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.1.24.tgz", - "integrity": "sha512-/Kd+I5mNPI2wJJFySOC8Mjj4lRnEwZhU0RteuVlzFCDWWEyTE//r+p2TLAufQ9J+Fd3Ru5fVMFLNyU8k71Viiw==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.1.25.tgz", + "integrity": "sha512-8YWefBhy77HMbWXWdbenalx+IDY/XkS+iURQ9qRYvSIFYx6RL04DzlakZNOY9CQAcxTA+cTSt4NNlhjopBjf2Q==", "requires": { - "@firebase/component": "0.1.15", - "@firebase/installations": "0.4.13", - "@firebase/logger": "0.2.5", + "@firebase/component": "0.1.16", + "@firebase/installations": "0.4.14", + "@firebase/logger": "0.2.6", "@firebase/remote-config-types": "0.1.9", "@firebase/util": "0.2.50", "tslib": "^1.11.1" @@ -1960,20 +1970,20 @@ "integrity": "sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA==" }, "@firebase/storage": { - "version": "0.3.37", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.3.37.tgz", - "integrity": "sha512-RLbiRQlnvXRP/30OaEiUoRHBxZygqrZyotPPWD2WmD3JMM9qGTVpYNQ092mqL3R8ViyejwlpjlPvrDo7Z9BzgQ==", + "version": "0.3.40", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.3.40.tgz", + "integrity": "sha512-xTUvSSXh8tNSlch4V+kNbw736H0z/lbW3rHlx1kZVnT8V5M4bXE+TEcG4WpqvcWH3p+N6N1bUorkDbOFgBrztw==", "requires": { - "@firebase/component": "0.1.15", - "@firebase/storage-types": "0.3.12", + "@firebase/component": "0.1.16", + "@firebase/storage-types": "0.3.13", "@firebase/util": "0.2.50", "tslib": "^1.11.1" } }, "@firebase/storage-types": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.12.tgz", - "integrity": "sha512-DDV6Fs6aYoGw3w/zZZTkqiipxihnsvHf6znbeZYjIIHit3tr1uLJdGPDPiCTfZcTGPpg2ux6ZmvNDvVgJdHALw==" + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.3.13.tgz", + "integrity": "sha512-pL7b8d5kMNCCL0w9hF7pr16POyKkb3imOW7w0qYrhBnbyJTdVxMWZhb0HxCFyQWC0w3EiIFFmxoz8NTFZDEFog==" }, "@firebase/util": { "version": "0.2.50", @@ -1989,17 +1999,17 @@ "integrity": "sha512-XcdMT5PSZHiuf7LJIhzKIe+RyYa25S3LHRRvLnZc6iFjwXkrSDJ8J/HWO6VT8d2ZTbawp3VcLEjRF/VN8glCrA==" }, "@grpc/grpc-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.1.tgz", - "integrity": "sha512-mhZRszS0SKwnWPJaNyrECePZ9U7vaHFGqrzxQbWinWR3WznBIU+nmh2L5J3elF+lp5DEUIzARXkifbs6LQVAHA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.3.tgz", + "integrity": "sha512-HtOsk2YUofBcm1GkPqGzb6pwHhv+74eC2CUO229USIDKRtg30ycbZmqC+HdNtY3nHqoc9IgcRlntFgopyQoYCA==", "requires": { "semver": "^6.2.0" } }, "@grpc/proto-loader": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", - "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" @@ -3436,13 +3446,13 @@ } }, "@oclif/command": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.6.1.tgz", - "integrity": "sha512-pvmMmfGn+zm4e4RwVw63mg9sIaqKqmVsFbImQoUrCO/43UmWzoSHWNXKdgEGigOezWrkZfFucaeZcSbp149OWg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.7.0.tgz", + "integrity": "sha512-TkknFtWcZI8te0E8sW+ohiblExrLx73rIcV4KdIzDX01u+oTZWZaap51F6TSGFnR/Gey0WctaDvJhZlt4xgKdA==", "dev": true, "requires": { "@oclif/config": "^1.15.1", - "@oclif/errors": "^1.2.2", + "@oclif/errors": "^1.3.3", "@oclif/parser": "^3.8.3", "@oclif/plugin-help": "^3", "debug": "^4.1.1", @@ -3512,18 +3522,58 @@ "requires": { "ansi-regex": "^4.1.0" } + }, + "wrap-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", + "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } } } }, "@oclif/config": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.15.1.tgz", - "integrity": "sha512-GdyHpEZuWlfU8GSaZoiywtfVBsPcfYn1KuSLT1JTfvZGpPG6vShcGr24YZ3HG2jXUFlIuAqDcYlTzOrqOdTPNQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@oclif/config/-/config-1.16.0.tgz", + "integrity": "sha512-vOnMPQcHokC03WBCuLipTxksTwgZcmDOnH2H0UHqndfKKN9GVDzpZTH6zaFVQBdjTME5VtRzg9A2UaNmq6OXWw==", "dev": true, "requires": { - "@oclif/errors": "^1.0.0", + "@oclif/errors": "^1.3.3", "@oclif/parser": "^3.8.0", "debug": "^4.1.1", + "globby": "^11.0.1", + "is-wsl": "^2.1.1", "tslib": "^1.9.3" }, "dependencies": { @@ -3536,6 +3586,29 @@ "ms": "^2.1.1" } }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3545,44 +3618,32 @@ } }, "@oclif/errors": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.2.2.tgz", - "integrity": "sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.3.tgz", + "integrity": "sha512-EJR6AIOEkt/NnARNIVAskPDVtdhtO5TTNXmhDrGqMoWVsr0R6DkkLrMyq95BmHvlVWM1nduoq4fQPuCyuF2jaA==", "dev": true, "requires": { - "clean-stack": "^1.3.0", - "fs-extra": "^7.0.0", - "indent-string": "^3.2.0", - "strip-ansi": "^5.0.0", - "wrap-ansi": "^4.0.0" + "clean-stack": "^3.0.0", + "fs-extra": "^9.0.1", + "indent-string": "^4.0.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, "clean-stack": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-1.3.0.tgz", - "integrity": "sha1-noIVAa6XmYbEax1m0tQy2y/UrjE=", - "dev": true - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.0.tgz", + "integrity": "sha512-RHxtgFvXsRQ+1AM7dlozLDY7ssmvUUh0XEnfnyhYgJTO6beNZHBogiaCwGM9Q3rFrUkYxOtsZRC0zAturg5bjg==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "escape-string-regexp": "4.0.0" } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true } } }, @@ -3628,6 +3689,17 @@ "ms": "^2.1.1" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3677,6 +3749,44 @@ "requires": { "ansi-regex": "^4.1.0" } + }, + "wrap-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", + "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } } } }, @@ -3734,6 +3844,23 @@ "tslib": "^1.9.3" } }, + "extract-stack": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-1.0.0.tgz", + "integrity": "sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo=", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "indent-string": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", @@ -3767,12 +3894,12 @@ } }, "@oclif/plugin-plugins": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-1.7.9.tgz", - "integrity": "sha512-o7qfmiUGl+NUyA2lM18/Ch5sasGGYPIINR3cZ/AjwtdQ3ooINnF00pUDcUOtbjW97gRmk6/j79tcyTo8i7rHZg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-1.9.0.tgz", + "integrity": "sha512-sq31nJk/n5pH5qGDioj2Z9x6MlRUrc/kkQrfCYKRPbQM80qewSP4RcPK3/gDvDSOAWD3wLAK9oMbDQO9lqImMA==", "dev": true, "requires": { - "@oclif/color": "^0.0.0", + "@oclif/color": "^0.x", "@oclif/command": "^1.5.12", "chalk": "^2.4.2", "cli-ux": "^5.2.1", @@ -3781,22 +3908,11 @@ "http-call": "^5.2.2", "load-json-file": "^5.2.0", "npm-run-path": "^3.0.0", - "semver": "^5.6.0", - "tslib": "^1.9.3", + "semver": "^7.3.2", + "tslib": "^2.0.0", "yarn": "^1.21.1" }, "dependencies": { - "@oclif/color": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@oclif/color/-/color-0.0.0.tgz", - "integrity": "sha512-KKd3W7eNwfNF061tr663oUNdt8EMnfuyf5Xv55SGWA1a0rjhWqS/32P7OeB7CbXcJUBdfVrPyR//1afaW12AWw==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "supports-color": "^5.4.0", - "tslib": "^1" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -3806,6 +3922,17 @@ "ms": "^2.1.1" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3813,19 +3940,16 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==", + "dev": true } } }, @@ -3855,6 +3979,17 @@ "ms": "^2.1.1" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4152,59 +4287,59 @@ } }, "@sentry/browser": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.19.1.tgz", - "integrity": "sha512-Aon5Nc2n8sIXKg6Xbr4RM3/Xs7vFpXksL56z3yIuGrmpCM8ToQ25/tQv8h+anYi72x5bn1npzaXB/NwU1Qwfhg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.20.0.tgz", + "integrity": "sha512-xVPL7/RuAPcemfSzXiyPHAt4M+0BfzkdTlN+PZb6frCEo4k6E0UiN6WLsGj/iwa2gXhyfTQXtbTuP+tDuNPEJw==", "requires": { - "@sentry/core": "5.19.1", - "@sentry/types": "5.19.1", - "@sentry/utils": "5.19.1", + "@sentry/core": "5.20.0", + "@sentry/types": "5.20.0", + "@sentry/utils": "5.20.0", "tslib": "^1.9.3" } }, "@sentry/core": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.19.1.tgz", - "integrity": "sha512-BGGxjeT95Og/hloBhQXAVcndVXPmIU6drtF3oKRT12cBpiG965xEDEUwiJVvyb5MAvojdVEZBK2LURUFY/d7Zw==", - "requires": { - "@sentry/hub": "5.19.1", - "@sentry/minimal": "5.19.1", - "@sentry/types": "5.19.1", - "@sentry/utils": "5.19.1", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.20.0.tgz", + "integrity": "sha512-fzzWKEolc0O6H/phdDenzKs7JXDSb0sooxVn0QCUkwWSzACALQh+NR/UciOXyhyuoUiqu4zthYQx02qtGqizeQ==", + "requires": { + "@sentry/hub": "5.20.0", + "@sentry/minimal": "5.20.0", + "@sentry/types": "5.20.0", + "@sentry/utils": "5.20.0", "tslib": "^1.9.3" } }, "@sentry/hub": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.19.1.tgz", - "integrity": "sha512-XjfbNGWVeDsP38alm5Cm08YPIw5Hu6HbPkw7a3y1piViTrg4HdtsE+ZJqq0YcURo2RTpg6Ks6coCS/zJxIPygQ==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.20.0.tgz", + "integrity": "sha512-DiU8fpjAAMOgSx5tsTekMtHPCAtSNWSNS91FFkDCqPn6fYG+/aK/hB5kTlJwr+GTM1815+WWrtXP6y2ecSmZuA==", "requires": { - "@sentry/types": "5.19.1", - "@sentry/utils": "5.19.1", + "@sentry/types": "5.20.0", + "@sentry/utils": "5.20.0", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.19.1.tgz", - "integrity": "sha512-pgNfsaCroEsC8gv+NqmPTIkj4wyK6ZgYLV12IT4k2oJLkGyg45TSAKabyB7oEP5jsj8sRzm8tDomu8M4HpaCHg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.20.0.tgz", + "integrity": "sha512-oA+0g7p3bapzjgGKQIkSjcjA85VG1HPmjxBD9wpRvNjmYuVmm80Cl1H/P+xg/hupw/kNmASAX4IOd5Z9pEeboA==", "requires": { - "@sentry/hub": "5.19.1", - "@sentry/types": "5.19.1", + "@sentry/hub": "5.20.0", + "@sentry/types": "5.20.0", "tslib": "^1.9.3" } }, "@sentry/types": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.19.1.tgz", - "integrity": "sha512-M5MhTLnjqYFwxMwcFPBpBgYQqI9hCvtVuj/A+NvcBHpe7VWOXdn/Sys+zD6C76DWGFYQdw3OWCsZimP24dL8mA==" + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.20.0.tgz", + "integrity": "sha512-/9tiGiXBRsOKM66HeCpt0iSF0vnAIqHzXgC97icNQIstx/ZA8tcLs9540cHDeaN0cyZUyZF1o8ECqcLXGNODWQ==" }, "@sentry/utils": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.19.1.tgz", - "integrity": "sha512-neUiNBnZSHjWTZWy2QV02EHTx1C2L3DBPzRXlh0ca5xrI7LMBLmhkHlhebn1E5ky3PW1teqZTgmh0jZoL99TEA==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.20.0.tgz", + "integrity": "sha512-w0AeAzWEf35h9U9QL/4lgS9MqaTPjeSmQYNU/n4ef3FKr+u8HP68Ra7NZ0adiKgi67Yxr652kWopOLPl7CxvZg==", "requires": { - "@sentry/types": "5.19.1", + "@sentry/types": "5.20.0", "tslib": "^1.9.3" } }, @@ -5624,22 +5759,22 @@ "dev": true }, "@testing-library/dom": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.20.0.tgz", - "integrity": "sha512-TywaC+qDGm/Ro34kRYkFQPdT+pxSF4UjZGLIqcGfFQH5IGR43Y7sGLPnkieIW/GNsu337oxNsLUAgpI0JWhXHw==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.21.0.tgz", + "integrity": "sha512-S8TTMCd7qCkLs6bhdh2+fk7/pIrD16UKvnwa6gJICalZzV1xoAxDY9Isp6qmelizYH4P1Tz+O5Y4nMmjx3x0uQ==", "dev": true, "requires": { "@babel/runtime": "^7.10.3", "@types/aria-query": "^4.2.0", "aria-query": "^4.2.2", - "dom-accessibility-api": "^0.4.5", + "dom-accessibility-api": "^0.4.6", "pretty-format": "^25.5.0" }, "dependencies": { "@babel/runtime": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz", - "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" @@ -5648,30 +5783,23 @@ } }, "@testing-library/react": { - "version": "10.4.4", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.4.4.tgz", - "integrity": "sha512-SKDQ2jBdg9UQQYQragkvXOzNp4hnCdOvXyZ52rg+OXiiumVxkAutdvvRzBF4PrbvMQ27Z6gx0GVo2YQ1Mcip8g==", + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.4.7.tgz", + "integrity": "sha512-hUYbum3X2f1ZKusKfPaooKNYqE/GtPiQ+D2HJaJ4pkxeNJQFVUEvAvEh9+3QuLdBeTWkDMNY5NSijc5+pGdM4Q==", "dev": true, "requires": { "@babel/runtime": "^7.10.3", - "@testing-library/dom": "^7.17.1", - "semver": "^7.3.2" + "@testing-library/dom": "^7.17.1" }, "dependencies": { "@babel/runtime": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz", - "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true } } }, @@ -5791,16 +5919,10 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, "@types/express": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", - "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", + "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", "dev": true, "requires": { "@types/body-parser": "*", @@ -5810,9 +5932,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", - "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", + "version": "4.17.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.8.tgz", + "integrity": "sha512-1SJZ+R3Q/7mLkOD9ewCBDYD2k0WyZQtWYqF/2VvoNN2/uhI49J9CDN4OAm+wGMA0DbArA4ef27xl4+JwMtGggw==", "dev": true, "requires": { "@types/node": "*", @@ -5827,12 +5949,11 @@ "dev": true }, "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { - "@types/events": "*", "@types/minimatch": "*", "@types/node": "*" } @@ -5887,9 +6008,9 @@ } }, "@types/jest": { - "version": "26.0.3", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.3.tgz", - "integrity": "sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg==", + "version": "26.0.5", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.5.tgz", + "integrity": "sha512-heU+7w8snfwfjtcj2H458aTx3m5unIToOJhx75ebHilBiiQ39OIdA18WkG4LP08YKeAoWAGvWg8s+22w/PeJ6w==", "dev": true, "requires": { "jest-diff": "^25.2.1", @@ -5912,9 +6033,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.157", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.157.tgz", - "integrity": "sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==", + "version": "4.14.158", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.158.tgz", + "integrity": "sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w==", "dev": true }, "@types/long": { @@ -6017,9 +6138,9 @@ "dev": true }, "@types/react": { - "version": "16.9.41", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.41.tgz", - "integrity": "sha512-6cFei7F7L4wwuM+IND/Q2cV1koQUvJ8iSV+Gwn0c3kvABZ691g7sp3hfEQHOUBJtccl1gPi+EyNjMIl9nGA0ug==", + "version": "16.9.43", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.43.tgz", + "integrity": "sha512-PxshAFcnJqIWYpJbLPriClH53Z2WlJcVZE+NP2etUtWQs2s7yIMj3/LDKZT/5CHJ/F62iyjVCDu2H3jHEXIxSg==", "dev": true, "requires": { "@types/prop-types": "*", @@ -6574,43 +6695,43 @@ } }, "apollo": { - "version": "2.28.3", - "resolved": "https://registry.npmjs.org/apollo/-/apollo-2.28.3.tgz", - "integrity": "sha512-+X1RqODYOz1VPO0a/6tZpZiFQr7K6z0ZSm7H9oT9PmuZ9TMC27mwOh2N0i1p+OP+6JORKh8lHmjMvle+doZ82A==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/apollo/-/apollo-2.30.1.tgz", + "integrity": "sha512-poh2tja2U8U5bGMxfPQjQP1voz2ZaNm/attwC8zpobdeLoT43LeQfFSTqCVmCMZJAfbMa40Cb54yV66DP2w9fw==", "dev": true, "requires": { "@apollographql/apollo-tools": "^0.4.8", - "@oclif/command": "1.6.1", - "@oclif/config": "1.15.1", - "@oclif/errors": "1.2.2", + "@oclif/command": "1.7.0", + "@oclif/config": "1.16.0", + "@oclif/errors": "1.3.3", "@oclif/plugin-autocomplete": "0.2.0", "@oclif/plugin-help": "2.2.3", "@oclif/plugin-not-found": "1.2.4", - "@oclif/plugin-plugins": "1.7.9", + "@oclif/plugin-plugins": "1.9.0", "@oclif/plugin-warn-if-update-available": "1.7.0", - "apollo-codegen-core": "^0.37.3", - "apollo-codegen-flow": "^0.35.3", - "apollo-codegen-scala": "^0.36.3", - "apollo-codegen-swift": "^0.37.3", - "apollo-codegen-typescript": "^0.37.3", + "apollo-codegen-core": "^0.37.7", + "apollo-codegen-flow": "^0.35.7", + "apollo-codegen-scala": "^0.36.7", + "apollo-codegen-swift": "^0.37.7", + "apollo-codegen-typescript": "^0.37.7", "apollo-env": "^0.6.5", - "apollo-graphql": "^0.4.5", - "apollo-language-server": "^1.22.3", + "apollo-graphql": "^0.5.0", + "apollo-language-server": "^1.23.2", "chalk": "2.4.2", - "cli-ux": "5.4.6", + "cli-ux": "5.4.9", "env-ci": "3.2.2", "gaze": "1.1.3", "git-parse": "1.0.4", "git-rev-sync": "2.0.0", "git-url-parse": "^11.1.2", "glob": "7.1.5", - "graphql": "14.0.2 - 14.2.0 || ^14.3.1", - "graphql-tag": "2.10.3", + "graphql": "14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0", + "graphql-tag": "2.10.4", "listr": "0.14.3", "lodash.identity": "3.0.0", "lodash.pickby": "4.6.0", "mkdirp": "0.5.5", - "moment": "2.26.0", + "moment": "2.27.0", "strip-ansi": "5.2.0", "table": "5.4.6", "tty": "1.0.1", @@ -6699,40 +6820,46 @@ } }, "apollo-codegen-core": { - "version": "0.37.3", - "resolved": "https://registry.npmjs.org/apollo-codegen-core/-/apollo-codegen-core-0.37.3.tgz", - "integrity": "sha512-/DwAhOOFzl57GdBfRGNnqIAcfZAXpsgFIeWYqlu3I/eIucGBCFWo9CEW1TcNwkZzYGAmSE8tURwPgt7dtnhFpg==", + "version": "0.37.7", + "resolved": "https://registry.npmjs.org/apollo-codegen-core/-/apollo-codegen-core-0.37.7.tgz", + "integrity": "sha512-7AMnzS+X7z91eUSctc0mQoQzVJrrKo+zLXevMDkGyTH+q541dYfpAdKQ5nffPcb1ZwwOONZCyl8kc8faJzD0Kw==", "dev": true, "requires": { - "@babel/generator": "7.10.2", + "@babel/generator": "7.10.4", "@babel/parser": "^7.1.3", - "@babel/types": "7.10.2", + "@babel/types": "7.10.4", "apollo-env": "^0.6.5", - "apollo-language-server": "^1.22.3", + "apollo-language-server": "^1.23.2", "ast-types": "^0.13.0", "common-tags": "^1.5.1", "recast": "^0.19.0" }, "dependencies": { "@babel/generator": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", - "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", "dev": true, "requires": { - "@babel/types": "^7.10.2", + "@babel/types": "^7.10.4", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/types": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", - "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -6746,38 +6873,44 @@ } }, "apollo-codegen-flow": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/apollo-codegen-flow/-/apollo-codegen-flow-0.35.3.tgz", - "integrity": "sha512-npDt9PEJiw/ygKUsKxaDGHCnjQUANPlg/F9piIbQ71jwBjNNoNSXrRaRiD5632MfcTvEuBvanElEX3AO2sA1gw==", + "version": "0.35.7", + "resolved": "https://registry.npmjs.org/apollo-codegen-flow/-/apollo-codegen-flow-0.35.7.tgz", + "integrity": "sha512-q7GsbHE0UtqXFat8wGyidUJRdGkbtfUqCtuQkV5qKOOnudFR32G7dz+6i/Z9R5IqOqWVMpxLq7UeiYRiz8c1dg==", "dev": true, "requires": { - "@babel/generator": "7.10.2", - "@babel/types": "7.10.2", - "apollo-codegen-core": "^0.37.3", - "change-case": "^3.0.1", + "@babel/generator": "7.10.4", + "@babel/types": "7.10.4", + "apollo-codegen-core": "^0.37.7", + "change-case": "^4.0.0", "common-tags": "^1.5.1", "inflected": "^2.0.3" }, "dependencies": { "@babel/generator": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", - "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", "dev": true, "requires": { - "@babel/types": "^7.10.2", + "@babel/types": "^7.10.4", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/types": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", - "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -6791,62 +6924,68 @@ } }, "apollo-codegen-scala": { - "version": "0.36.3", - "resolved": "https://registry.npmjs.org/apollo-codegen-scala/-/apollo-codegen-scala-0.36.3.tgz", - "integrity": "sha512-KOmcP/0RiqSGY03fXdF87IzQ1RSsApbno3/M8KRVF2yhf+4X3GQXQRpXejHMt4DxnnfPBOn7MqwtP/cDMJIiyw==", + "version": "0.36.7", + "resolved": "https://registry.npmjs.org/apollo-codegen-scala/-/apollo-codegen-scala-0.36.7.tgz", + "integrity": "sha512-x8EWMOrW4e/kl5QFUHCJsJzemPk1Fa0hMCyjgnlBGQHBrAkHzc33qMMs6WTGvLLL8x8sMvqxCX+NiE/jgtYEvg==", "dev": true, "requires": { - "apollo-codegen-core": "^0.37.3", - "change-case": "^3.0.1", + "apollo-codegen-core": "^0.37.7", + "change-case": "^4.0.0", "common-tags": "^1.5.1", "inflected": "^2.0.3" } }, "apollo-codegen-swift": { - "version": "0.37.3", - "resolved": "https://registry.npmjs.org/apollo-codegen-swift/-/apollo-codegen-swift-0.37.3.tgz", - "integrity": "sha512-0BpwtSE+IP12C7OdoScsOWUgxbcuThgwVRsxybBNugUcmddF5sCdGiRbdm+oKvX1vErsG+XimwmIuZnBPWv6ew==", + "version": "0.37.7", + "resolved": "https://registry.npmjs.org/apollo-codegen-swift/-/apollo-codegen-swift-0.37.7.tgz", + "integrity": "sha512-97uCfBt3UVq0hlAWIBZpQoZjgdeKGObxsNp2L2R5ldMLoD3cQzjzuUDJGG1DoAsn5RMqv2gGNEk5QZMrWhidLw==", "dev": true, "requires": { - "apollo-codegen-core": "^0.37.3", - "change-case": "^3.0.1", + "apollo-codegen-core": "^0.37.7", + "change-case": "^4.0.0", "common-tags": "^1.5.1", "inflected": "^2.0.3" } }, "apollo-codegen-typescript": { - "version": "0.37.3", - "resolved": "https://registry.npmjs.org/apollo-codegen-typescript/-/apollo-codegen-typescript-0.37.3.tgz", - "integrity": "sha512-tuf/AQTFcNrngQrT4q4WKRdiuPbglyR1m3L58g/nNevoO0cRmF6koIix4NB5sO05LgF8XJmd2zHvInUI5v33Ig==", + "version": "0.37.7", + "resolved": "https://registry.npmjs.org/apollo-codegen-typescript/-/apollo-codegen-typescript-0.37.7.tgz", + "integrity": "sha512-LIx1tsWqRrhTcYcRPjhbzBwSaCbMK3UKSN+AlOzNDvG/Rm6wFutHznj14kn/iqcIHmCbGGuFNjiZNbLwCJ3SyQ==", "dev": true, "requires": { - "@babel/generator": "7.10.2", - "@babel/types": "7.10.2", - "apollo-codegen-core": "^0.37.3", - "change-case": "^3.0.1", + "@babel/generator": "7.10.4", + "@babel/types": "7.10.4", + "apollo-codegen-core": "^0.37.7", + "change-case": "^4.0.0", "common-tags": "^1.5.1", "inflected": "^2.0.3" }, "dependencies": { "@babel/generator": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz", - "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", "dev": true, "requires": { - "@babel/types": "^7.10.2", + "@babel/types": "^7.10.4", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/types": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz", - "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.1", + "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -6860,13 +6999,13 @@ } }, "apollo-datasource": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.1.tgz", - "integrity": "sha512-h++/jQAY7GA+4TBM+7ezvctFmmGNLrAPf51KsagZj+NkT9qvxp585rdsuatynVbSl59toPK2EuVmc6ilmQHf+g==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", + "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", "dev": true, "requires": { - "apollo-server-caching": "^0.5.1", - "apollo-server-env": "^2.4.4" + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" } }, "apollo-env": { @@ -6896,9 +7035,9 @@ } }, "apollo-graphql": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.4.5.tgz", - "integrity": "sha512-0qa7UOoq7E71kBYE7idi6mNQhHLVdMEDInWk6TNw3KsSWZE2/I68gARP84Mj+paFTO5NYuw1Dht66PVX76Cc2w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.5.0.tgz", + "integrity": "sha512-YSdF/BKPbsnQpxWpmCE53pBJX44aaoif31Y22I/qKpB6ZSGzYijV5YBoCL5Q15H2oA/v/02Oazh9lbp4ek3eig==", "dev": true, "requires": { "apollo-env": "^0.6.5", @@ -6906,18 +7045,18 @@ } }, "apollo-language-server": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/apollo-language-server/-/apollo-language-server-1.22.3.tgz", - "integrity": "sha512-RurKlBUNE1RrvY4m93b5WS/DXInUEI47MlzuvholRqZSQovt2rQi81R0RvmS/l3d6y5TBfxbPFpT5RyHbdAntw==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/apollo-language-server/-/apollo-language-server-1.23.2.tgz", + "integrity": "sha512-2EfnA0DUVhGk018XYPb44EM+KuBnAqdciRD+j9BuUT3+nVq7pc8pjjcS7M8r5ea8hnOYrAoxC6f4I2YNdhjHJg==", "dev": true, "requires": { - "@apollo/federation": "0.16.2", + "@apollo/federation": "0.17.0", "@apollographql/apollo-tools": "^0.4.8", "@apollographql/graphql-language-service-interface": "^2.0.2", "@endemolshinegroup/cosmiconfig-typescript-loader": "^1.0.0", "apollo-datasource": "^0.7.0", "apollo-env": "^0.6.5", - "apollo-graphql": "^0.4.5", + "apollo-graphql": "^0.5.0", "apollo-link": "^1.2.3", "apollo-link-context": "^1.0.9", "apollo-link-error": "^1.1.1", @@ -6928,12 +7067,12 @@ "cosmiconfig": "^5.0.6", "dotenv": "^8.0.0", "glob": "^7.1.3", - "graphql": "14.0.2 - 14.2.0 || ^14.3.1", + "graphql": "14.0.2 - 14.2.0 || ^14.3.1 || ^15.0.0", "graphql-tag": "^2.10.1", "lodash.debounce": "^4.0.8", "lodash.merge": "^4.6.1", "minimatch": "^3.0.4", - "moment": "2.26.0", + "moment": "2.27.0", "vscode-languageserver": "^5.1.0", "vscode-uri": "1.0.6" }, @@ -7016,18 +7155,18 @@ } }, "apollo-server-caching": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz", - "integrity": "sha512-L7LHZ3k9Ao5OSf2WStvQhxdsNVplRQi7kCAPfqf9Z3GBEnQ2uaL0EgO0hSmtVHfXTbk5CTRziMT1Pe87bXrFIw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", + "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", "dev": true, "requires": { "lru-cache": "^5.0.0" } }, "apollo-server-env": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.4.tgz", - "integrity": "sha512-c2oddDS3lwAl6QNCIKCLEzt/dF9M3/tjjYRVdxOVN20TidybI7rAbnT4QOzf4tORnGXtiznEAvr/Kc9ahhKADg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", + "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", "dev": true, "requires": { "node-fetch": "^2.1.2", @@ -7043,9 +7182,9 @@ } }, "apollo-server-errors": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.1.tgz", - "integrity": "sha512-7oEd6pUxqyWYUbQ9TA8tM0NU/3aGtXSEibo6+txUkuHe7QaxfZ2wHRp+pfT1LC1K3RXYjKj61/C2xEO19s3Kdg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz", + "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==", "dev": true }, "apollo-utilities": { @@ -7112,18 +7251,18 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz", - "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/runtime-corejs3": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz", - "integrity": "sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", + "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", "dev": true, "requires": { "core-js-pure": "^3.0.0", @@ -7287,6 +7426,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -8526,13 +8671,13 @@ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" }, "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", + "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", "dev": true, "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" } }, "camelcase": { @@ -8585,6 +8730,38 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz", "integrity": "sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw==" }, + "capital-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.3.tgz", + "integrity": "sha512-OlUSJpUr7SY0uZFOxcwnDOU7/MpHlKTZx2mqnDYQFrDudXLFm0JJ9wr/l4csB+rh2Ug0OPuoSO53PqiZBqno9A==", + "dev": true, + "requires": { + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + } + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -8673,29 +8850,44 @@ } }, "change-case": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-3.1.0.tgz", - "integrity": "sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==", - "dev": true, - "requires": { - "camel-case": "^3.0.0", - "constant-case": "^2.0.0", - "dot-case": "^2.1.0", - "header-case": "^1.0.0", - "is-lower-case": "^1.1.0", - "is-upper-case": "^1.1.0", - "lower-case": "^1.1.1", - "lower-case-first": "^1.0.0", - "no-case": "^2.3.2", - "param-case": "^2.1.0", - "pascal-case": "^2.0.0", - "path-case": "^2.1.0", - "sentence-case": "^2.1.0", - "snake-case": "^2.1.0", - "swap-case": "^1.1.0", - "title-case": "^2.1.0", - "upper-case": "^1.1.1", - "upper-case-first": "^1.1.0" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.1.tgz", + "integrity": "sha512-qRlUWn/hXnX1R1LBDF/RelJLiqNjKjUqlmuBVSEIyye8kq49CXqkZWKmi8XeUAdDXWFOcGLUMZ+aHn3Q5lzUXw==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "capital-case": "^1.0.3", + "constant-case": "^3.0.3", + "dot-case": "^3.0.3", + "header-case": "^2.0.3", + "no-case": "^3.0.3", + "param-case": "^3.0.3", + "pascal-case": "^3.1.1", + "path-case": "^3.0.3", + "sentence-case": "^3.0.3", + "snake-case": "^3.0.3", + "tslib": "^1.10.0" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + } } }, "character-entities": { @@ -8767,24 +8959,59 @@ } }, "chromedriver": { - "version": "83.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-83.0.0.tgz", - "integrity": "sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ==", + "version": "84.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-84.0.1.tgz", + "integrity": "sha512-iJ6Y680yp58+KlAPS5YgYe3oePVFf8jY5k4YoczhXkT0p/mQZKfGNkGG/Xc0LjGWDQRTgZwXg66hOXoApIQecg==", "dev": true, "requires": { "@testim/chrome-version": "^1.0.7", "axios": "^0.19.2", "del": "^5.1.0", - "extract-zip": "^2.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", "mkdirp": "^1.0.4", "tcp-port-used": "^1.0.1" }, "dependencies": { + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -8879,9 +9106,9 @@ } }, "cli-spinners": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz", - "integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz", + "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==", "dev": true }, "cli-table3": { @@ -8943,9 +9170,9 @@ } }, "cli-ux": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.4.6.tgz", - "integrity": "sha512-EeiS2TzEndRVknCqE+8Ri8g0bsP617a1nq6n+3Trwft1JCDzyUNlX2J1fl7fwTgRPWtmBmiF6xIyueL5YGs65g==", + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.4.9.tgz", + "integrity": "sha512-4yCKJbFQqNQxf1v0E5T5aBJLt3SbW6dXc/R7zHp4ycdPMg9fAy5f2vhPsWgXEGCMQg+fgN0Sp7EYcZ1XGkFyUA==", "dev": true, "requires": { "@oclif/command": "^1.6.0", @@ -8955,33 +9182,27 @@ "ansi-escapes": "^4.3.0", "ansi-styles": "^4.2.0", "cardinal": "^2.1.1", - "chalk": "^2.4.1", - "clean-stack": "^2.0.0", + "chalk": "^3.0.0", + "clean-stack": "^3.0.0", "cli-progress": "^3.4.0", - "extract-stack": "^1.0.0", - "fs-extra": "^7.0.1", + "extract-stack": "^2.0.0", + "fs-extra": "^9.0.1", "hyperlinker": "^1.0.0", "indent-string": "^4.0.0", - "is-wsl": "^1.1.0", + "is-wsl": "^2.2.0", "js-yaml": "^3.13.1", "lodash": "^4.17.11", "natural-orderby": "^2.0.1", "object-treeify": "^1.1.4", "password-prompt": "^1.1.2", "semver": "^5.6.0", - "string-width": "^3.1.0", + "string-width": "^4.2.0", "strip-ansi": "^5.1.0", - "supports-color": "^5.5.0", + "supports-color": "^7.1.0", "supports-hyperlinks": "^1.0.1", - "tslib": "^1.9.3" + "tslib": "^2.0.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -8992,6 +9213,25 @@ "color-convert": "^2.0.1" } }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "clean-stack": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.0.tgz", + "integrity": "sha512-RHxtgFvXsRQ+1AM7dlozLDY7ssmvUUh0XEnfnyhYgJTO6beNZHBogiaCwGM9Q3rFrUkYxOtsZRC0zAturg5bjg==", + "dev": true, + "requires": { + "escape-string-regexp": "4.0.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9007,6 +9247,39 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -9014,14 +9287,25 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } } }, "strip-ansi": { @@ -9031,16 +9315,30 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } + }, + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==", + "dev": true } } }, @@ -9309,6 +9607,12 @@ "path-is-absolute": "^1.0.0" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -9396,13 +9700,44 @@ "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" }, "constant-case": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-2.0.0.tgz", - "integrity": "sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.3.tgz", + "integrity": "sha512-FXtsSnnrFYpzDmvwDGQW+l8XK3GV1coLyBN0eBz16ZUzGaZcT2ANVCJmLeuw2GQgxKHQIe9e0w2dzkSfaRlUmA==", "dev": true, "requires": { - "snake-case": "^2.1.0", - "upper-case": "^1.1.1" + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case": "^2.0.1" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + }, + "upper-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz", + "integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + } } }, "constants-browserify": { @@ -10450,9 +10785,9 @@ } }, "date-fns": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz", - "integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==" + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz", + "integrity": "sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==" }, "deasync": { "version": "0.1.20", @@ -10864,9 +11199,9 @@ } }, "dom-accessibility-api": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz", - "integrity": "sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.6.tgz", + "integrity": "sha512-qxFVFR/ymtfamEQT/AsYLe048sitxFCoCHiM+vuOdR3fE94i3so2SCFJxyz/RxV69PZ+9FgToYWOd7eqJqcbYw==", "dev": true }, "dom-helpers": { @@ -10956,12 +11291,34 @@ "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" }, "dot-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-2.1.1.tgz", - "integrity": "sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", + "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", "dev": true, "requires": { - "no-case": "^2.2.0" + "no-case": "^3.0.3", + "tslib": "^1.10.0" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + } } }, "dot-prop": { @@ -11212,9 +11569,9 @@ } }, "envinfo": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.5.1.tgz", - "integrity": "sha512-hQBkDf2iO4Nv0CNHpCuSBeaSrveU6nThVxFGTrq/eDlV716UQk09zChaJae4mZRsos1x4YLY2TaH3LHUae3ZmQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.0.tgz", + "integrity": "sha512-XX0+kACx7HcIFhar/JjsDtDIVcC8hnzQO1Asehq+abs+v9MtzpUuujFb6eBTT4lF9j2Bh6d2XFngbFRryjUAeQ==", "dev": true }, "errno": { @@ -11807,15 +12164,15 @@ "integrity": "sha512-qRW6y9eKF0VbCyOoOEtFhzJ3uykAw8GKwQVXyAIqwocyEWW4m+v+evec34RwtUkkxxHh7NKBLJ6AnXM8W4dH5w==" }, "extract-stack": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-1.0.0.tgz", - "integrity": "sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-2.0.0.tgz", + "integrity": "sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==", "dev": true }, "extract-zip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.0.tgz", - "integrity": "sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "requires": { "@types/yauzl": "^2.9.1", @@ -12163,23 +12520,23 @@ "integrity": "sha512-H1k/ESTD2rJ3liupyqWBPjZC+LKfCGixQzz/NDN4dkgbmG1bVFyMOh7luKSkVDoyfhgvRm62pviNMPI+eJTZcQ==" }, "firebase": { - "version": "7.15.5", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-7.15.5.tgz", - "integrity": "sha512-yeXo3KDp/ZWO0/Uyen99cUvGM76femebmyNOBTHcGSDkBXvIGth6235KhclxLROIKCC5b3YNwmKX11tbaC6RJg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-7.17.0.tgz", + "integrity": "sha512-+y7c1pCj8xp98CIDhVjg0rKhGtsFskGB8hyhjsyp549Upwa0cropdK5emCFTmMIbvDjZmP8rTuuDXPBeREAaCg==", "requires": { - "@firebase/analytics": "0.3.8", - "@firebase/app": "0.6.7", + "@firebase/analytics": "0.4.0", + "@firebase/app": "0.6.8", "@firebase/app-types": "0.6.1", - "@firebase/auth": "0.14.7", - "@firebase/database": "0.6.6", - "@firebase/firestore": "1.15.5", - "@firebase/functions": "0.4.47", - "@firebase/installations": "0.4.13", - "@firebase/messaging": "0.6.19", - "@firebase/performance": "0.3.8", + "@firebase/auth": "0.14.9", + "@firebase/database": "0.6.8", + "@firebase/firestore": "1.16.1", + "@firebase/functions": "0.4.48", + "@firebase/installations": "0.4.14", + "@firebase/messaging": "0.6.20", + "@firebase/performance": "0.3.9", "@firebase/polyfill": "0.3.36", - "@firebase/remote-config": "0.1.24", - "@firebase/storage": "0.3.37", + "@firebase/remote-config": "0.1.25", + "@firebase/storage": "0.3.40", "@firebase/util": "0.2.50" } }, @@ -12417,9 +12774,9 @@ } }, "formik": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.4.tgz", - "integrity": "sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.5.tgz", + "integrity": "sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==", "requires": { "deepmerge": "^2.1.1", "hoist-non-react-statics": "^3.3.0", @@ -12465,14 +12822,33 @@ "dev": true }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "dependencies": { + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + } } }, "fs-minipass": { @@ -12902,9 +13278,9 @@ } }, "graphql-tag": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.3.tgz", - "integrity": "sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==" + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz", + "integrity": "sha512-O7vG5BT3w6Sotc26ybcvLKNTdfr4GfsIVMD+LdYqXCeJIYPRyp8BIsDOUtxw7S1PYvRw5vH3278J2EDezR6mfA==" }, "growl": { "version": "1.10.5", @@ -13087,13 +13463,13 @@ "optional": true }, "header-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-1.0.1.tgz", - "integrity": "sha1-lTWXMZfBRLCWE81l0xfvGZY70C0=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz", + "integrity": "sha512-LChe/V32mnUQnTwTxd3aAlNMk8ia9tjCDb/LjYtoMrdAPApxLB+azejUk5ERZIZdIqvinwv6BAUuFXH/tQPdZA==", "dev": true, "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.3" + "capital-case": "^1.0.3", + "tslib": "^1.10.0" } }, "helmet": { @@ -13549,9 +13925,9 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "ignore": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.6.tgz", - "integrity": "sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "imagemin": { @@ -14157,8 +14533,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", - "dev": true, - "optional": true + "dev": true }, "is-extendable": { "version": "0.1.1", @@ -14220,15 +14595,6 @@ "integrity": "sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=", "dev": true }, - "is-lower-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", - "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", - "dev": true, - "requires": { - "lower-case": "^1.1.0" - } - }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -14383,15 +14749,6 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, - "is-upper-case": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", - "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", - "dev": true, - "requires": { - "upper-case": "^1.1.0" - } - }, "is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -17231,9 +17588,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash-es": { "version": "4.17.15", @@ -17611,15 +17968,6 @@ "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", "dev": true }, - "lower-case-first": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-1.0.2.tgz", - "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", - "dev": true, - "requires": { - "lower-case": "^1.1.2" - } - }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -18434,9 +18782,9 @@ } }, "moment": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz", - "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==", "dev": true }, "move-concurrently": { @@ -18739,9 +19087,9 @@ "dev": true }, "nightwatch": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-1.3.6.tgz", - "integrity": "sha512-61kz2mw3Ng8Rrs2CDZv6aVB+bW+oNIFXL543L9kOvOqft3zVh2j08W8ww6BxGDzmWZe1HGRXNEI5U8+I4hO4KA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-1.3.7.tgz", + "integrity": "sha512-Cy9MJsBVNs+duREiyISKpCmR20F3VKFaus1rBUpBO+fHTh2RULW41wJlTdrXYnjUThn9DeurI/rjo1lMrYI/nQ==", "dev": true, "requires": { "assertion-error": "^1.1.0", @@ -18765,87 +19113,11 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, "dotenv": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==", "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } } } }, @@ -19206,9 +19478,9 @@ "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" }, "object-treeify": { - "version": "1.1.25", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.25.tgz", - "integrity": "sha512-6Abx0xlXDnYd50JkQefvoIly3jWOu8/PqH4lh8p2/aMFEx5TjsUGHt0H9NHfzt+pCwOhpPgNYofD8e2YywIXig==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.26.tgz", + "integrity": "sha512-0WTfU7SGM8umY4YPpOg+oHXL66E6dPVCr+sMR6KitPmvg8CkVrHUUZYEFtx0+5Wb0HjFEsBwBYXyGRNeX7c/oQ==", "dev": true }, "object-visit": { @@ -19329,9 +19601,9 @@ } }, "ora": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.4.tgz", - "integrity": "sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.5.tgz", + "integrity": "sha512-jCDgm9DqvRcNIAEv2wZPrh7E5PcQiDUnbnWbAfu4NGAE2ZNqPFbDixmWldy1YG2QfLeQhuiu6/h5VRrk6cG50w==", "dev": true, "requires": { "chalk": "^3.0.0", @@ -19680,12 +19952,13 @@ } }, "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", + "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", "dev": true, "requires": { - "no-case": "^2.2.0" + "dot-case": "^3.0.3", + "tslib": "^1.10.0" } }, "parchment": { @@ -19786,13 +20059,34 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "pascal-case": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-2.0.1.tgz", - "integrity": "sha1-LVeNNFX2YNpl7KGO+VtODekSdh4=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", + "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", "dev": true, "requires": { - "camel-case": "^3.0.0", - "upper-case-first": "^1.1.0" + "no-case": "^3.0.3", + "tslib": "^1.10.0" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + } } }, "pascalcase": { @@ -19824,12 +20118,13 @@ "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" }, "path-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-2.1.1.tgz", - "integrity": "sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.3.tgz", + "integrity": "sha512-UMFU6UETFpCNWbIWNczshPrnK/7JAXBP2NYw80ojElbQ2+JYxdqWDBkvvqM93u4u6oLmuJ/tPOf2tM8KtXv4eg==", "dev": true, "requires": { - "no-case": "^2.2.0" + "dot-case": "^3.0.3", + "tslib": "^1.10.0" } }, "path-dirname": { @@ -21538,9 +21833,9 @@ "dev": true }, "protobufjs": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz", - "integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", + "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -21558,9 +21853,9 @@ }, "dependencies": { "@types/node": { - "version": "13.13.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz", - "integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==" + "version": "13.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.14.tgz", + "integrity": "sha512-Az3QsOt1U/K1pbCQ0TXGELTuTkPLOiFIQf3ILzbOyo0FqgV9SxRnxbxM5QlAveERZMHpZY+7u3Jz2tKyl+yg6g==" } } }, @@ -21916,9 +22211,9 @@ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, "react-focus-lock": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.4.0.tgz", - "integrity": "sha512-mue/boxdfNhfxnQcZtEBvqwZ5XQxk0uRoAMwLGl8j6XolFV3UIlt6iGFBGqRdJsvVHhtyKC5i8fkLnBidxCTbA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.4.1.tgz", + "integrity": "sha512-c5ZP56KSpj9EAxzScTqQO7bQQNPltf/W1ZEBDqNDOV1XOIwvAyHX0O7db9ekiAtxyKgnqZjQlLppVg94fUeL9w==", "requires": { "@babel/runtime": "^7.0.0", "focus-lock": "^0.7.0", @@ -22476,17 +22771,26 @@ } }, "request-promise": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz", - "integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", "dev": true, "requires": { "bluebird": "^3.5.0", - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, "dependencies": { + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -23021,13 +23325,35 @@ } }, "sentence-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-2.1.1.tgz", - "integrity": "sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.3.tgz", + "integrity": "sha512-ZPr4dgTcNkEfcGOMFQyDdJrTU9uQO1nb1cjf+nuzb6FxgMDgKddZOM29qEsB7jvsZSMruLRcL2KfM4ypKpa0LA==", "dev": true, "requires": { - "no-case": "^2.2.0", - "upper-case-first": "^1.1.2" + "no-case": "^3.0.3", + "tslib": "^1.10.0", + "upper-case-first": "^2.0.1" + }, + "dependencies": { + "lower-case": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", + "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "no-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", + "dev": true, + "requires": { + "lower-case": "^2.0.1", + "tslib": "^1.10.0" + } + } } }, "serialize-error": { @@ -23201,12 +23527,13 @@ "dev": true }, "snake-case": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", - "integrity": "sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.3.tgz", + "integrity": "sha512-WM1sIXEO+rsAHBKjGf/6R1HBBcgbncKS08d2Aqec/mrDSpU80SiOU41hO7ny6DToHSyrlwTYzQBIK1FPSx4Y3Q==", "dev": true, "requires": { - "no-case": "^2.2.0" + "dot-case": "^3.0.3", + "tslib": "^1.10.0" } }, "snapdragon": { @@ -24608,9 +24935,9 @@ "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" }, "subscriptions-transport-ws": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz", - "integrity": "sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz", + "integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==", "requires": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -24710,16 +25037,6 @@ "util.promisify": "~1.0.0" } }, - "swap-case": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", - "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", - "dev": true, - "requires": { - "lower-case": "^1.1.1", - "upper-case": "^1.1.1" - } - }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -25563,9 +25880,9 @@ } }, "typescript": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", - "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "ua-parser-js": { @@ -25832,12 +26149,12 @@ "dev": true }, "upper-case-first": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", - "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.1.tgz", + "integrity": "sha512-105J8XqQ+9RxW3l9gHZtgve5oaiR9TIwvmZAMAIZWRHe00T21cdvewKORTlOJf/zXW6VukuTshM+HXZNWz7N5w==", "dev": true, "requires": { - "upper-case": "^1.1.1" + "tslib": "^1.10.0" } }, "uri-js": { @@ -26849,29 +27166,62 @@ } }, "wrap-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz", - "integrity": "sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } } } diff --git a/package.json b/package.json index e133bc4685..5bfbd559f5 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "matters-web", - "version": "3.10.2", + "version": "3.11.0", "description": "codebase of Matters' website", "sideEffects": false, - "author": "", + "author": "Matters ", "engines": { "node": ">=12.16", "npm": ">=6.14" }, - "license": "ISC", + "license": "Apache-2.0", "scripts": { "dev": "PORT=\"${PORT:-3000}\"; next -p $PORT", "start": "PORT=\"${PORT:-3000}\"; next start -p $PORT", @@ -41,7 +41,7 @@ "@reach/alert": "^0.10.5", "@reach/dialog": "^0.10.5", "@reach/visually-hidden": "^0.10.4", - "@sentry/browser": "^5.19.1", + "@sentry/browser": "^5.20.0", "@stripe/react-stripe-js": "^1.1.2", "@stripe/stripe-js": "^1.8.0", "@tippyjs/react": "^4.0.4", @@ -56,17 +56,17 @@ "apollo-utilities": "^1.3.4", "autosize": "^4.0.2", "classnames": "^2.2.6", - "date-fns": "^2.14.0", + "date-fns": "^2.15.0", "express": "^4.17.1", "fingerprintjs2": "^2.1.0", - "firebase": "^7.15.5", - "formik": "^2.1.4", + "firebase": "^7.17.0", + "formik": "^2.1.5", "graphql": "^14.7.0", - "graphql-tag": "^2.10.3", + "graphql-tag": "^2.10.4", "helmet": "^3.23.3", "isomorphic-unfetch": "^3.0.0", "jump.js": "^1.0.2", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "next": "^9.4.4", "next-with-apollo": "^5.1.0", "nprogress": "^0.2.0", @@ -78,32 +78,32 @@ "react-beautiful-dnd": "^13.0.0", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.13.1", - "react-focus-lock": "^2.4.0", + "react-focus-lock": "^2.4.1", "react-remove-scroll": "^2.3.0", "react-spring": "^9.0.0-rc.3", "react-use-gesture": "^7.0.15", "react-virtualized": "^9.21.2", "react-waypoint": "^9.0.3", - "subscriptions-transport-ws": "^0.9.16", + "subscriptions-transport-ws": "^0.9.17", "use-debounce": "^3.4.3", "validator": "^13.1.1" }, "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.10.4", "@svgr/webpack": "^5.4.0", - "@testing-library/react": "^10.4.4", + "@testing-library/react": "^10.4.7", "@types/autosize": "^3.0.7", "@types/classnames": "^2.2.10", - "@types/express": "^4.17.4", + "@types/express": "^4.17.7", "@types/fingerprintjs2": "^2.0.0", "@types/grecaptcha": "^3.0.1", "@types/helmet": "0.0.47", - "@types/jest": "^26.0.3", + "@types/jest": "^26.0.5", "@types/jump.js": "^1.0.3", - "@types/lodash": "^4.14.157", + "@types/lodash": "^4.14.158", "@types/nprogress": "0.2.0", "@types/pulltorefreshjs": "^0.1.3", - "@types/react": "^16.9.41", + "@types/react": "^16.9.43", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-dom": "^16.9.8", @@ -113,11 +113,11 @@ "@types/styled-jsx": "^2.2.8", "@types/validator": "^13.1.0", "@zeit/next-bundle-analyzer": "^0.1.2", - "apollo": "^2.28.3", + "apollo": "^2.30.1", "babel-jest": "^26.1.0", "babel-plugin-dynamic-import-node": "^2.3.3", "babel-polyfill": "^6.26.0", - "chromedriver": "^83.0.0", + "chromedriver": "^84.0.1", "cucumber": "^6.0.5", "cucumber-pretty": "^6.0.0", "cz-conventional-changelog": "^3.2.0", @@ -132,7 +132,7 @@ "next-compose-plugins": "^2.2.0", "next-offline": "^5.0.0-beta.10", "next-optimized-images": "^2.6.1", - "nightwatch": "^1.3.6", + "nightwatch": "^1.3.7", "nightwatch-api": "^3.0.1", "postcss-calc": "^7.0.2", "postcss-color-function": "^4.1.0", @@ -153,7 +153,7 @@ "tslint-config-prettier": "^1.18.0", "tslint-react": "^4.2.0", "tslint-react-hooks": "^2.2.2", - "typescript": "^3.9.6" + "typescript": "^3.9.7" }, "prettier": { "singleQuote": true, diff --git a/src/common/enums/errorCode.ts b/src/common/enums/errorCode.ts index 2d87f87d42..44f02cd55b 100644 --- a/src/common/enums/errorCode.ts +++ b/src/common/enums/errorCode.ts @@ -43,6 +43,7 @@ export const ERROR_CODES = { // TAG DUPLICATE_TAG: 'DUPLICATE_TAG', + NOT_ALLOW_ADD_TAG: 'NOT_ALLOW_ADD_TAG', // Verification Code CODE_INVALID: 'CODE_INVALID', diff --git a/src/common/enums/route.ts b/src/common/enums/route.ts index 08dd2c20ea..c7f2258c3a 100644 --- a/src/common/enums/route.ts +++ b/src/common/enums/route.ts @@ -12,8 +12,6 @@ type ROUTE_KEY = | 'HOME' | 'FOLLOW' | 'AUTHORS' - | 'TOPICS' - | 'ICYMI' | 'SEARCH' | 'TAGS' | 'TAG_DETAIL' @@ -58,12 +56,14 @@ export const ROUTES: Array<{ pathname: string handler?: (req: Request, res: Response, next: NextFunction) => any }> = [ + /** + * Public + */ { key: 'HOME', pathname: '/' }, { key: 'FOLLOW', pathname: '/follow' }, { key: 'AUTHORS', pathname: '/authors' }, - { key: 'TOPICS', pathname: '/topics' }, - { key: 'ICYMI', pathname: '/icymi' }, { key: 'SEARCH', pathname: '/search' }, + // experient page for recommendation engine testing { key: 'RECOMMENDATION', pathname: '/recommendation' }, @@ -80,6 +80,22 @@ export const ROUTES: Array<{ // Article { key: 'ARTICLE_DETAIL', pathname: '/[userName]/[mediaHash]' }, + // Auth + { key: 'LOGIN', pathname: '/login' }, + { key: 'SIGNUP', pathname: '/signup' }, + { key: 'FORGET', pathname: '/forget' }, + + // Misc + { key: 'HELP', pathname: '/help' }, + { key: 'MIGRATION', pathname: '/migration' }, + { key: 'ABOUT', pathname: '/about' }, + { key: 'GUIDE', pathname: '/guide' }, + { key: 'COMMUNITY', pathname: '/community' }, + { key: 'TOS', pathname: '/tos' }, + + /** + * Protected + */ // Me { key: 'ME_DRAFTS', pathname: '/me/drafts' }, { key: 'ME_BOOKMARKS', pathname: '/me/bookmarks' }, @@ -107,11 +123,6 @@ export const ROUTES: Array<{ // Draft { key: 'ME_DRAFT_DETAIL', pathname: '/me/drafts/[draftId]' }, - // Auth - { key: 'LOGIN', pathname: '/login' }, - { key: 'SIGNUP', pathname: '/signup' }, - { key: 'FORGET', pathname: '/forget' }, - // OAuth { key: 'OAUTH_AUTHORIZE', pathname: '/oauth/authorize' }, { key: 'OAUTH_CALLBACK_SUCCESS', pathname: '/oauth/[provider]/success' }, @@ -120,14 +131,6 @@ export const ROUTES: Array<{ // Pay { key: 'PAY_CALLBACK_SUCCESS', pathname: '/pay/[provider]/success' }, { key: 'PAY_CALLBACK_FAILURE', pathname: '/pay/[provider]/failure' }, - - // Misc - { key: 'HELP', pathname: '/help' }, - { key: 'MIGRATION', pathname: '/migration' }, - { key: 'ABOUT', pathname: '/about' }, - { key: 'GUIDE', pathname: '/guide' }, - { key: 'COMMUNITY', pathname: '/community' }, - { key: 'TOS', pathname: '/tos' }, ] export const UrlFragments = { diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index 105c3a701b..f95b1cd393 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -71,6 +71,7 @@ export const TEXT = { draft: '草稿', DUPLICATE_TAG: '標籤名稱已被使用', edit: '編輯', + editArticle: '編輯作品', editComment: '編輯評論', editTag: '編輯標籤', editUserProfile: '編輯資料', @@ -152,6 +153,7 @@ export const TEXT = { NETWORK_ERROR: '網路錯誤,請用力刷新', newPassword: '新密碼', nextStep: '下一步', + NOT_ALLOW_ADD_TAG: '無法添加', NOT_ENOUGH_MAT: '沒有足夠的 MAT 以讚賞', NOTICE_NOT_FOUND: '通知不存在', notification: '通知', @@ -234,6 +236,8 @@ export const TEXT = { tagDescriptionPlaceholder: '輸入一段標籤描述…', tagEdited: '標籤已更新', tagName: '標籤名稱', + tagAddArticle: '添加我的作品', + tagAddSelectedArticle: '添加精選內容', term: '用戶協議', termAndPrivacy: '用戶協議與隱私政策', termHint: '我們的用戶協議和隱私政策發生了更改,請閱讀並同意後繼續使用。', @@ -342,6 +346,7 @@ export const TEXT = { draft: '草稿', DUPLICATE_TAG: '标签名称已被使用', edit: '编辑', + editArticle: '編輯作品', editComment: '编辑评论', editTag: '编辑标签', editUserProfile: '编辑资料', @@ -423,6 +428,7 @@ export const TEXT = { NETWORK_ERROR: '网络不给力,请用力刷新', newPassword: '新密码', nextStep: '下一步', + NOT_ALLOW_ADD_TAG: '无法添加', NOT_ENOUGH_MAT: '没有足够的 MAT 以赞赏', NOTICE_NOT_FOUND: '通知不存在', notification: '通知', @@ -506,6 +512,8 @@ export const TEXT = { tagDescriptionPlaceholder: '输入一段话题描述…', tagEdited: '标签已更新', tagName: '标签名称', + tagAddArticle: '添加我的作品', + tagAddSelectedArticle: '添加精选內容', term: '用户协议', termAndPrivacy: '用户协议与隐私政策', termHint: '我们的用户协议和隐私政策发生了更改,请阅读并同意后继续使用。', diff --git a/src/common/styles/layouts/grids.css b/src/common/styles/layouts/grids.css index d9ca380fa9..d566f32d38 100644 --- a/src/common/styles/layouts/grids.css +++ b/src/common/styles/layouts/grids.css @@ -8,9 +8,9 @@ * Row */ .l-row { + lost-center: 100%; margin-right: 1rem; margin-left: 1rem; - lost-center: 100%; @media (--sm-up) { & { diff --git a/src/common/utils/analytics.ts b/src/common/utils/analytics.ts index 3c4061c03a..c58e7eec84 100644 --- a/src/common/utils/analytics.ts +++ b/src/common/utils/analytics.ts @@ -25,6 +25,8 @@ type EventArgs = | ['load_more', LoadMoreProp] | ['share', ShareProp] | ['purchase', PurchaseProp] + | ['view_add_credit_dialog', ViewDialogProp] + | ['view_donation_dialog', ViewDialogProp] type ClickFeedProp = | ArticleFeedProp @@ -70,6 +72,10 @@ interface PurchaseProp { message?: string } +interface ViewDialogProp { + step: string +} + interface ArticleFeedProp { type: ArticleFeedType diff --git a/src/common/utils/types/index.ts b/src/common/utils/types/index.ts index 60d9260b86..99d7ec3ca2 100644 --- a/src/common/utils/types/index.ts +++ b/src/common/utils/types/index.ts @@ -54,6 +54,7 @@ export default gql` enum FollowFeedType { article comment + tag } enum ViewMode { diff --git a/src/common/utils/withApollo.ts b/src/common/utils/withApollo.ts index 9490b725eb..44e42c6f05 100644 --- a/src/common/utils/withApollo.ts +++ b/src/common/utils/withApollo.ts @@ -43,7 +43,6 @@ const persistedQueryLink = createPersistedQueryLink({ const httpLink = ({ headers }: { [key: string]: any }) => createUploadLink({ uri: process.env.NEXT_PUBLIC_API_URL, - credentials: 'include', headers, fetchOptions: { agent, @@ -67,8 +66,17 @@ const errorLink = onError(({ graphQLErrors, networkError }) => { } }) -const authLink = setContext((_, { headers }) => { +const authLink = setContext((operation, { headers }) => { + const operationName = operation.operationName || '' + + if (process.env.NODE_ENV !== 'production') { + console.log(`\x1b[32m[GraphQL operation]\x1b[0m`, operationName) + } + + const isPublicOperation = /Public$/.test(operationName) + return { + credentials: isPublicOperation ? 'omit' : 'include', headers: { ...headers, 'x-client-name': 'web', @@ -122,9 +130,9 @@ export default withApollo(({ ctx, headers, initialState }) => { link: ApolloLink.from([ persistedQueryLink, errorLink, - authLink, sentryLink, agentHashLink, + authLink, httpLink({ headers }), ]), cache, diff --git a/src/components/ArticleDigest/DropdownActions/ArchiveArticle/Dialog.tsx b/src/components/ArticleDigest/DropdownActions/ArchiveArticle/Dialog.tsx index df64b16e7e..27c51a9ef9 100644 --- a/src/components/ArticleDigest/DropdownActions/ArchiveArticle/Dialog.tsx +++ b/src/components/ArticleDigest/DropdownActions/ArchiveArticle/Dialog.tsx @@ -12,7 +12,7 @@ import { ArchiveArticleArticle } from './__generated__/ArchiveArticleArticle' const ARCHIVE_ARTICLE = gql` mutation ArchiveArticle($id: ID!) { - archiveArticle(input: { id: $id }) { + editArticle(input: { id: $id, state: archived }) { id articleState: state sticky @@ -36,7 +36,7 @@ const ArchiveArticleDialog = ({ const [archiveArticle] = useMutation(ARCHIVE_ARTICLE, { variables: { id: article.id }, optimisticResponse: { - archiveArticle: { + editArticle: { id: article.id, articleState: 'archived' as any, sticky: false, diff --git a/src/components/ArticleDigest/DropdownActions/EditButton.tsx b/src/components/ArticleDigest/DropdownActions/EditButton.tsx new file mode 100644 index 0000000000..37c3096fc6 --- /dev/null +++ b/src/components/ArticleDigest/DropdownActions/EditButton.tsx @@ -0,0 +1,13 @@ +import { IconEdit, Menu, TextIcon, Translate } from '~/components' + +const EditArticleButton = ({ editArticle }: { editArticle: () => void }) => { + return ( + + } size="md" spacing="base"> + + + + ) +} + +export default EditArticleButton diff --git a/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx b/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx index cb5d431420..6ca8260962 100644 --- a/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/RemoveTagButton.tsx @@ -4,8 +4,10 @@ import { useRouter } from 'next/router' import { IconRemoveMedium, Menu, TextIcon, Translate } from '~/components' import { useMutation } from '~/components/GQL' +import updateTagArticlesCount from '~/components/GQL/updates/tagArticlesCount' import { REFETCH_TAG_DETAIL_ARTICLES } from '~/common/enums' +import { getQuery } from '~/common/utils' import { DeleteArticlesTags } from './__generated__/DeleteArticlesTags' import { RemoveTagButtonArticle } from './__generated__/RemoveTagButtonArticle' @@ -40,14 +42,16 @@ const fragments = { const RemoveTagButton = ({ article }: { article: RemoveTagButtonArticle }) => { const router = useRouter() - const { - query: { id }, - } = router - const tagId = _isArray(id) ? id[0] : id + const id = getQuery({ router, key: 'tagId' }) const [deleteArticlesTags] = useMutation( DELETE_ARTICLES_TAGS, - { variables: { id: tagId, articles: [article.id] } } + { + variables: { id, articles: [article.id] }, + update: (cache) => { + updateTagArticlesCount({ cache, type: 'decrement', id }) + }, + } ) return ( @@ -65,7 +69,7 @@ const RemoveTagButton = ({ article }: { article: RemoveTagButtonArticle }) => { }} > } size="md" spacing="base"> - + ) diff --git a/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx b/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx index 2f4efc57a2..a3459c74d3 100644 --- a/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/SetTagSelectedButton.tsx @@ -1,7 +1,7 @@ import gql from 'graphql-tag' import { useRouter } from 'next/router' -import { IconPinMedium, Menu, TextIcon, Translate } from '~/components' +import { IconAddMedium, Menu, TextIcon, Translate } from '~/components' import { useMutation } from '~/components/GQL' import { ADD_TOAST } from '~/common/enums' @@ -12,7 +12,9 @@ import { SetTagSelectedButtonArticle } from './__generated__/SetTagSelectedButto const SET_TAG_SELECTED = gql` mutation SetTagSelected($id: ID!, $articles: [ID!]) { - putArticlesTags(input: { id: $id, articles: $articles, selected: true }) { + updateArticlesTags( + input: { id: $id, articles: $articles, isSelected: true } + ) { id articles(input: { first: 0, selected: true }) { totalCount @@ -70,8 +72,8 @@ const SetTagSelectedButton = ({ ) }} > - } size="md" spacing="base"> - + } size="md" spacing="base"> + ) diff --git a/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx b/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx index 2f92fd1c28..2d1a5e5766 100644 --- a/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/SetTagUnselectedButton.tsx @@ -10,15 +10,17 @@ import { ADD_TOAST } from '~/common/enums' import { getQuery } from '~/common/utils' import { - TagArticles, - TagArticles_node_Tag, -} from '~/components/GQL/queries/__generated__/TagArticles' + TagArticlesPublic, + TagArticlesPublic_node_Tag, +} from '~/components/GQL/queries/__generated__/TagArticlesPublic' import { SetTagUnselected } from './__generated__/SetTagUnselected' import { SetTagUnselectedButtonArticle } from './__generated__/SetTagUnselectedButtonArticle' const SET_TAG_UNSELECTED = gql` mutation SetTagUnselected($id: ID!, $articles: [ID!]) { - putArticlesTags(input: { id: $id, articles: $articles, selected: false }) { + updateArticlesTags( + input: { id: $id, articles: $articles, isSelected: false } + ) { id articles(input: { first: 0, selected: true }) { totalCount @@ -55,10 +57,12 @@ const SetTagUnselectedButton = ({ variables: { id: tagId, articles: [article.id] }, update: (cache) => { try { - const query = require('~/components/GQL/queries/tagArticles').default + const { + TAG_ARTICLES_PUBLIC: query, + } = require('~/components/GQL/queries/tagArticles').default const variables = { id: tagId, selected: true } - const data = cache.readQuery({ query, variables }) - const node = _get(data, 'node', {}) as TagArticles_node_Tag + const data = cache.readQuery({ query, variables }) + const node = _get(data, 'node', {}) as TagArticlesPublic_node_Tag if ( !node.articles || !node.articles.edges || diff --git a/src/components/ArticleDigest/DropdownActions/StickyButton.tsx b/src/components/ArticleDigest/DropdownActions/StickyButton.tsx index 261f7df137..456390345f 100644 --- a/src/components/ArticleDigest/DropdownActions/StickyButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/StickyButton.tsx @@ -11,11 +11,11 @@ import { useMutation } from '~/components/GQL' import updateUserArticles from '~/components/GQL/updates/userArticles' import { StickyButtonArticle } from './__generated__/StickyButtonArticle' -import { UpdateArticleInfo } from './__generated__/UpdateArticleInfo' +import { ToggleSticky } from './__generated__/ToggleSticky' -const UPDATE_ARTICLE_INFO = gql` - mutation UpdateArticleInfo($id: ID!, $sticky: Boolean!) { - updateArticleInfo(input: { id: $id, sticky: $sticky }) { +const TOGGLE_STICKY = gql` + mutation ToggleSticky($id: ID!, $sticky: Boolean!) { + editArticle(input: { id: $id, sticky: $sticky }) { id sticky } @@ -36,10 +36,10 @@ const fragments = { } const StickyButton = ({ article }: { article: StickyButtonArticle }) => { - const [update] = useMutation(UPDATE_ARTICLE_INFO, { + const [toggleSticky] = useMutation(TOGGLE_STICKY, { variables: { id: article.id, sticky: !article.sticky }, optimisticResponse: { - updateArticleInfo: { + editArticle: { id: article.id, sticky: !article.sticky, __typename: 'Article', @@ -56,11 +56,7 @@ const StickyButton = ({ article }: { article: StickyButtonArticle }) => { }) return ( - { - update() - }} - > + {article.sticky ? ( } size="md" spacing="base"> diff --git a/src/components/ArticleDigest/DropdownActions/index.tsx b/src/components/ArticleDigest/DropdownActions/index.tsx index bdcd1bdba5..c3cd9ed49f 100644 --- a/src/components/ArticleDigest/DropdownActions/index.tsx +++ b/src/components/ArticleDigest/DropdownActions/index.tsx @@ -23,6 +23,7 @@ import { getQuery } from '~/common/utils' import AppreciatorsButton from './AppreciatorsButton' import ArchiveArticle from './ArchiveArticle' +import EditButton from './EditButton' import ExtendButton from './ExtendButton' import FingerprintButton from './FingerprintButton' import RemoveTagButton from './RemoveTagButton' @@ -39,6 +40,7 @@ export interface DropdownActionsControls { inUserArticles?: boolean inTagDetailLatest?: boolean inTagDetailSelected?: boolean + editArticle?: () => any } type DropdownActionsProps = { @@ -54,6 +56,7 @@ interface Controls { hasSetTagSelected: boolean hasSetTagUnSelected: boolean hasRemoveTag: boolean + hasEdit: boolean } interface DialogProps { @@ -93,6 +96,7 @@ const BaseDropdownActions = ({ color = 'grey', size, inCard, + editArticle, hasAppreciators, hasFingerprint, @@ -102,6 +106,7 @@ const BaseDropdownActions = ({ hasSetTagSelected, hasSetTagUnSelected, hasRemoveTag, + hasEdit, openFingerprintDialog, openAppreciatorsDialog, @@ -129,6 +134,7 @@ const BaseDropdownActions = ({ {hasSetTagSelected && } {hasSetTagUnSelected && } {hasRemoveTag && } + {hasEdit && editArticle && } ) @@ -165,6 +171,7 @@ const DropdownActions = (props: DropdownActionsProps) => { inUserArticles, inTagDetailLatest, inTagDetailSelected, + editArticle, } = props const router = useRouter() const viewer = useContext(ViewerContext) @@ -201,6 +208,7 @@ const DropdownActions = (props: DropdownActionsProps) => { hasSetTagSelected: !!(inTagDetailLatest && canEditTag), hasSetTagUnSelected: !!(inTagDetailSelected && canEditTag), hasRemoveTag: !!(isInTagDetail && canEditTag), + hasEdit: isActive && !!editArticle, } if (_isEmpty(_pickBy(controls))) { diff --git a/src/components/ArticleDigest/Feed/index.tsx b/src/components/ArticleDigest/Feed/index.tsx index cc58c8d1be..a2a888e63e 100644 --- a/src/components/ArticleDigest/Feed/index.tsx +++ b/src/components/ArticleDigest/Feed/index.tsx @@ -17,7 +17,7 @@ import InactiveState from './InactiveState' import styles from './styles.css' import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' -import { ArticleDigestFeedArticle } from './__generated__/ArticleDigestFeedArticle' +import { ArticleDigestFeedArticlePublic } from './__generated__/ArticleDigestFeedArticlePublic' export type ArticleDigestFeedControls = { onClick?: () => any @@ -26,37 +26,48 @@ export type ArticleDigestFeedControls = { } & FooterActionsControls type ArticleDigestFeedProps = { - article: ArticleDigestFeedArticle + article: ArticleDigestFeedArticlePublic } & ArticleDigestFeedControls const fragments = { - article: gql` - fragment ArticleDigestFeedArticle on Article { - id - title - slug - mediaHash - articleState: state - cover - summary - author { + article: { + public: gql` + fragment ArticleDigestFeedArticlePublic on Article { id - userName - ...UserDigestMiniUser + title + slug + mediaHash + articleState: state + cover + summary + author { + id + userName + ...UserDigestMiniUser + } + ...CreatedAtArticle + ...InactiveStateArticle + ...ArticleDigestTitleArticle + ...DropdownActionsArticle + ...FooterActionsArticlePublic + ...FooterActionsArticlePrivate } - ...CreatedAtArticle - ...InactiveStateArticle - ...ArticleDigestTitleArticle - ...FooterActionsArticle - ...DropdownActionsArticle - } - ${UserDigest.Mini.fragments.user} - ${CreatedAt.fragments.article} - ${InactiveState.fragments.article} - ${ArticleDigestTitle.fragments.article} - ${FooterActions.fragments.article} - ${DropdownActions.fragments.article} - `, + ${UserDigest.Mini.fragments.user} + ${CreatedAt.fragments.article} + ${InactiveState.fragments.article} + ${ArticleDigestTitle.fragments.article} + ${DropdownActions.fragments.article} + ${FooterActions.fragments.article.public} + ${FooterActions.fragments.article.private} + `, + private: gql` + fragment ArticleDigestFeedArticlePrivate on Article { + id + ...FooterActionsArticlePrivate + } + ${FooterActions.fragments.article.private} + `, + }, } const BaseArticleDigestFeed = ({ diff --git a/src/components/ArticleDigest/FooterActions/index.tsx b/src/components/ArticleDigest/FooterActions/index.tsx index 3667e281f8..ab498a7b4a 100644 --- a/src/components/ArticleDigest/FooterActions/index.tsx +++ b/src/components/ArticleDigest/FooterActions/index.tsx @@ -10,35 +10,41 @@ import Appreciation from './Appreciation' import ResponseCount from './ResponseCount' import styles from './styles.css' -import { FooterActionsArticle } from './__generated__/FooterActionsArticle' +import { FooterActionsArticlePublic } from './__generated__/FooterActionsArticlePublic' export type FooterActionsControls = DropdownActionsControls type FooterActionsProps = { - article: FooterActionsArticle + article: FooterActionsArticlePublic } & FooterActionsControls const fragments = { - article: gql` - fragment FooterActionsArticle on Article { - id - title - slug - mediaHash - author { + article: { + public: gql` + fragment FooterActionsArticlePublic on Article { id - userName + title + slug + mediaHash + author { + id + userName + } + ...AppreciationArticle + ...ActionsResponseCountArticle + ...DropdownActionsArticle } - ...AppreciationArticle - ...ActionsResponseCountArticle - ...BookmarkArticlePrivate - ...DropdownActionsArticle - } - ${Appreciation.fragments.article} - ${ResponseCount.fragments.article} - ${BookmarkButton.fragments.article.private} - ${DropdownActions.fragments.article} - `, + ${Appreciation.fragments.article} + ${ResponseCount.fragments.article} + ${DropdownActions.fragments.article} + `, + private: gql` + fragment FooterActionsArticlePrivate on Article { + ...BookmarkArticlePrivate + } + ${BookmarkButton.fragments.article.private} + `, + }, } const FooterActions = ({ diff --git a/src/components/Avatar/styles.css b/src/components/Avatar/styles.css index cd575d268e..aa91d8dd4c 100644 --- a/src/components/Avatar/styles.css +++ b/src/components/Avatar/styles.css @@ -4,6 +4,7 @@ /* may under a flex layout, keep it unshrinkable */ flex-shrink: 0; + border-radius: 50%; & :global(img) { @mixin object-fit-cover; diff --git a/src/components/BlockUser/Button/index.tsx b/src/components/BlockUser/Button/index.tsx index 0359331b2a..c1b0eaff78 100644 --- a/src/components/BlockUser/Button/index.tsx +++ b/src/components/BlockUser/Button/index.tsx @@ -6,11 +6,11 @@ import { Translate, } from '~/components' import { useMutation } from '~/components/GQL' -import UNBLOCK_USER from '~/components/GQL/mutations/unblockUser' +import TOGGLE_BLOCK_USER from '~/components/GQL/mutations/toggleBlockUser' import { ADD_TOAST } from '~/common/enums' -import { UnblockUser } from '~/components/GQL/mutations/__generated__/UnblockUser' +import { ToggleBlockUser } from '~/components/GQL/mutations/__generated__/ToggleBlockUser' import { BlockUserPrivate } from '../__generated__/BlockUserPrivate' import { BlockUserPublic } from '../__generated__/BlockUserPublic' @@ -21,10 +21,10 @@ const BlockUserButton = ({ user: BlockUserPublic & Partial openDialog: () => void }) => { - const [unblockUser] = useMutation(UNBLOCK_USER, { - variables: { id: user.id }, + const [unblockUser] = useMutation(TOGGLE_BLOCK_USER, { + variables: { id: user.id, enabled: false }, optimisticResponse: { - unblockUser: { + toggleBlockUser: { id: user.id, isBlocked: false, __typename: 'User', diff --git a/src/components/BlockUser/Dialog/index.tsx b/src/components/BlockUser/Dialog/index.tsx index 59606398a0..3a2fc6edb8 100644 --- a/src/components/BlockUser/Dialog/index.tsx +++ b/src/components/BlockUser/Dialog/index.tsx @@ -2,13 +2,13 @@ import { useState } from 'react' import { Dialog, Translate } from '~/components' import { useMutation } from '~/components/GQL' -import BLOCK_USER from '~/components/GQL/mutations/blockUser' +import TOGGLE_BLOCK_USER from '~/components/GQL/mutations/toggleBlockUser' import { ADD_TOAST } from '~/common/enums' import ViewBlocksButton from './ViewBlocksButton' -import { BlockUser as BlockUserMutate } from '~/components/GQL/mutations/__generated__/BlockUser' +import { ToggleBlockUser } from '~/components/GQL/mutations/__generated__/ToggleBlockUser' import { BlockUserPrivate } from '../__generated__/BlockUserPrivate' import { BlockUserPublic } from '../__generated__/BlockUserPublic' @@ -22,10 +22,10 @@ const BlockUserDialog = ({ user, children }: BlockUserDialogProps) => { const open = () => setShowDialog(true) const close = () => setShowDialog(false) - const [blockUser] = useMutation(BLOCK_USER, { - variables: { id: user.id }, + const [blockUser] = useMutation(TOGGLE_BLOCK_USER, { + variables: { id: user.id, enabled: true }, optimisticResponse: { - blockUser: { + toggleBlockUser: { id: user.id, isBlocked: true, __typename: 'User', diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 1c1e59fd57..73e3f85873 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -54,7 +54,10 @@ type ButtonColor = | 'gold' | 'red' -type ButtonTextColor = Extract +type ButtonTextColor = Extract< + ButtonColor, + 'white' | 'black' | 'green' | 'gold' | 'red' +> export type ButtonBgColor = Extract< ButtonColor, @@ -70,7 +73,12 @@ export type ButtonBgColor = Extract< type ButtonBgActiveColor = Extract< ButtonColor, - 'grey-lighter' | 'green-lighter' | 'grey-lighter-active' | 'green' | 'red' + | 'grey-lighter' + | 'green-lighter' + | 'grey-lighter-active' + | 'green' + | 'gold' + | 'red' > export interface ButtonProps { diff --git a/src/components/Button/styles.css b/src/components/Button/styles.css index 72d38d7d41..7b64d85060 100644 --- a/src/components/Button/styles.css +++ b/src/components/Button/styles.css @@ -21,6 +21,10 @@ span.container { color: var(--color-matters-green); } +.text-gold { + color: var(--color-matters-gold); +} + .text-red { color: var(--color-red); } @@ -317,6 +321,16 @@ span.container { } } +.bg-active-gold { + &:hover, + &:focus { + & .hotarea { + color: var(--color-white); + background: var(--color-matters-gold); + } + } +} + .bg-active-red { &:hover, &:focus { diff --git a/src/components/Buttons/Bookmark/Subscribe.tsx b/src/components/Buttons/Bookmark/Subscribe.tsx index 7c219260e3..3afb1a21c8 100644 --- a/src/components/Buttons/Bookmark/Subscribe.tsx +++ b/src/components/Buttons/Bookmark/Subscribe.tsx @@ -31,7 +31,7 @@ const Subscribe = ({ articleId, size, disabled, inCard }: SubscribeProps) => { const [subscribe] = useMutation( TOGGLE_SUBSCRIBE_ARTICLE, { - variables: { id: articleId }, + variables: { id: articleId, enabled: true }, optimisticResponse: articleId ? { toggleSubscribeArticle: { diff --git a/src/components/Buttons/Bookmark/Unsubscribe.tsx b/src/components/Buttons/Bookmark/Unsubscribe.tsx index c7b1598d71..b14555992a 100644 --- a/src/components/Buttons/Bookmark/Unsubscribe.tsx +++ b/src/components/Buttons/Bookmark/Unsubscribe.tsx @@ -21,7 +21,7 @@ const Unsubscribe = ({ const [unsubscribe] = useMutation( TOGGLE_SUBSCRIBE_ARTICLE, { - variables: { id: articleId }, + variables: { id: articleId, enabled: false }, optimisticResponse: articleId ? { toggleSubscribeArticle: { diff --git a/src/components/Buttons/Follow/Follow.tsx b/src/components/Buttons/Follow/Follow.tsx index 3d2c92d8a5..3b331c5089 100644 --- a/src/components/Buttons/Follow/Follow.tsx +++ b/src/components/Buttons/Follow/Follow.tsx @@ -1,4 +1,3 @@ -import gql from 'graphql-tag' import _get from 'lodash/get' import _isNil from 'lodash/isNil' @@ -10,32 +9,23 @@ import { Translate, } from '~/components' import { useMutation } from '~/components/GQL' +import TOGGLE_FOLLOW_USER from '~/components/GQL/mutations/toggleFollowUser' import updateUserFollowerCount from '~/components/GQL/updates/userFollowerCount' import updateViewerFolloweeCount from '~/components/GQL/updates/viewerFolloweeCount' import { FollowButtonSize } from './index' +import { ToggleFollowUser } from '~/components/GQL/mutations/__generated__/ToggleFollowUser' import { FollowButtonUserPrivate } from './__generated__/FollowButtonUserPrivate' -import { FollowUser } from './__generated__/FollowUser' interface FollowProps { user: Partial size: FollowButtonSize } -const FOLLOW_USER = gql` - mutation FollowUser($id: ID!) { - toggleFollowUser(input: { id: $id }) { - id - isFollowee - isFollower - } - } -` - const Follow = ({ user, size }: FollowProps) => { - const [follow] = useMutation(FOLLOW_USER, { - variables: { id: user.id }, + const [follow] = useMutation(TOGGLE_FOLLOW_USER, { + variables: { id: user.id, enabled: true }, optimisticResponse: !_isNil(user.id) && !_isNil(user.isFollower) ? { diff --git a/src/components/Buttons/Follow/Unfollow.tsx b/src/components/Buttons/Follow/Unfollow.tsx index 16ef172541..672068c42b 100644 --- a/src/components/Buttons/Follow/Unfollow.tsx +++ b/src/components/Buttons/Follow/Unfollow.tsx @@ -1,4 +1,3 @@ -import gql from 'graphql-tag' import _get from 'lodash/get' import _isNil from 'lodash/isNil' import { useState } from 'react' @@ -11,33 +10,24 @@ import { Translate, } from '~/components' import { useMutation } from '~/components/GQL' +import TOGGLE_FOLLOW_USER from '~/components/GQL/mutations/toggleFollowUser' import updateUserFollowerCount from '~/components/GQL/updates/userFollowerCount' import updateViewerFolloweeCount from '~/components/GQL/updates/viewerFolloweeCount' import { FollowButtonSize } from './index' +import { ToggleFollowUser } from '~/components/GQL/mutations/__generated__/ToggleFollowUser' import { FollowButtonUserPrivate } from './__generated__/FollowButtonUserPrivate' -import { UnfollowUser } from './__generated__/UnfollowUser' interface UnfollowProps { user: Partial size: FollowButtonSize } -const UNFOLLOW_USER = gql` - mutation UnfollowUser($id: ID!) { - toggleFollowUser(input: { id: $id }) { - id - isFollowee - isFollower - } - } -` - const Unfollow = ({ user, size }: UnfollowProps) => { const [hover, setHover] = useState(false) - const [unfollow] = useMutation(UNFOLLOW_USER, { - variables: { id: user.id }, + const [unfollow] = useMutation(TOGGLE_FOLLOW_USER, { + variables: { id: user.id, enabled: false }, optimisticResponse: !_isNil(user.id) && !_isNil(user.isFollower) ? { diff --git a/src/components/Buttons/UnblockUser/index.tsx b/src/components/Buttons/UnblockUser/index.tsx index d04d0406ac..0d599baf48 100644 --- a/src/components/Buttons/UnblockUser/index.tsx +++ b/src/components/Buttons/UnblockUser/index.tsx @@ -3,11 +3,11 @@ import _isNil from 'lodash/isNil' import { Button, TextIcon, Translate } from '~/components' import { useMutation } from '~/components/GQL' -import UNBLOCK_USER from '~/components/GQL/mutations/unblockUser' +import TOGGLE_BLOCK_USER from '~/components/GQL/mutations/toggleBlockUser' import { ADD_TOAST } from '~/common/enums' -import { UnblockUser } from '~/components/GQL/mutations/__generated__/UnblockUser' +import { ToggleBlockUser } from '~/components/GQL/mutations/__generated__/ToggleBlockUser' import { UnblockUserButtonUserPrivate } from './__generated__/UnblockUserButtonUserPrivate' interface UnblockUserButtonProps { @@ -26,11 +26,11 @@ const fragments = { } export const UnblockUserButton = ({ user }: UnblockUserButtonProps) => { - const [unblockUser] = useMutation(UNBLOCK_USER, { - variables: { id: user.id }, + const [unblockUser] = useMutation(TOGGLE_BLOCK_USER, { + variables: { id: user.id, enabled: false }, optimisticResponse: !_isNil(user.id) ? { - unblockUser: { + toggleBlockUser: { id: user.id, isBlocked: false, __typename: 'User', diff --git a/src/components/Buttons/Write/index.tsx b/src/components/Buttons/Write/index.tsx index b1d6275c5a..8034eb3759 100644 --- a/src/components/Buttons/Write/index.tsx +++ b/src/components/Buttons/Write/index.tsx @@ -1,4 +1,3 @@ -import gql from 'graphql-tag' import { useContext } from 'react' import { @@ -11,6 +10,7 @@ import { Translate, } from '~/components' import { useMutation } from '~/components/GQL' +import CREATE_DRAFT from '~/components/GQL/mutations/createDraft' import { ADD_TOAST, TEXT } from '~/common/enums' import { @@ -21,7 +21,7 @@ import { translate, } from '~/common/utils' -import { CreateDraft } from './__generated__/CreateDraft' +import { CreateDraft } from '~/components/GQL/mutations/__generated__/CreateDraft' interface Props { allowed: boolean @@ -29,15 +29,6 @@ interface Props { forbidden?: boolean } -export const CREATE_DRAFT = gql` - mutation CreateDraft($title: String!) { - putDraft(input: { title: $title }) { - id - slug - } - } -` - const BaseWriteButton = ({ onClick, loading, diff --git a/src/components/Comment/DropdownActions/PinButton.tsx b/src/components/Comment/DropdownActions/PinButton.tsx index ddff6efe46..cf62a616db 100644 --- a/src/components/Comment/DropdownActions/PinButton.tsx +++ b/src/components/Comment/DropdownActions/PinButton.tsx @@ -8,36 +8,10 @@ import { Translate, } from '~/components' import { useMutation } from '~/components/GQL' +import TOGGLE_PIN_COMMENT from '~/components/GQL/mutations/togglePinComment' +import { TogglePinComment } from '~/components/GQL/mutations/__generated__/TogglePinComment' import { PinButtonComment } from './__generated__/PinButtonComment' -import { PinComment } from './__generated__/PinComment' -import { UnpinComment } from './__generated__/UnpinComment' - -const PIN_COMMENT = gql` - mutation PinComment($id: ID!) { - pinComment(input: { id: $id }) { - id - pinned - article { - id - pinCommentLeft - } - } - } -` - -const UNPIN_COMMENT = gql` - mutation UnpinComment($id: ID!) { - unpinComment(input: { id: $id }) { - id - pinned - article { - id - pinCommentLeft - } - } - } -` const fragments = { comment: gql` @@ -54,10 +28,10 @@ const fragments = { const PinButton = ({ comment }: { comment: PinButtonComment }) => { const canPin = comment.article.pinCommentLeft > 0 - const [unpinComment] = useMutation(UNPIN_COMMENT, { - variables: { id: comment.id }, + const [unpinComment] = useMutation(TOGGLE_PIN_COMMENT, { + variables: { id: comment.id, enabled: false }, optimisticResponse: { - unpinComment: { + togglePinComment: { id: comment.id, pinned: false, article: { @@ -67,10 +41,10 @@ const PinButton = ({ comment }: { comment: PinButtonComment }) => { }, }, }) - const [pinComment] = useMutation(PIN_COMMENT, { - variables: { id: comment.id }, + const [pinComment] = useMutation(TOGGLE_PIN_COMMENT, { + variables: { id: comment.id, enabled: true }, optimisticResponse: { - pinComment: { + togglePinComment: { id: comment.id, pinned: true, article: { diff --git a/src/components/Comment/Feed/index.tsx b/src/components/Comment/Feed/index.tsx index 2f6494cbf0..75e17dbc98 100644 --- a/src/components/Comment/Feed/index.tsx +++ b/src/components/Comment/Feed/index.tsx @@ -1,4 +1,5 @@ import { useLazyQuery } from '@apollo/react-hooks' +import React from 'react' import { AvatarSize, UserDigest } from '~/components' @@ -23,7 +24,7 @@ export type CommentProps = { comment: FeedCommentPublic & Partial } & CommentControls -export const Feed = ({ +export const BaseCommentFeed = ({ comment, avatarSize = 'lg', hasUserName, @@ -82,6 +83,24 @@ export const Feed = ({ ) } -Feed.fragments = fragments +/** + * Memoizing + */ +type MemoizedCommentFeed = React.MemoExoticComponent> & { + fragments: typeof fragments +} + +const CommentFeed = React.memo( + BaseCommentFeed, + ({ comment: prevComment }, { comment }) => { + return ( + prevComment.content === comment.content && + prevComment.upvotes === comment.upvotes && + prevComment.downvotes === comment.downvotes + ) + } +) as MemoizedCommentFeed + +CommentFeed.fragments = fragments -export default Feed +export default CommentFeed diff --git a/src/components/Context/Viewer/index.tsx b/src/components/Context/Viewer/index.tsx index 06bafbee96..989f194ae9 100644 --- a/src/components/Context/Viewer/index.tsx +++ b/src/components/Context/Viewer/index.tsx @@ -46,9 +46,13 @@ export type Viewer = ViewerUser & { isInactive: boolean isCivicLiker: boolean shouldSetupLikerID: boolean + privateFetched: boolean } -export const processViewer = (viewer: ViewerUser): Viewer => { +export const processViewer = ( + viewer: ViewerUser, + privateFetched: boolean +): Viewer => { const isAuthed = !!viewer.id const state = viewer?.status?.state const isActive = state === 'active' @@ -80,6 +84,7 @@ export const processViewer = (viewer: ViewerUser): Viewer => { isInactive, isCivicLiker, shouldSetupLikerID, + privateFetched, } } @@ -90,12 +95,14 @@ export const ViewerConsumer = ViewerContext.Consumer export const ViewerProvider = ({ children, viewer, + privateFetched, }: { children: React.ReactNode viewer: ViewerUser + privateFetched: boolean }) => { return ( - + {children} ) diff --git a/src/components/Dialogs/AddCreditDialog/index.tsx b/src/components/Dialogs/AddCreditDialog/index.tsx index edb8a17b1b..beb1960bf3 100644 --- a/src/components/Dialogs/AddCreditDialog/index.tsx +++ b/src/components/Dialogs/AddCreditDialog/index.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { Dialog, PaymentForm, ViewerContext } from '~/components' @@ -38,6 +38,7 @@ const BaseAddCreditDialog = ({ children }: AddCreditDialogProps) => { transaction: undefined, client_secret: '', }) + const resetData = () => setData({ transaction: undefined, @@ -50,12 +51,25 @@ const BaseAddCreditDialog = ({ children }: AddCreditDialogProps) => { analytics.trackEvent('click_button', { type: 'checkout' }) } + // set password if needed const isSetPaymentPassword = step === 'setPaymentPassword' + + // confirm add credit amount const isConfirm = step === 'confirm' + + // stripe elements for credit card info const isCheckout = step === 'checkout' + + // loader and error catching const isProcessing = step === 'processing' + + // confirmation const isComplete = step === 'complete' + useEffect(() => { + analytics.trackEvent('view_add_credit_dialog', { step }) + }, [step]) + return ( <> {children({ open })} diff --git a/src/components/Dialogs/DonationDialog/index.tsx b/src/components/Dialogs/DonationDialog/index.tsx index 10a2764fb9..927b76b6b2 100644 --- a/src/components/Dialogs/DonationDialog/index.tsx +++ b/src/components/Dialogs/DonationDialog/index.tsx @@ -1,10 +1,10 @@ import gql from 'graphql-tag' -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { Dialog, PaymentForm, Translate, ViewerContext } from '~/components' import { PAYMENT_CURRENCY as CURRENCY } from '~/common/enums' -import { numRound } from '~/common/utils' +import { analytics, numRound } from '~/common/utils' import { AddCredit_addCredit_transaction as AddCreditTx } from '~/components/Forms/PaymentForm/AddCredit/__generated__/AddCredit' import { PayTo_payTo_transaction as PayToTx } from '~/components/GQL/mutations/__generated__/PayTo' @@ -152,21 +152,44 @@ const BaseDonationDialog = ({ ) + /** + * Add Credit + */ + // add credit when credit not enough const isAddCredit = step === 'addCredit' const isAddCreditComplete = step === 'addCreditComplete' const isAddCreditProcessing = step === 'addCreditProcessing' + // stripe elements const isCheckout = step === 'checkout' + // processing + const isProcessing = step === 'processing' + + /** + * Donation + */ + // complete dialog for donation const isComplete = step === 'complete' + // set donation amount + const isSetAmount = step === 'setAmount' + // confirm donation amount const isConfirm = step === 'confirm' + + /** + * Password + */ + // wrong password const isPasswordInvalid = step === 'passwordInvalid' - const isProcessing = step === 'processing' const isResetPasswordComplete = step === 'resetPasswordComplete' const isResetPasswordConfirm = step === 'resetPasswordConfirm' const isResetPasswordRequest = step === 'resetPasswordRequest' - const isSetAmount = step === 'setAmount' const isSetPaymentPassword = step === 'setPaymentPassword' + const isHKD = currency === CURRENCY.HKD + useEffect(() => { + analytics.trackEvent('view_donation_dialog', { step }) + }, [step]) + return ( <> {children({ open })} diff --git a/src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx b/src/components/Editor/Sidebar/Collapsable/index.tsx similarity index 100% rename from src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx rename to src/components/Editor/Sidebar/Collapsable/index.tsx diff --git a/src/views/Me/DraftDetail/Sidebar/Collapsable/styles.css b/src/components/Editor/Sidebar/Collapsable/styles.css similarity index 100% rename from src/views/Me/DraftDetail/Sidebar/Collapsable/styles.css rename to src/components/Editor/Sidebar/Collapsable/styles.css diff --git a/src/components/CollectionEditor/CollectForm.tsx b/src/components/Editor/Sidebar/Collection/CollectionEditor/CollectForm.tsx similarity index 100% rename from src/components/CollectionEditor/CollectForm.tsx rename to src/components/Editor/Sidebar/Collection/CollectionEditor/CollectForm.tsx diff --git a/src/components/CollectionEditor/index.tsx b/src/components/Editor/Sidebar/Collection/CollectionEditor/index.tsx similarity index 100% rename from src/components/CollectionEditor/index.tsx rename to src/components/Editor/Sidebar/Collection/CollectionEditor/index.tsx diff --git a/src/components/CollectionEditor/styles.css b/src/components/Editor/Sidebar/Collection/CollectionEditor/styles.css similarity index 100% rename from src/components/CollectionEditor/styles.css rename to src/components/Editor/Sidebar/Collection/CollectionEditor/styles.css diff --git a/src/components/Editor/Sidebar/Collection/index.tsx b/src/components/Editor/Sidebar/Collection/index.tsx new file mode 100644 index 0000000000..83cb50b220 --- /dev/null +++ b/src/components/Editor/Sidebar/Collection/index.tsx @@ -0,0 +1,50 @@ +import classNames from 'classnames' +import _uniq from 'lodash/uniq' +import dynamic from 'next/dynamic' + +import { Spinner, Translate } from '~/components' + +import Collapsable from '../Collapsable' +import styles from './styles.css' + +import { ArticleDigestDropdownArticle } from '~/components/ArticleDigest/Dropdown/__generated__/ArticleDigestDropdownArticle' + +const DynamicCollectionEditor = dynamic(() => import('./CollectionEditor'), { + ssr: false, + loading: Spinner, +}) + +interface CollectionProps { + articles: ArticleDigestDropdownArticle[] + onEdit: (articles: ArticleDigestDropdownArticle[]) => any + disabled?: boolean +} + +const Collection = ({ articles, onEdit, disabled }: CollectionProps) => { + const containerClasses = classNames({ + container: true, + 'u-area-disable': disabled, + }) + + return ( + } + defaultCollapsed={articles.length <= 0} + > +

+ +

+ +
+ +
+ + +
+ ) +} + +export default Collection diff --git a/src/views/Me/DraftDetail/Sidebar/CollectArticles/styles.css b/src/components/Editor/Sidebar/Collection/styles.css similarity index 100% rename from src/views/Me/DraftDetail/Sidebar/CollectArticles/styles.css rename to src/components/Editor/Sidebar/Collection/styles.css diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx b/src/components/Editor/Sidebar/Tags/SearchTags.tsx similarity index 99% rename from src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx rename to src/components/Editor/Sidebar/Tags/SearchTags.tsx index a95ae5fb11..cd74a608bd 100644 --- a/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx +++ b/src/components/Editor/Sidebar/Tags/SearchTags.tsx @@ -106,6 +106,7 @@ const SearchTags = ({ addTag }: { addTag: (tag: string) => void }) => { addTag={(tag: string) => { addTag(tag) setSearch('') + close() }} /> } diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/Tag.tsx b/src/components/Editor/Sidebar/Tags/Tag.tsx similarity index 100% rename from src/views/Me/DraftDetail/Sidebar/AddTags/Tag.tsx rename to src/components/Editor/Sidebar/Tags/Tag.tsx diff --git a/src/components/Editor/Sidebar/Tags/index.tsx b/src/components/Editor/Sidebar/Tags/index.tsx new file mode 100644 index 0000000000..5741f61c21 --- /dev/null +++ b/src/components/Editor/Sidebar/Tags/index.tsx @@ -0,0 +1,47 @@ +import classNames from 'classnames' +import _uniq from 'lodash/uniq' + +import { Translate } from '~/components' + +import Collapsable from '../Collapsable' +import SearchTags from './SearchTags' +import styles from './styles.css' +import Tag from './Tag' + +interface AddTagsProps { + tags: string[] + onAddTag: (tag: string) => void + onDeleteTag: (tag: string) => void + disabled?: boolean +} + +const AddTags = ({ tags, onAddTag, onDeleteTag, disabled }: AddTagsProps) => { + const hasTags = tags.length > 0 + const tagsContainerClasses = classNames({ + 'tags-container': true, + 'u-area-disable': disabled, + }) + + return ( + } defaultCollapsed={!hasTags}> +

+ +

+ +
+ {tags.map((tag) => ( + + ))} + + +
+ + +
+ ) +} + +export default AddTags diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/styles.css b/src/components/Editor/Sidebar/Tags/styles.css similarity index 100% rename from src/views/Me/DraftDetail/Sidebar/AddTags/styles.css rename to src/components/Editor/Sidebar/Tags/styles.css diff --git a/src/components/Empty/EmptyFollowingTag.tsx b/src/components/Empty/EmptyFollowingTag.tsx new file mode 100644 index 0000000000..196730d874 --- /dev/null +++ b/src/components/Empty/EmptyFollowingTag.tsx @@ -0,0 +1,13 @@ +import { Empty, IconEmptyWarning, Translate } from '~/components' + +export const EmptyFollowingTag = () => ( + } + description={ + + } + /> +) diff --git a/src/components/Empty/index.tsx b/src/components/Empty/index.tsx index 3f4703a071..f644b1fe3f 100644 --- a/src/components/Empty/index.tsx +++ b/src/components/Empty/index.tsx @@ -4,6 +4,7 @@ export * from './EmptyArticle' export * from './EmptyBookmark' export * from './EmptyComment' export * from './EmptyDraft' +export * from './EmptyFollowingTag' export * from './EmptyHistory' export * from './EmptyNotice' export * from './EmptyResponse' diff --git a/src/components/Form/DropdownInput/index.tsx b/src/components/Form/DropdownInput/index.tsx index 9614a22bc2..8411a53c47 100644 --- a/src/components/Form/DropdownInput/index.tsx +++ b/src/components/Form/DropdownInput/index.tsx @@ -16,6 +16,7 @@ interface DropdownProps { dropdownCallback?: (params: any) => void dropdownZIndex?: number query: any + queryFilter?: Record } type InputProps = { @@ -44,6 +45,7 @@ const DropdownInput: React.FC = ({ dropdownCallback, dropdownZIndex, query, + queryFilter, ...inputProps }) => { @@ -71,7 +73,7 @@ const DropdownInput: React.FC = ({ } const { data, loading } = useQuery(query, { - variables: { search: debouncedSearch }, + variables: { search: debouncedSearch, filter: queryFilter }, skip: !debouncedSearch, }) diff --git a/src/components/Forms/PaymentForm/Checkout/CardSection.tsx b/src/components/Forms/PaymentForm/Checkout/CardSection.tsx index b69877047d..7f3ed238f7 100644 --- a/src/components/Forms/PaymentForm/Checkout/CardSection.tsx +++ b/src/components/Forms/PaymentForm/Checkout/CardSection.tsx @@ -47,7 +47,12 @@ const CardSection: React.FC = ({ error, onChange }) => { } + label={ + + } /> diff --git a/src/components/GQL/mutations/blockUser.ts b/src/components/GQL/mutations/blockUser.ts deleted file mode 100644 index ce44ca61b2..0000000000 --- a/src/components/GQL/mutations/blockUser.ts +++ /dev/null @@ -1,10 +0,0 @@ -import gql from 'graphql-tag' - -export default gql` - mutation BlockUser($id: ID!) { - blockUser(input: { id: $id }) { - id - isBlocked - } - } -` diff --git a/src/components/GQL/mutations/createDraft.ts b/src/components/GQL/mutations/createDraft.ts new file mode 100644 index 0000000000..645f23b551 --- /dev/null +++ b/src/components/GQL/mutations/createDraft.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag' + +export default gql` + mutation CreateDraft($title: String!, $tags: [String!]) { + putDraft(input: { title: $title, tags: $tags }) { + id + slug + } + } +` diff --git a/src/components/GQL/mutations/toggleBlockUser.ts b/src/components/GQL/mutations/toggleBlockUser.ts new file mode 100644 index 0000000000..e823676324 --- /dev/null +++ b/src/components/GQL/mutations/toggleBlockUser.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag' + +export default gql` + mutation ToggleBlockUser($id: ID!, $enabled: Boolean) { + toggleBlockUser(input: { id: $id, enabled: $enabled }) { + id + isBlocked + } + } +` diff --git a/src/components/GQL/mutations/toggleFollowTag.ts b/src/components/GQL/mutations/toggleFollowTag.ts new file mode 100644 index 0000000000..9cf4224cae --- /dev/null +++ b/src/components/GQL/mutations/toggleFollowTag.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag' + +export default gql` + mutation ToggleFollowTag($id: ID!, $enabled: Boolean) { + toggleFollowTag(input: { id: $id, enabled: $enabled }) { + id + isFollower + } + } +` diff --git a/src/components/GQL/mutations/toggleFollowUser.ts b/src/components/GQL/mutations/toggleFollowUser.ts new file mode 100644 index 0000000000..31ee472d29 --- /dev/null +++ b/src/components/GQL/mutations/toggleFollowUser.ts @@ -0,0 +1,11 @@ +import gql from 'graphql-tag' + +export default gql` + mutation ToggleFollowUser($id: ID!, $enabled: Boolean) { + toggleFollowUser(input: { id: $id, enabled: $enabled }) { + id + isFollowee + isFollower + } + } +` diff --git a/src/components/GQL/mutations/togglePinComment.ts b/src/components/GQL/mutations/togglePinComment.ts new file mode 100644 index 0000000000..7bbac6aa3c --- /dev/null +++ b/src/components/GQL/mutations/togglePinComment.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export default gql` + mutation TogglePinComment($id: ID!, $enabled: Boolean) { + togglePinComment(input: { id: $id, enabled: $enabled }) { + id + pinned + article { + id + pinCommentLeft + } + } + } +` diff --git a/src/components/GQL/mutations/toggleSubscribeArticle.ts b/src/components/GQL/mutations/toggleSubscribeArticle.ts index 551ad4d95e..9f0948275d 100644 --- a/src/components/GQL/mutations/toggleSubscribeArticle.ts +++ b/src/components/GQL/mutations/toggleSubscribeArticle.ts @@ -1,8 +1,8 @@ import gql from 'graphql-tag' export default gql` - mutation ToggleSubscribeArticle($id: ID!) { - toggleSubscribeArticle(input: { id: $id }) { + mutation ToggleSubscribeArticle($id: ID!, $enabled: Boolean) { + toggleSubscribeArticle(input: { id: $id, enabled: $enabled }) { id subscribed } diff --git a/src/components/GQL/mutations/unblockUser.ts b/src/components/GQL/mutations/unblockUser.ts deleted file mode 100644 index f753ecb950..0000000000 --- a/src/components/GQL/mutations/unblockUser.ts +++ /dev/null @@ -1,10 +0,0 @@ -import gql from 'graphql-tag' - -export default gql` - mutation UnblockUser($id: ID!) { - unblockUser(input: { id: $id }) { - id - isBlocked - } - } -` diff --git a/src/components/GQL/queries/followeeCount.ts b/src/components/GQL/queries/followeeCount.ts deleted file mode 100644 index 99537d03c0..0000000000 --- a/src/components/GQL/queries/followeeCount.ts +++ /dev/null @@ -1,12 +0,0 @@ -import gql from 'graphql-tag' - -export default gql` - query ViewerFolloweeCount { - viewer { - id - followees(input: { first: 0 }) { - totalCount - } - } - } -` diff --git a/src/components/GQL/queries/searchArticles.ts b/src/components/GQL/queries/searchArticles.ts index 2c0f3e6041..05f4223a7f 100644 --- a/src/components/GQL/queries/searchArticles.ts +++ b/src/components/GQL/queries/searchArticles.ts @@ -3,8 +3,8 @@ import gql from 'graphql-tag' import { ArticleDigestDropdown } from '~/components' export default gql` - query SearchArticles($search: String!) { - search(input: { key: $search, type: Article, first: 5 }) { + query SearchArticles($search: String!, $filter: SearchFilter) { + search(input: { key: $search, type: Article, first: 5, filter: $filter }) { edges { node { ... on Article { diff --git a/src/components/GQL/queries/tagArticles.ts b/src/components/GQL/queries/tagArticles.ts index eac0f3dcbf..a89f016518 100644 --- a/src/components/GQL/queries/tagArticles.ts +++ b/src/components/GQL/queries/tagArticles.ts @@ -2,8 +2,8 @@ import gql from 'graphql-tag' import { ArticleDigestFeed } from '~/components' -export default gql` - query TagArticles($id: ID!, $after: String, $selected: Boolean) { +export const TAG_ARTICLES_PUBLIC = gql` + query TagArticlesPublic($id: ID!, $after: String, $selected: Boolean) { node(input: { id: $id }) { ... on Tag { id @@ -16,12 +16,26 @@ export default gql` edges { cursor node { - ...ArticleDigestFeedArticle + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate } } } } } } - ${ArticleDigestFeed.fragments.article} + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +export const TAG_ARTICLES_PRIVATE = gql` + query TagArticlesPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Article { + ...ArticleDigestFeedArticlePrivate + } + } + } + ${ArticleDigestFeed.fragments.article.private} ` diff --git a/src/components/GQL/queries/tagArticlesCount.ts b/src/components/GQL/queries/tagArticlesCount.ts new file mode 100644 index 0000000000..04e476b03d --- /dev/null +++ b/src/components/GQL/queries/tagArticlesCount.ts @@ -0,0 +1,14 @@ +import gql from 'graphql-tag' + +export default gql` + query TagArticlesCount($id: ID!) { + node(input: { id: $id }) { + ... on Tag { + id + articles(input: { first: 0, selected: false }) { + totalCount + } + } + } + } +` diff --git a/src/components/GQL/queries/tagFollowers.ts b/src/components/GQL/queries/tagFollowers.ts new file mode 100644 index 0000000000..99bab6db26 --- /dev/null +++ b/src/components/GQL/queries/tagFollowers.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag' + +import { Avatar } from '~/components' + +export default gql` + query TagFollowers($id: ID!) { + node(input: { id: $id }) { + ... on Tag { + id + followers(input: { first: 5 }) { + totalCount + edges { + cursor + node { + ... on User { + id + ...AvatarUser + } + } + } + } + } + } + } + ${Avatar.fragments.user} +` diff --git a/src/components/GQL/queries/unreadResponseInfoPopUp.ts b/src/components/GQL/queries/unreadResponseInfoPopUp.ts deleted file mode 100644 index 40026c5ade..0000000000 --- a/src/components/GQL/queries/unreadResponseInfoPopUp.ts +++ /dev/null @@ -1,12 +0,0 @@ -import gql from 'graphql-tag' - -export default gql` - query UnreadResponseInfoPopUp { - viewer { - id - status { - unreadResponseInfoPopUp - } - } - } -` diff --git a/src/components/GQL/queries/userArticles.ts b/src/components/GQL/queries/userArticles.ts index e9b6d15fb6..cd5cabb6ba 100644 --- a/src/components/GQL/queries/userArticles.ts +++ b/src/components/GQL/queries/userArticles.ts @@ -2,38 +2,69 @@ import gql from 'graphql-tag' import { ArticleDigestFeed } from '~/components' -export default gql` - query UserArticles($userName: String!, $after: String) { - user(input: { userName: $userName }) { - id - displayName - info { - description - profileCover +const fragment = gql` + fragment ArticlesUser on User { + id + displayName + info { + description + profileCover + } + articles(input: { first: 10, after: $after }) { + totalCount + pageInfo { + startCursor + endCursor + hasNextPage } - articles(input: { first: 10, after: $after }) - @connection(key: "userArticles") { - totalCount - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - createdAt - wordCount - ...ArticleDigestFeedArticle - } + edges { + cursor + node { + createdAt + wordCount + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate } } - status { - state - articleCount - totalWordCount + } + status { + state + articleCount + totalWordCount + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +// without `Public` suffix, query as a logged-in user +export const VIEWER_ARTICLES = gql` + query ViewerArticles($userName: String!, $after: String) { + user(input: { userName: $userName }) @connection(key: "viewerArticles") { + ...ArticlesUser + } + } + ${fragment} +` + +// with `Public` suffix, query as an anonymous user +export const USER_ARTICLES_PUBLIC = gql` + query UserArticlesPublic($userName: String!, $after: String) { + user(input: { userName: $userName }) { + ...ArticlesUser + } + } + ${fragment} +` + +export const USER_ARTICLES_PRIVATE = gql` + query UserArticlesPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Article { + ...ArticleDigestFeedArticlePrivate } } } - ${ArticleDigestFeed.fragments.article} + ${ArticleDigestFeed.fragments.article.private} ` diff --git a/src/components/GQL/updates/tagArticlesCount.ts b/src/components/GQL/updates/tagArticlesCount.ts new file mode 100644 index 0000000000..c26044d1b9 --- /dev/null +++ b/src/components/GQL/updates/tagArticlesCount.ts @@ -0,0 +1,57 @@ +import { DataProxy } from 'apollo-cache' +import _cloneDeep from 'lodash/cloneDeep' + +import TAG_ARTICLES_COUNT from '~/components/GQL/queries/tagArticlesCount' + +import { ERROR_CODES } from '~/common/enums' + +import { TagArticlesCount } from '~/components/GQL/queries/__generated__/TagArticlesCount' + +const update = ({ + cache, + id, + count = 1, + type, +}: { + cache: DataProxy + id: string + count?: number + type: 'increment' | 'decrement' +}) => { + try { + if (!id) { + return + } + + const variables = { id } + const cacheData = cache.readQuery({ + query: TAG_ARTICLES_COUNT, + variables, + }) + + const data = _cloneDeep(cacheData) + if (!data || !data.node || data.node.__typename !== 'Tag') { + return + } + + if (type === 'increment') { + data.node.articles.totalCount += count + } else { + data.node.articles.totalCount -= count + } + + cache.writeQuery({ + query: TAG_ARTICLES_COUNT, + variables, + data, + }) + } catch (e) { + if (e.message.startsWith("Can't find field")) { + console.warn(ERROR_CODES.QUERY_FIELD_NOT_FOUND) + } else { + console.error(e) + } + } +} + +export default update diff --git a/src/components/GQL/updates/tagFollowers.ts b/src/components/GQL/updates/tagFollowers.ts new file mode 100644 index 0000000000..70008ab9a3 --- /dev/null +++ b/src/components/GQL/updates/tagFollowers.ts @@ -0,0 +1,73 @@ +import { DataProxy } from 'apollo-cache' + +import TAG_FOLLOWERS from '~/components/GQL/queries/tagFollowers' + +import { ERROR_CODES } from '~/common/enums' + +import { TagFollowers } from '~/components/GQL/queries/__generated__/TagFollowers' + +const update = ({ + cache, + id, + type, + viewer, +}: { + cache: DataProxy + id: string + type: 'follow' | 'unfollow' + viewer: any +}) => { + try { + if (!id) { + return + } + + const variables = { id } + const cacheData = cache.readQuery({ + query: TAG_FOLLOWERS, + variables, + }) + + if (!cacheData || !cacheData.node || cacheData.node.__typename !== 'Tag') { + return + } + + const followers = cacheData.node.followers.edges || [] + if (type === 'follow') { + followers.unshift({ + cursor: window.btoa(`arrayconnection:${followers.length}:0`) || '', + node: { + avatar: viewer.avatar, + id: viewer.id, + liker: { + civicLiker: viewer.liker.civicLiker, + __typename: 'Liker', + }, + __typename: 'User', + }, + __typename: 'UserEdge', + }) + cacheData.node.followers.edges = followers + cacheData.node.followers.totalCount++ + } else { + cacheData.node.followers.edges = followers.filter( + (follower) => follower.node.id !== viewer.id + ) + cacheData.node.followers.totalCount-- + } + + cache.writeQuery({ + query: TAG_FOLLOWERS, + variables, + data: cacheData, + }) + } catch (e) { + if (e.message.startsWith("Can't find field")) { + console.warn(ERROR_CODES.QUERY_FIELD_NOT_FOUND) + } else { + console.error(e) + } + } +} + +export default update diff --git a/src/components/GQL/updates/userArticles.ts b/src/components/GQL/updates/userArticles.ts index 0e7a537a30..9870c8a623 100644 --- a/src/components/GQL/updates/userArticles.ts +++ b/src/components/GQL/updates/userArticles.ts @@ -1,12 +1,12 @@ import { DataProxy } from 'apollo-cache' import { - UserArticles, - UserArticles_user_articles_edges, -} from '~/components/GQL/queries/__generated__/UserArticles' + UserArticlesPublic, + UserArticlesPublic_user_articles_edges, +} from '~/components/GQL/queries/__generated__/UserArticlesPublic' const sortEdgesByCreatedAtDesc = ( - edges: UserArticles_user_articles_edges[] + edges: UserArticlesPublic_user_articles_edges[] ) => { return edges.sort( ({ node: n1 }, { node: n2 }) => @@ -26,15 +26,17 @@ const update = ({ type: 'sticky' | 'unsticky' | 'archive' }) => { // FIXME: circular dependencies - const USER_ARTICLES = require('~/components/GQL/queries/userArticles').default + const { + USER_ARTICLES_PUBLIC, + } = require('~/components/GQL/queries/userArticles').default if (!userName) { return } try { - const data = cache.readQuery({ - query: USER_ARTICLES, + const data = cache.readQuery({ + query: USER_ARTICLES_PUBLIC, variables: { userName }, }) @@ -72,7 +74,7 @@ const update = ({ } cache.writeQuery({ - query: USER_ARTICLES, + query: USER_ARTICLES_PUBLIC, variables: { userName }, data: { user: { diff --git a/src/components/GQL/updates/viewerFolloweeCount.ts b/src/components/GQL/updates/viewerFolloweeCount.ts index fd0c004f79..3e71e5767f 100644 --- a/src/components/GQL/updates/viewerFolloweeCount.ts +++ b/src/components/GQL/updates/viewerFolloweeCount.ts @@ -1,10 +1,20 @@ import { DataProxy } from 'apollo-cache' - -import VIEWER_FOLLOWEE_COUNT from '~/components/GQL/queries/followeeCount' +import gql from 'graphql-tag' import { ERROR_CODES } from '~/common/enums' -import { ViewerFolloweeCount } from '~/components/GQL/queries/__generated__/ViewerFolloweeCount' +import { ViewerFolloweeCount } from './__generated__/ViewerFolloweeCount' + +const VIEWER_FOLLOWEE_COUNT = gql` + query ViewerFolloweeCount { + viewer { + id + followees(input: { first: 0 }) { + totalCount + } + } + } +` const update = ({ cache, diff --git a/src/components/Icon/IconAddMedium.tsx b/src/components/Icon/IconAddMedium.tsx new file mode 100644 index 0000000000..33123addd9 --- /dev/null +++ b/src/components/Icon/IconAddMedium.tsx @@ -0,0 +1,4 @@ +import { ReactComponent as AddMedium } from './icons/add-md.svg' +import { withIcon } from './withIcon' + +export const IconAddMedium = withIcon(AddMedium) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 708a81925d..4d8e736343 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -20,6 +20,7 @@ export * from './withIcon' export * from './IconAdd' +export * from './IconAddMedium' export * from './IconAppreciationMAX' export * from './IconArchiveMedium' export * from './IconAvatarLogo' diff --git a/src/components/Interaction/InfiniteScroll.tsx b/src/components/Interaction/InfiniteScroll.tsx index 9b2edaec03..a7b4780ae8 100644 --- a/src/components/Interaction/InfiniteScroll.tsx +++ b/src/components/Interaction/InfiniteScroll.tsx @@ -1,5 +1,3 @@ -import { ApolloQueryResult } from 'apollo-client' -import { forwardRef, Ref } from 'react' import { Waypoint } from 'react-waypoint' import { PullToRefresh, Spinner } from '~/components' @@ -34,7 +32,7 @@ interface Props { /** * Callback to load more entities */ - loadMore: () => Promise> + loadMore: () => Promise /** * A React component to act as loader @@ -54,28 +52,23 @@ export const InfiniteScroll: React.FC = ({ pullToRefresh, children, }) => { - const LoaderWithRef = forwardRef((props, ref: Ref) => ( -
{loader || }
- )) - - const Inner = () => ( -
- {children} - {hasNextPage && ( - loadMore()}> - - - )} -
- ) - if (pullToRefresh) { return ( - + <> + {children} + {hasNextPage && } + {hasNextPage && loader} + ) } - return + return ( + <> + {children} + {hasNextPage && } + {hasNextPage && loader} + + ) } diff --git a/src/components/Protected/index.tsx b/src/components/Protected/index.tsx index 6160424a15..ba6fc2c755 100644 --- a/src/components/Protected/index.tsx +++ b/src/components/Protected/index.tsx @@ -8,23 +8,15 @@ export const Protected: React.FC = ({ children }) => { const viewer = useContext(ViewerContext) useEffect(() => { - if (!viewer.isAuthed && process.browser) { + if (viewer.privateFetched && !viewer.isAuthed) { redirectToLogin() } - }, []) + }, [viewer.privateFetched]) - if (viewer.isAuthed) { + if (viewer.isAuthed && viewer.privateFetched) { return <>{children} } - if (!process.browser) { - return ( - - - - ) - } - return ( diff --git a/src/components/PushInitializer/index.tsx b/src/components/PushInitializer/index.tsx index 6520b197ad..06a1c8a72d 100644 --- a/src/components/PushInitializer/index.tsx +++ b/src/components/PushInitializer/index.tsx @@ -9,8 +9,10 @@ const PushInitializer = ({ client }: { client: ApolloClient }) => { const viewer = useContext(ViewerContext) useEffect(() => { - initializePush({ client, viewer }) - }, []) + if (viewer.privateFetched) { + initializePush({ client, viewer }) + } + }, [viewer.privateFetched]) return null } diff --git a/src/components/Root/gql.ts b/src/components/Root/gql.ts new file mode 100644 index 0000000000..9ef76582af --- /dev/null +++ b/src/components/Root/gql.ts @@ -0,0 +1,51 @@ +import gql from 'graphql-tag' + +import { + AnalyticsListener, + FeaturesProvider, + ViewerProvider, +} from '~/components' + +const fragments = { + user: gql` + fragment Viewer on User { + id + ...ViewerUser + ...AnalyticsUser + } + ${ViewerProvider.fragments.user} + ${AnalyticsListener.fragments.user} + `, + official: gql` + fragment Official on Official { + ...FeatureOfficial + } + ${FeaturesProvider.fragments.official} + `, +} + +export const ROOT_QUERY_PUBLIC = gql` + query RootQueryPublic { + viewer { + ...Viewer + } + official { + ...Official + } + } + ${fragments.user} + ${fragments.official} +` + +export const ROOT_QUERY_PRIVATE = gql` + query RootQueryPrivate { + viewer { + ...Viewer + } + official { + ...Official + } + } + ${fragments.user} + ${fragments.official} +` diff --git a/src/components/Root/index.tsx b/src/components/Root/index.tsx new file mode 100644 index 0000000000..abaaf4b863 --- /dev/null +++ b/src/components/Root/index.tsx @@ -0,0 +1,125 @@ +import { useQuery } from '@apollo/react-hooks' +import { InMemoryCache } from 'apollo-cache-inmemory' +import { ApolloClient } from 'apollo-client' +import dynamic from 'next/dynamic' +import { useRouter } from 'next/router' +import React, { useEffect, useState } from 'react' + +import { + AnalyticsListener, + Error, + FeaturesProvider, + LanguageProvider, + Layout, + Toast, + ViewerProvider, +} from '~/components' +import { QueryError } from '~/components/GQL' + +import { PATHS } from '~/common/enums' +import { analytics } from '~/common/utils' + +import { ROOT_QUERY_PRIVATE, ROOT_QUERY_PUBLIC } from './gql' + +import { RootQueryPublic } from './__generated__/RootQueryPublic' + +const DynamicPushInitializer = dynamic( + () => import('~/components/PushInitializer'), + { + ssr: false, + } +) +const DynamicProgressBar = dynamic(() => import('~/components/ProgressBar'), { + ssr: false, +}) +const DynamicGlobalDialogs = dynamic( + () => import('~/components/GlobalDialogs'), + { + ssr: false, + } +) +const DynamicFingerprint = dynamic(() => import('~/components/Fingerprint'), { + ssr: false, +}) + +/** + * `` contains components that depend on viewer + * + */ +// Sentry +import('@sentry/browser').then((Sentry) => { + Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '' }) +}) + +const Root = ({ + client, + children, +}: { + client: ApolloClient + children: React.ReactNode +}) => { + useEffect(() => { + analytics.trackPage() + }) + + useEffect(() => { + analytics.identifyUser() + }, []) + + const router = useRouter() + const isInAbout = router.pathname === PATHS.ABOUT + const isInMigration = router.pathname === PATHS.MIGRATION + const shouldApplyLayout = !isInAbout && !isInMigration + + // anonymous + const { loading, data, error } = useQuery(ROOT_QUERY_PUBLIC) + const viewer = data?.viewer + const official = data?.official + + // viewer + const [privateFetched, setPrivateFetched] = useState(false) + const fetchPrivateViewer = async () => { + await client.query({ + query: ROOT_QUERY_PRIVATE, + fetchPolicy: 'network-only', + }) + setPrivateFetched(true) + } + useEffect(() => { + if (!data) { + return + } + fetchPrivateViewer() + }, [!!data]) + + if (loading) { + return null + } + + if (error) { + return + } + + if (!viewer) { + return + } + + return ( + + + + {shouldApplyLayout ? {children} : children} + + + + + + + + + + + ) +} + +export default Root diff --git a/src/components/Search/SearchOverview/gql.ts b/src/components/Search/SearchOverview/gql.ts new file mode 100644 index 0000000000..f4c44491e5 --- /dev/null +++ b/src/components/Search/SearchOverview/gql.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +import ClearHistoryButton from './ClearHistoryButton' + +export const SEARCH_AUTOCOMPLETE_PUBLIC = gql` + query SearchOverviewPublic { + frequentSearch(input: { first: 5, key: "" }) + viewer { + id + ...RecentSearchesUser + } + } + ${ClearHistoryButton.fragments.user} +` + +export const SEARCH_AUTOCOMPLETE_PRIVATE = gql` + query SearchOverviewPrivate { + viewer { + id + ...RecentSearchesUser + } + } + ${ClearHistoryButton.fragments.user} +` diff --git a/src/components/Search/SearchOverview/index.tsx b/src/components/Search/SearchOverview/index.tsx index 7e4ca991af..7c2a45fb0e 100644 --- a/src/components/Search/SearchOverview/index.tsx +++ b/src/components/Search/SearchOverview/index.tsx @@ -1,36 +1,33 @@ import { useQuery } from '@apollo/react-hooks' import classNames from 'classnames' -import gql from 'graphql-tag' import Link from 'next/link' -import { Fragment } from 'react' +import { Fragment, useContext, useEffect } from 'react' -import { Menu, Translate } from '~/components' +import { Menu, Translate, ViewerContext } from '~/components' import { Spinner } from '~/components/Spinner' import { toPath } from '~/common/utils' import ClearHistoryButton from './ClearHistoryButton' +import { SEARCH_AUTOCOMPLETE_PRIVATE, SEARCH_AUTOCOMPLETE_PUBLIC } from './gql' import styles from './styles.css' -import { SearchOverview as SearchOverviewType } from './__generated__/SearchOverview' +import { SearchOverviewPublic } from './__generated__/SearchOverviewPublic' interface SearchOverviewProps { inPage?: boolean } -const SEARCH_AUTOCOMPLETE = gql` - query SearchOverview { - frequentSearch(input: { first: 5, key: "" }) - viewer { - id - ...RecentSearchesUser - } - } - ${ClearHistoryButton.fragments.user} -` - export const SearchOverview = ({ inPage }: SearchOverviewProps) => { - const { data, loading } = useQuery(SEARCH_AUTOCOMPLETE) + const viewer = useContext(ViewerContext) + + /** + * Data Fetching + */ + // public data + const { data, loading, client } = useQuery( + SEARCH_AUTOCOMPLETE_PUBLIC + ) const frequentSearch = data?.frequentSearch || [] const recentSearches = data?.viewer?.activity.recentSearches.edges || [] @@ -46,6 +43,25 @@ export const SearchOverview = ({ inPage }: SearchOverviewProps) => { inPage, }) + // private data + const loadPrivate = () => { + if (!viewer.id) { + return + } + + client.query({ + query: SEARCH_AUTOCOMPLETE_PRIVATE, + fetchPolicy: 'network-only', + }) + } + + useEffect(() => { + loadPrivate() + }, [viewer.id]) + + /** + * Render + */ if (loading) { return ( @@ -55,7 +71,6 @@ export const SearchOverview = ({ inPage }: SearchOverviewProps) => { } if (!showFrequentSearch && !showSearchHistory) { - // TODO: Empty Notice return null } diff --git a/src/components/Title/index.tsx b/src/components/Title/index.tsx index 28db018049..273bc61568 100644 --- a/src/components/Title/index.tsx +++ b/src/components/Title/index.tsx @@ -3,7 +3,7 @@ import React from 'react' import styles from './styles.css' -type TitleType = 'article' | 'feed' | 'sidebar' | 'nav' +type TitleType = 'article' | 'feed' | 'sidebar' | 'nav' | 'tag' type TitleIs = 'h1' | 'h2' | 'h3' diff --git a/src/components/Title/styles.css b/src/components/Title/styles.css index 725be38755..d2f74c2d61 100644 --- a/src/components/Title/styles.css +++ b/src/components/Title/styles.css @@ -1,4 +1,5 @@ -.article { +.article, +.tag { font-size: var(--font-size-article-title); font-weight: var(--font-weight-article-title); line-height: var(--line-height-article-title); diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx index 7347ea60cd..5fabd64e72 100644 --- a/src/components/UserProfile/DropdownActions/index.tsx +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -15,22 +15,29 @@ import { BlockUser } from '~/components/BlockUser' import { TEXT } from '~/common/enums' -import { DropdownActionsUser } from './__generated__/DropdownActionsUser' +import { DropdownActionsUserPublic } from './__generated__/DropdownActionsUserPublic' const fragments = { - user: gql` - fragment DropdownActionsUser on User { - id - ...BlockUserPublic - ...BlockUserPrivate - } - ${BlockUser.fragments.user.public} - ${BlockUser.fragments.user.private} - `, + user: { + public: gql` + fragment DropdownActionsUserPublic on User { + id + ...BlockUserPublic + } + ${BlockUser.fragments.user.public} + `, + private: gql` + fragment DropdownActionsUserPrivate on User { + id + ...BlockUserPrivate + } + ${BlockUser.fragments.user.private} + `, + }, } interface DropdownActionsProps { - user: DropdownActionsUser + user: DropdownActionsUserPublic isMe: boolean } diff --git a/src/components/UserProfile/EditProfileButton/ProfileEditor/ProfileCoverUploader/index.tsx b/src/components/UserProfile/EditProfileButton/ProfileEditor/ProfileCoverUploader/index.tsx index 4be2b51efa..8d7a74e45c 100644 --- a/src/components/UserProfile/EditProfileButton/ProfileEditor/ProfileCoverUploader/index.tsx +++ b/src/components/UserProfile/EditProfileButton/ProfileEditor/ProfileCoverUploader/index.tsx @@ -21,7 +21,7 @@ import Cover from '../../../Cover' import styles from './styles.css' import { SingleFileUpload } from '~/components/GQL/mutations/__generated__/SingleFileUpload' -import { ProfileUser } from '~/components/UserProfile/__generated__/ProfileUser' +import { ProfileUserPublic } from '~/components/UserProfile/__generated__/ProfileUserPublic' /** * This component is for uploading profile cover. @@ -34,7 +34,7 @@ import { ProfileUser } from '~/components/UserProfile/__generated__/ProfileUser' */ interface Props { - user: ProfileUser + user: ProfileUserPublic onUpload: (assetId: string | null) => void } diff --git a/src/components/UserProfile/EditProfileButton/ProfileEditor/index.tsx b/src/components/UserProfile/EditProfileButton/ProfileEditor/index.tsx index bb53ead644..2235c9a051 100644 --- a/src/components/UserProfile/EditProfileButton/ProfileEditor/index.tsx +++ b/src/components/UserProfile/EditProfileButton/ProfileEditor/index.tsx @@ -23,10 +23,10 @@ import { import ProfileCoverUploader from './ProfileCoverUploader' import styles from './styles.css' -import { ProfileUser } from '~/components/UserProfile/__generated__/ProfileUser' +import { ProfileUserPublic } from '~/components/UserProfile/__generated__/ProfileUserPublic' import { UpdateUserInfoProfile } from './__generated__/UpdateUserInfoProfile' -export type ProfileEditorUser = ProfileUser +export type ProfileEditorUser = ProfileUserPublic interface FormProps { user: ProfileEditorUser diff --git a/src/components/UserProfile/gql.ts b/src/components/UserProfile/gql.ts new file mode 100644 index 0000000000..3f83dbc440 --- /dev/null +++ b/src/components/UserProfile/gql.ts @@ -0,0 +1,69 @@ +import gql from 'graphql-tag' + +import { Avatar, FollowButton } from '~/components' + +import DropdownActions from './DropdownActions' + +const fragments = { + user: { + public: gql` + fragment ProfileUserPublic on User { + id + userName + displayName + liker { + civicLiker + } + info { + badges { + type + } + description + profileCover + } + followees(input: { first: 0 }) { + totalCount + } + followers(input: { first: 0 }) { + totalCount + } + status { + state + } + ...AvatarUser + ...DropdownActionsUserPublic + } + ${Avatar.fragments.user} + ${DropdownActions.fragments.user.public} + `, + private: gql` + fragment ProfileUserPrivate on User { + id + ...FollowButtonUserPrivate + ...DropdownActionsUserPrivate + } + ${FollowButton.fragments.user.private} + ${DropdownActions.fragments.user.private} + `, + }, +} + +export const USER_PROFILE_PUBLIC = gql` + query UserProfileUserPublic($userName: String!) { + user(input: { userName: $userName }) { + ...ProfileUserPublic + ...ProfileUserPrivate + } + } + ${fragments.user.public} + ${fragments.user.private} +` + +export const USER_PROFILE_PRIVATE = gql` + query UserProfileUserPrivate($userName: String!) { + user(input: { userName: $userName }) { + ...ProfileUserPrivate + } + } + ${fragments.user.private} +` diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index 085f77bf35..158b2cc2d8 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -1,10 +1,8 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' -import _get from 'lodash/get' import _some from 'lodash/some' import Link from 'next/link' import { useRouter } from 'next/router' -import { useContext } from 'react' +import { useContext, useEffect } from 'react' import { Avatar, @@ -25,79 +23,45 @@ import { CivicLikerBadge, SeedBadge } from './Badges' import Cover from './Cover' import DropdownActions from './DropdownActions' import EditProfileButton from './EditProfileButton' +import { USER_PROFILE_PRIVATE, USER_PROFILE_PUBLIC } from './gql' import styles from './styles.css' -import { MeProfileUser } from './__generated__/MeProfileUser' -import { UserProfileUser } from './__generated__/UserProfileUser' - -const fragments = { - user: gql` - fragment ProfileUser on User { - id - userName - displayName - liker { - civicLiker - } - info { - badges { - type - } - description - profileCover - } - followees(input: { first: 0 }) { - totalCount - } - followers(input: { first: 0 }) { - totalCount - } - status { - state - } - ...AvatarUser - ...FollowButtonUserPrivate @skip(if: $isMe) - ...DropdownActionsUser - } - ${Avatar.fragments.user} - ${FollowButton.fragments.user.private} - ${DropdownActions.fragments.user} - `, -} - -const USER_PROFILE = gql` - query UserProfileUser($userName: String!, $isMe: Boolean = false) { - user(input: { userName: $userName }) { - ...ProfileUser - } - } - ${fragments.user} -` - -const ME_PROFILE = gql` - query MeProfileUser($isMe: Boolean = true) { - viewer { - ...ProfileUser - } - } - ${fragments.user} -` +import { UserProfileUserPublic } from './__generated__/UserProfileUserPublic' export const UserProfile = () => { const isSmallUp = useResponsive('sm-up') const router = useRouter() const viewer = useContext(ViewerContext) + // public data const userName = getQuery({ router, key: 'userName' }) const isMe = !userName || viewer.userName === userName - const { data, loading } = useQuery( - isMe ? ME_PROFILE : USER_PROFILE, + const { data, loading, client } = useQuery( + USER_PROFILE_PUBLIC, { - variables: isMe ? {} : { userName }, + variables: { userName }, } ) - const user = isMe ? _get(data, 'viewer') : _get(data, 'user') + const user = data?.user + // fetch private data + useEffect(() => { + if (!viewer.id || !user) { + return + } + + client.query({ + query: USER_PROFILE_PRIVATE, + fetchPolicy: 'network-only', + variables: { + userName, + }, + }) + }, [user?.id, viewer.id]) + + /** + * Render + */ const LayoutHeader = () => ( { const userFollowersPath = toPath({ page: 'userFollowers', - userName: user.userName, + userName, }) const userFolloweesPath = toPath({ page: 'userFollowees', - userName: user.userName, + userName, }) const badges = user.info.badges || [] const hasSeedBadge = _some(badges, { type: 'seed' }) const profileCover = user.info.profileCover || '' + const userState = user.status?.state as string const isCivicLiker = user.liker.civicLiker - const isUserArchived = user.status.state === 'archived' - const isUserBanned = user.status.state === 'banned' + const isUserArchived = userState === 'archived' + const isUserBanned = userState === 'banned' const isUserInactive = isUserArchived || isUserBanned /** diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1c75713aef..1dcc86d918 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,135 +1,17 @@ -import { ApolloProvider, useQuery } from '@apollo/react-hooks' +import { ApolloProvider } from '@apollo/react-hooks' import { getDataFromTree } from '@apollo/react-ssr' import { InMemoryCache } from 'apollo-cache-inmemory' import { ApolloClient } from 'apollo-client' -import gql from 'graphql-tag' import { AppProps } from 'next/app' -import dynamic from 'next/dynamic' -import { useRouter } from 'next/router' -import React, { useEffect } from 'react' -import { - AnalyticsListener, - Error, - ErrorBoundary, - FeaturesProvider, - LanguageProvider, - Layout, - Toast, - ViewerProvider, -} from '~/components' +import { ErrorBoundary } from '~/components' import { ClientUpdater } from '~/components/ClientUpdater' import { GlobalStyles } from '~/components/GlobalStyles' -import { QueryError } from '~/components/GQL' +import Root from '~/components/Root' import SplashScreen from '~/components/SplashScreen' -import { PATHS } from '~/common/enums' -import { analytics } from '~/common/utils' import withApollo from '~/common/utils/withApollo' -import { RootQuery } from './__generated__/RootQuery' - -const ROOT_QUERY = gql` - query RootQuery { - viewer { - id - ...ViewerUser - ...AnalyticsUser - } - official { - ...FeatureOfficial - } - } - ${ViewerProvider.fragments.user} - ${AnalyticsListener.fragments.user} - ${FeaturesProvider.fragments.official} -` - -// Sentry -import('@sentry/browser').then((Sentry) => { - /** - * Initialize - */ - Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '' }) -}) - -/** - * `` contains components that depend on viewer - * - */ -const DynamicPushInitializer = dynamic( - () => import('~/components/PushInitializer'), - { - ssr: false, - } -) -const DynamicProgressBar = dynamic(() => import('~/components/ProgressBar'), { - ssr: false, -}) -const DynamicGlobalDialogs = dynamic( - () => import('~/components/GlobalDialogs'), - { - ssr: false, - } -) -const DynamicFingerprint = dynamic(() => import('~/components/Fingerprint'), { - ssr: false, -}) - -const Root = ({ - client, - children, -}: { - client: ApolloClient - children: React.ReactNode -}) => { - useEffect(() => { - analytics.trackPage() - }) - - useEffect(() => { - analytics.identifyUser() - }, []) - - const router = useRouter() - const isInAbout = router.pathname === PATHS.ABOUT - const isInMigration = router.pathname === PATHS.MIGRATION - const shouldApplyLayout = !isInAbout && !isInMigration - - const { loading, data, error } = useQuery(ROOT_QUERY) - const viewer = data?.viewer - const official = data?.official - - if (loading) { - return null - } - - if (error) { - return - } - - if (!viewer) { - return - } - - return ( - - - - {shouldApplyLayout ? {children} : children} - - - - - - - - - - - ) -} - const MattersApp = ({ Component, pageProps, diff --git a/src/pages/icymi.tsx b/src/pages/icymi.tsx deleted file mode 100644 index c774a27cf9..0000000000 --- a/src/pages/icymi.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ArticleFeed from '~/views/ArticleFeed' - -export default () => diff --git a/src/pages/recommendation.tsx b/src/pages/recommendation.tsx index 08a99cc8b8..150a99856a 100644 --- a/src/pages/recommendation.tsx +++ b/src/pages/recommendation.tsx @@ -30,14 +30,16 @@ const query = gql` cursor node { id - ...ArticleDigestFeedArticle + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate } } } } } } - ${ArticleDigestFeed.fragments.article} + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} ` const Feed = () => { diff --git a/src/pages/topics.tsx b/src/pages/topics.tsx deleted file mode 100644 index be1d47ba85..0000000000 --- a/src/pages/topics.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ArticleFeed from '~/views/ArticleFeed' - -export default () => diff --git a/src/views/ArticleDetail/AppreciationButton/index.tsx b/src/views/ArticleDetail/AppreciationButton/index.tsx index ba4daf5488..b2739fd5f9 100644 --- a/src/views/ArticleDetail/AppreciationButton/index.tsx +++ b/src/views/ArticleDetail/AppreciationButton/index.tsx @@ -75,11 +75,12 @@ const AppreciationButton = ({ article }: AppreciationButtonProps) => { // bundle appreciations const [amount, setAmount] = useState(0) const [sendAppreciation] = useMutation(APPRECIATE_ARTICLE) + const hasAppreciate = article.hasAppreciate const limit = article.appreciateLimit const left = (article.appreciateLeft || 0) - amount const total = article.appreciationsReceivedTotal + amount - const appreciatedCount = limit - left + const appreciatedCount = hasAppreciate || amount ? limit - left : 0 const [debouncedSendAppreciation] = useDebouncedCallback(async () => { try { await sendAppreciation({ @@ -130,7 +131,7 @@ const AppreciationButton = ({ article }: AppreciationButtonProps) => { /** * Appreciate Button */ - if (canAppreciate) { + if (canAppreciate || (!hasAppreciate && amount <= 0)) { return ( = ({ - data, - loading, - error, -}) => { - const isMediumUp = useResponsive('md-up') - const { edges, pageInfo } = data?.article?.collection || {} - - if (loading) { - return - } - - if (error) { - return - } - - if (!edges || !pageInfo) { - return null - } - - return ( - - {edges.map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: 'collection', - styleType: 'small_cover', - contentType: 'article', - location: i, - }) - } - /> - - ))} - - ) -} - -export default CollectionList diff --git a/src/views/ArticleDetail/Collection/EditButton.tsx b/src/views/ArticleDetail/Collection/EditButton.tsx deleted file mode 100644 index ceeb20cbc9..0000000000 --- a/src/views/ArticleDetail/Collection/EditButton.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import classNames from 'classnames' -import gql from 'graphql-tag' -import _uniq from 'lodash/uniq' - -import { - Button, - ButtonProps, - IconEdit, - IconPen, - IconSpinner, - TextIcon, - Translate, -} from '~/components' -import { useMutation } from '~/components/GQL' -import articleFragments from '~/components/GQL/fragments/article' - -import { ADD_TOAST } from '~/common/enums' - -import styles from './styles.css' - -import { ArticleDetailPublic_article } from '../__generated__/ArticleDetailPublic' -import { EditorSetCollection } from './__generated__/EditorSetCollection' - -/** - * Note: - * - * The response of this mutation is aligned with `COLLECTION_LIST` in `CollectionList.tsx`, - * so that it will auto update the local cache and prevent refetch logics - */ -const EDITOR_SET_COLLECTION = gql` - mutation EditorSetCollection( - $id: ID! - $after: String - $first: Int - $collection: [ID!]! - ) { - setCollection(input: { id: $id, collection: $collection }) { - ...ArticleCollection - } - } - ${articleFragments.articleCollection} -` - -const EditButton = ({ - article, - canEdit, - editing, - setEditing, - editingArticles, -}: { - article: ArticleDetailPublic_article - canEdit: boolean - editing: boolean - setEditing: any - editingArticles: string[] -}) => { - const [setCollection, { loading }] = useMutation( - EDITOR_SET_COLLECTION - ) - const onSave = async () => { - try { - await setCollection({ - variables: { - id: article.id, - collection: _uniq(editingArticles.map((item: any) => item.id)), - first: null, - }, - }) - - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'green', - content: , - duration: 2000, - }, - }) - ) - } catch (error) { - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'red', - content: , - clostButton: true, - duration: 2000, - }, - }) - ) - } - setEditing(false) - } - const editButtonClass = classNames({ - 'edit-button': true, - }) - - const buttonProps = { - size: ['4rem', '1.25rem'], - bgColor: 'grey-lighter', - } as ButtonProps - - if (!editing) { - return ( - - - - - - ) - } - - return ( - - - - - - - - ) -} - -export default EditButton diff --git a/src/views/ArticleDetail/Collection/EditingList.tsx b/src/views/ArticleDetail/Collection/EditingList.tsx deleted file mode 100644 index a44b2190e6..0000000000 --- a/src/views/ArticleDetail/Collection/EditingList.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' -import _uniqBy from 'lodash/uniqBy' -import dynamic from 'next/dynamic' -import { useEffect } from 'react' - -import { Spinner } from '~/components' -import { QueryError } from '~/components/GQL' -import articleFragments from '~/components/GQL/fragments/article' - -import styles from './styles.css' - -import { ArticleDetailPublic_article } from '../__generated__/ArticleDetailPublic' -import { - EditorCollection, - EditorCollection_article_collection_edges_node, -} from './__generated__/EditorCollection' - -const EDITOR_COLLECTION = gql` - query EditorCollection($mediaHash: String) { - article(input: { mediaHash: $mediaHash }) { - ...EditorCollection - } - } - ${articleFragments.editorCollection} -` - -const CollectionEditor = dynamic( - () => import('~/components/CollectionEditor'), - { - ssr: false, - loading: Spinner, - } -) - -const EditingList = ({ - article, - editingArticles, - setEditingArticles, -}: { - article: ArticleDetailPublic_article - editingArticles: EditorCollection_article_collection_edges_node[] - setEditingArticles: ( - articles: EditorCollection_article_collection_edges_node[] - ) => void -}) => { - const { data, loading, error } = useQuery( - EDITOR_COLLECTION, - { - variables: { mediaHash: article.mediaHash }, - fetchPolicy: 'no-cache', - } - ) - const edges = data?.article?.collection.edges || [] - - // init `editingArticles` when network collection is received - const edgesKeys = edges.map(({ node }) => node.id).join(',') || '' - useEffect(() => { - setEditingArticles(edges.map(({ node }) => node)) - }, [edgesKeys]) - - if (loading) { - return - } - - if (error) { - return - } - - return ( -
- setEditingArticles(_uniqBy(articles, 'id'))} - /> - - -
- ) -} - -export default EditingList diff --git a/src/views/ArticleDetail/Collection/index.tsx b/src/views/ArticleDetail/Collection/index.tsx index 31e3818411..4272d9430f 100644 --- a/src/views/ArticleDetail/Collection/index.tsx +++ b/src/views/ArticleDetail/Collection/index.tsx @@ -1,16 +1,21 @@ import { useQuery } from '@apollo/react-hooks' import gql from 'graphql-tag' import _uniq from 'lodash/uniq' -import { useContext, useState } from 'react' -import { Title, Translate, ViewerContext, ViewMoreButton } from '~/components' +import { + ArticleDigestSidebar, + List, + Spinner, + Title, + Translate, + useResponsive, + ViewMoreButton, +} from '~/components' +import { QueryError } from '~/components/GQL' import articleFragments from '~/components/GQL/fragments/article' -import { mergeConnections } from '~/common/utils' +import { analytics, mergeConnections } from '~/common/utils' -import CollectionList from './CollectionList' -import EditButton from './EditButton' -import EditingList from './EditingList' import styles from './styles.css' import { ArticleDetailPublic_article } from '../__generated__/ArticleDetailPublic' @@ -30,17 +35,13 @@ const Collection: React.FC<{ article: ArticleDetailPublic_article collectionCount?: number }> = ({ article, collectionCount }) => { - const viewer = useContext(ViewerContext) - - const [editing, setEditing] = useState(false) - const [editingArticles, setEditingArticles] = useState([]) - + const isMediumUp = useResponsive('md-up') const { data, loading, error, fetchMore } = useQuery( COLLECTION_LIST, { variables: { mediaHash: article.mediaHash, first: 3 } } ) const connectionPath = 'article.collection' - const { pageInfo } = data?.article?.collection || {} + const { edges, pageInfo } = data?.article?.collection || {} const loadAll = () => fetchMore({ variables: { @@ -56,8 +57,17 @@ const Collection: React.FC<{ }), }) - const isAuthor = viewer.id === article.author.id - const canEdit = isAuthor && !viewer.isInactive + if (loading) { + return + } + + if (error) { + return + } + + if (!edges || !pageInfo) { + return null + } return (
@@ -69,35 +79,29 @@ const Collection: React.FC<{ {collectionCount} - -
- {isAuthor && ( - - )} -
- {!editing && ( - - )} - - {editing && ( - - )} + + {edges.map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: 'collection', + styleType: 'small_cover', + contentType: 'article', + location: i, + }) + } + /> + + ))} + - {!editing && pageInfo?.hasNextPage && ( - - )} + {pageInfo?.hasNextPage && }
diff --git a/src/views/ArticleDetail/Collection/styles.css b/src/views/ArticleDetail/Collection/styles.css index 52066a8798..77d283d592 100644 --- a/src/views/ArticleDetail/Collection/styles.css +++ b/src/views/ArticleDetail/Collection/styles.css @@ -1,10 +1,3 @@ -:root { - --collection-list-number-width: 1.25rem; - --collection-list-spacing: calc( - var(--collection-list-number-width) + var(--spacing-x-tight) - ); -} - .collection { margin: 0 0 var(--spacing-loose); @@ -27,21 +20,3 @@ font-weight: var(--font-weight-bold); color: var(--color-matters-green); } - -.edit-button { - line-height: 1; - - & :global(> * + *) { - margin-left: var(--spacing-x-tight); - } -} - -.editing-list { - @mixin max-height-scroll; - - padding: 0 var(--spacing-x-tight); - - @media (--lg-up) { - padding: 0 var(--spacing-loose); - } -} diff --git a/src/views/ArticleDetail/Content/index.tsx b/src/views/ArticleDetail/Content/index.tsx index 7660721ff4..3cd8568e4c 100644 --- a/src/views/ArticleDetail/Content/index.tsx +++ b/src/views/ArticleDetail/Content/index.tsx @@ -31,7 +31,7 @@ const Content = ({ }: { article: ContentArticle translation?: string | null - translating: boolean + translating?: boolean }) => { const [read] = useMutation(READ_ARTICLE) @@ -56,41 +56,46 @@ const Content = ({ // register read useEffect(() => { - const timerId = setInterval(() => { - const isReading = () => { - // tab hidden - if (document.hidden) { - return false + const timerId = setInterval( + (function heartbeat() { + const isReading = () => { + // tab hidden + if (document.hidden) { + return false + } + + // idle for more than 5 minutes + if (Date.now() / 1000 - lastScroll > 60 * 5) { + return false + } + + if (!contentContainer || !contentContainer.current) { + return false + } + + // if overlay is shown + const overlaySelectors = ['reach-portal', '.tippy-popper'] + if (document.querySelector(overlaySelectors.join(','))) { + return false + } + + // if bottom is above center + const { + bottom, + } = ((contentContainer.current as unknown) as Element).getBoundingClientRect() + + const isBottomAboveCenter = bottom <= window.innerHeight / 2 + return !isBottomAboveCenter } - // idle for more than 5 minutes - if (Date.now() / 1000 - lastScroll > 60 * 5) { - return false + if (isReading()) { + read({ variables: { id } }) } - if (!contentContainer || !contentContainer.current) { - return false - } - - // if overlay is shown - const overlaySelectors = ['reach-portal', '.tippy-popper'] - if (document.querySelector(overlaySelectors.join(','))) { - return false - } - - // if bottom is above center - const { - bottom, - } = ((contentContainer.current as unknown) as Element).getBoundingClientRect() - - const isBottomAboveCenter = bottom <= window.innerHeight / 2 - return !isBottomAboveCenter - } - - if (isReading()) { - read({ variables: { id } }) - } - }, 5000) + return heartbeat + })(), + 5000 + ) // clean timer return () => { diff --git a/src/views/ArticleDetail/EditMode/Header/index.tsx b/src/views/ArticleDetail/EditMode/Header/index.tsx new file mode 100644 index 0000000000..1142b36758 --- /dev/null +++ b/src/views/ArticleDetail/EditMode/Header/index.tsx @@ -0,0 +1,113 @@ +import gql from 'graphql-tag' + +import { Button, IconSpinner, Tag, TextIcon, Translate } from '~/components' +import { useMutation } from '~/components/GQL' +import articleFragments from '~/components/GQL/fragments/article' + +import { ADD_TOAST } from '~/common/enums' + +import styles from './styles.css' + +import { ArticleDigestDropdownArticle } from '~/components/ArticleDigest/Dropdown/__generated__/ArticleDigestDropdownArticle' +import { EditArticle } from './__generated__/EditArticle' + +interface EditModeHeaderProps { + id: string + mediaHash: string + editModeTags: string[] + editModeCollection: ArticleDigestDropdownArticle[] + onEditSaved: () => any +} + +/** + * Note: + * + * The response of this mutation is aligned with `COLLECTION_LIST` in `CollectionList.tsx`, + * so that it will auto update the local cache and prevent refetch logics + */ +const EDIT_ARTICLE = gql` + mutation EditArticle( + $id: ID! + $mediaHash: String! + $tags: [String!] + $collection: [ID!] + $after: String + $first: Int = null + ) { + editArticle(input: { id: $id, tags: $tags, collection: $collection }) { + id + tags { + ...DigestTag + selected(input: { mediaHash: $mediaHash }) + } + ...ArticleCollection + } + } + ${Tag.fragments.tag} + ${articleFragments.articleCollection} +` + +const EditModeHeader = ({ + id, + mediaHash, + editModeTags, + editModeCollection, + onEditSaved, +}: EditModeHeaderProps) => { + const [editArticle, { loading }] = useMutation(EDIT_ARTICLE) + + const onSave = async () => { + try { + await editArticle({ + variables: { + id, + mediaHash, + tags: editModeTags, + collection: editModeCollection.map(({ id: articleId }) => articleId), + first: null, + }, + }) + onEditSaved() + } catch (e) { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: , + }, + }) + ) + } + } + + return ( + <> +

+ +

+ + + + + + ) +} + +export default EditModeHeader diff --git a/src/views/ArticleDetail/EditMode/Header/styles.css b/src/views/ArticleDetail/EditMode/Header/styles.css new file mode 100644 index 0000000000..d65cfeaaca --- /dev/null +++ b/src/views/ArticleDetail/EditMode/Header/styles.css @@ -0,0 +1,5 @@ +p { + margin-right: var(--spacing-base); + font-size: var(--font-size-sm); + color: var(--color-grey-darker); +} diff --git a/src/views/ArticleDetail/EditMode/Sidebar/index.tsx b/src/views/ArticleDetail/EditMode/Sidebar/index.tsx new file mode 100644 index 0000000000..fb432ca576 --- /dev/null +++ b/src/views/ArticleDetail/EditMode/Sidebar/index.tsx @@ -0,0 +1,94 @@ +import { useQuery } from '@apollo/react-hooks' +import gql from 'graphql-tag' +import _uniq from 'lodash/uniq' +import { useEffect } from 'react' + +import { Spinner, Tag } from '~/components' +import SidebarCollection from '~/components/Editor/Sidebar/Collection' +import SidebarTags from '~/components/Editor/Sidebar/Tags' +import { QueryError } from '~/components/GQL' +import articleFragments from '~/components/GQL/fragments/article' + +import { ArticleDigestDropdownArticle } from '~/components/ArticleDigest/Dropdown/__generated__/ArticleDigestDropdownArticle' +import { EditModeArticle } from './__generated__/EditModeArticle' + +interface EditModeSidebarProps { + mediaHash: string + editModeTags: string[] + setEditModeTags: (tags: string[]) => any + editModeCollection: ArticleDigestDropdownArticle[] + setEditModeCollection: (articles: ArticleDigestDropdownArticle[]) => any +} + +const EDIT_MODE_ARTICLE = gql` + query EditModeArticle( + $mediaHash: String! + $after: String + $first: Int = null + ) { + article(input: { mediaHash: $mediaHash }) { + id + tags { + ...DigestTag + } + ...ArticleCollection + } + } + ${Tag.fragments.tag} + ${articleFragments.articleCollection} +` + +const EditModeSidebar = ({ + mediaHash, + editModeTags, + setEditModeTags, + editModeCollection, + setEditModeCollection, +}: EditModeSidebarProps) => { + const { data, loading, error } = useQuery( + EDIT_MODE_ARTICLE, + { + variables: { mediaHash }, + } + ) + const article = data?.article + const tags = article?.tags?.map(({ content }) => content) || [] + const collection = article?.collection.edges?.map(({ node }) => node) || [] + + useEffect(() => { + setEditModeTags(tags) + setEditModeCollection(collection) + }, [data?.article?.id]) + + if (loading) { + return + } + + if (error) { + return + } + + if (!article) { + return null + } + + return ( + <> + 0 ? editModeTags : tags} + onAddTag={(tag) => setEditModeTags(_uniq(editModeTags.concat(tag)))} + onDeleteTag={(tag) => + setEditModeTags(editModeTags.filter((it) => it !== tag)) + } + /> + 0 ? editModeCollection : collection + } + onEdit={setEditModeCollection} + /> + + ) +} + +export default EditModeSidebar diff --git a/src/views/ArticleDetail/EditMode/index.tsx b/src/views/ArticleDetail/EditMode/index.tsx new file mode 100644 index 0000000000..7d2f502289 --- /dev/null +++ b/src/views/ArticleDetail/EditMode/index.tsx @@ -0,0 +1,7 @@ +import Header from './Header' +import Sidebar from './Sidebar' + +export default { + Header, + Sidebar, +} diff --git a/src/views/ArticleDetail/Responses/FeaturedComments/index.tsx b/src/views/ArticleDetail/Responses/FeaturedComments/index.tsx index bb23d38155..66f615fd66 100644 --- a/src/views/ArticleDetail/Responses/FeaturedComments/index.tsx +++ b/src/views/ArticleDetail/Responses/FeaturedComments/index.tsx @@ -37,7 +37,7 @@ const FeaturedComments = () => { * Data Fetching */ // public data - const { data, loading, fetchMore, refetch, client } = useQuery< + const { data, loading, fetchMore, refetch: refetchPublic, client } = useQuery< FeaturedCommentsPublic >(FEATURED_COMMENTS_PUBLIC, { variables: { mediaHash }, @@ -94,6 +94,11 @@ const FeaturedComments = () => { loadPrivate(newData) } + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } usePullToRefresh.Handler(refetch) if (loading && !data) { diff --git a/src/views/ArticleDetail/Responses/LatestResponses/index.tsx b/src/views/ArticleDetail/Responses/LatestResponses/index.tsx index 99d51c1c1d..b217c4678c 100644 --- a/src/views/ArticleDetail/Responses/LatestResponses/index.tsx +++ b/src/views/ArticleDetail/Responses/LatestResponses/index.tsx @@ -75,9 +75,14 @@ const LatestResponses = () => { * Data Fetching */ // public data - const { data, loading, error, fetchMore, refetch, client } = useQuery< - LatestResponsesPublic - >(LATEST_RESPONSES_PUBLIC, { + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(LATEST_RESPONSES_PUBLIC, { variables: { mediaHash, first: RESPONSES_COUNT, @@ -145,7 +150,11 @@ const LatestResponses = () => { loadPrivate(newData) } - // refetch when comment is sent or pull down + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } useEventListener(REFETCH_RESPONSES, refetch) usePullToRefresh.Handler(refetch) diff --git a/src/views/ArticleDetail/Toolbar/index.tsx b/src/views/ArticleDetail/Toolbar/index.tsx index a501c83324..b5f77471b9 100644 --- a/src/views/ArticleDetail/Toolbar/index.tsx +++ b/src/views/ArticleDetail/Toolbar/index.tsx @@ -6,7 +6,9 @@ import { ShareButton, useResponsive, } from '~/components' -import DropdownActions from '~/components/ArticleDigest/DropdownActions' +import DropdownActions, { + DropdownActionsControls, +} from '~/components/ArticleDigest/DropdownActions' import AppreciationButton from '../AppreciationButton' import Appreciators from './Appreciators' @@ -16,9 +18,9 @@ import styles from './styles.css' import { ToolbarArticlePrivate } from './__generated__/ToolbarArticlePrivate' import { ToolbarArticlePublic } from './__generated__/ToolbarArticlePublic' -export interface ToolbarProps { +export type ToolbarProps = { article: ToolbarArticlePublic & Partial -} +} & DropdownActionsControls const fragments = { article: { @@ -49,7 +51,7 @@ const fragments = { }, } -const Toolbar = ({ article }: ToolbarProps) => { +const Toolbar = ({ article, editArticle }: ToolbarProps) => { const isSmallUp = useResponsive('sm-up') return ( @@ -73,6 +75,7 @@ const Toolbar = ({ article }: ToolbarProps) => { color="black" size="md-s" inCard={false} + editArticle={editArticle} /> diff --git a/src/views/ArticleDetail/gql.ts b/src/views/ArticleDetail/gql.ts index c6e19fd83f..53bc94cb6f 100644 --- a/src/views/ArticleDetail/gql.ts +++ b/src/views/ArticleDetail/gql.ts @@ -50,9 +50,10 @@ export const ARTICLE_DETAIL_PUBLIC = gql` ` export const ARTICLE_DETAIL_PRIVATE = gql` - query ArticleDetailPrivate($mediaHash: String) { + query ArticleDetailPrivate($mediaHash: String, $includeContent: Boolean!) { article(input: { mediaHash: $mediaHash }) { id + content @include(if: $includeContent) author { ...UserDigestRichUserPrivate } diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index 94a27a42ec..962c0186c0 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -29,6 +29,7 @@ import { getQuery } from '~/common/utils' import Collection from './Collection' import Content from './Content' +import EditMode from './EditMode' import FingerprintButton from './FingerprintButton' import { ARTICLE_DETAIL_PRIVATE, @@ -43,6 +44,7 @@ import Toolbar from './Toolbar' import TranslationButton from './TranslationButton' import Wall from './Wall' +import { ArticleDigestDropdownArticle } from '~/components/ArticleDigest/Dropdown/__generated__/ArticleDigestDropdownArticle' import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' import { ArticleDetailPublic } from './__generated__/ArticleDetailPublic' import { ArticleTranslation } from './__generated__/ArticleTranslation' @@ -84,21 +86,21 @@ const ArticleDetail = () => { const shouldShowWall = !viewer.isAuthed && wall // public data - const { data, loading, error, client } = useQuery( - ARTICLE_DETAIL_PUBLIC, - { - variables: { mediaHash }, - } - ) + const { data, loading, error, client, refetch: refetchPublic } = useQuery< + ArticleDetailPublic + >(ARTICLE_DETAIL_PUBLIC, { + variables: { mediaHash }, + }) const article = data?.article const authorId = article?.author?.id const collectionCount = article?.collection?.totalCount || 0 const isAuthor = viewer.id === authorId + const canEdit = isAuthor && !viewer.isInactive // fetch private data - useEffect(() => { - if (!viewer.id || !article) { + const loadPrivate = () => { + if (!viewer.id || !article || !article?.mediaHash) { return } @@ -106,10 +108,15 @@ const ArticleDetail = () => { query: ARTICLE_DETAIL_PRIVATE, fetchPolicy: 'network-only', variables: { - mediaHash, + mediaHash: article?.mediaHash, + includeContent: article.state !== 'active' && isAuthor, }, }) - }, [mediaHash, viewer.id, article]) + } + + useEffect(() => { + loadPrivate() + }, [article?.mediaHash, viewer.id]) // translation const [translate, setTranslate] = useState(false) @@ -149,15 +156,27 @@ const ArticleDetail = () => { ) } - /** - * Render - */ + // edit mode + const [editMode, setEditMode] = useState(false) + const [editModeTags, setEditModeTags] = useState([]) + const [editModeCollection, setEditModeCollection] = useState< + ArticleDigestDropdownArticle[] + >([]) + const onEditSaved = async () => { + setEditMode(false) + await refetchPublic() + loadPrivate() + } + useEffect(() => { if (shouldShowWall && window.location.hash && article) { jump('#comments', { offset: -10 }) } }, [mediaHash]) + /** + * Render:Loading + */ if (loading) { return ( @@ -166,6 +185,9 @@ const ArticleDetail = () => { ) } + /** + * Render:Error + */ if (error) { return ( @@ -174,6 +196,9 @@ const ArticleDetail = () => { ) } + /** + * Render:404 + */ if (!article) { return ( @@ -182,7 +207,10 @@ const ArticleDetail = () => { ) } - if (article.state !== 'active' && viewer.id !== authorId) { + /** + * Render:Archived/Banned + */ + if (article.state !== 'active' && !isAuthor) { return ( { ) } + /** + * Render:Edit Mode + */ + if (editMode) { + return ( + + } + keepAside + > + + } + /> + +
+
+ + {translate && titleTranslation ? titleTranslation : article.title} + +
+ + +
+ + +
+ ) + } + + /** + * Render + */ return ( }> { }} /> */} - {(collectionCount > 0 || isAuthor) && ( + {collectionCount > 0 && (
@@ -298,7 +373,17 @@ const ArticleDetail = () => { )} - + { + setEditMode(true) + jump(document.body) + } + : undefined + } + /> {shouldShowWall && ( <> diff --git a/src/views/ArticleDetail/styles.css b/src/views/ArticleDetail/styles.css index 87d1073fae..1b1f98fe8e 100644 --- a/src/views/ArticleDetail/styles.css +++ b/src/views/ArticleDetail/styles.css @@ -1,6 +1,11 @@ .content { min-height: 100vh; padding: var(--spacing-base); + + &.editing { + cursor: not-allowed; + opacity: 0.3; + } } .title { diff --git a/src/views/ArticleFeed/index.tsx b/src/views/ArticleFeed/index.tsx deleted file mode 100644 index 3d20f92150..0000000000 --- a/src/views/ArticleFeed/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' - -import { - ArticleDigestFeed, - EmptyArticle, - Head, - InfiniteScroll, - Layout, - List, - Spinner, -} from '~/components' -import { QueryError } from '~/components/GQL' - -import { analytics, mergeConnections } from '~/common/utils' - -import { AllIcymis } from './__generated__/AllIcymis' -import { AllTopics } from './__generated__/AllTopics' - -interface ArticleFeedProp { - type?: 'icymi' | 'topic' -} - -const QUERIES = { - topic: gql` - query AllTopics($after: String) { - viewer { - id - recommendation { - articles: topics(input: { first: 10, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ...ArticleDigestFeedArticle - } - } - } - } - } - } - ${ArticleDigestFeed.fragments.article} - `, - icymi: gql` - query AllIcymis($after: String) { - viewer { - id - recommendation { - articles: icymi(input: { first: 10, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ...ArticleDigestFeedArticle - } - } - } - } - } - } - ${ArticleDigestFeed.fragments.article} - `, -} - -const Feed = ({ type = 'topic' }: ArticleFeedProp) => { - const { data, loading, error, fetchMore, refetch } = useQuery< - AllTopics | AllIcymis - >(QUERIES[type]) - - if (loading) { - return - } - - if (error) { - return - } - - const connectionPath = 'viewer.recommendation.articles' - const { edges, pageInfo } = data?.viewer?.recommendation.articles || {} - - if (!edges || edges.length <= 0 || !pageInfo) { - return - } - - const loadMore = () => { - analytics.trackEvent('load_more', { - type: type === 'topic' ? 'all_topics' : 'all_icymi', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - - return ( - - - {edges.map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: type === 'topic' ? 'all_topics' : 'all_icymi', - contentType: 'article', - styleType: 'small_cover', - location: i, - }) - } - /> - - ))} - - - ) -} - -export default ({ type = 'topic' }: ArticleFeedProp) => ( - - } - right={ - - } - /> - - - - - -) diff --git a/src/views/Authors/gql.ts b/src/views/Authors/gql.ts new file mode 100644 index 0000000000..90ad81a3d8 --- /dev/null +++ b/src/views/Authors/gql.ts @@ -0,0 +1,41 @@ +import gql from 'graphql-tag' + +import { UserDigest } from '~/components' + +export const ALL_AUTHORS_PUBLIC = gql` + query AllAuthorsPublic($after: String) { + viewer @connection(key: "viewerAuthors") { + id + recommendation { + authors(input: { first: 20, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ...UserDigestRichUserPublic + ...UserDigestRichUserPrivate + } + } + } + } + } + } + ${UserDigest.Rich.fragments.user.public} + ${UserDigest.Rich.fragments.user.private} +` + +export const ALL_AUTHORS_PRIVATE = gql` + query AllAuthorsPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on User { + ...UserDigestRichUserPrivate + } + } + } + ${UserDigest.Rich.fragments.user.private} +` diff --git a/src/views/Authors/index.tsx b/src/views/Authors/index.tsx index 8b48481dc2..707dd33173 100644 --- a/src/views/Authors/index.tsx +++ b/src/views/Authors/index.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' +import { useContext, useEffect } from 'react' import { EmptyWarning, @@ -10,71 +10,67 @@ import { Spinner, Translate, UserDigest, + ViewerContext, } from '~/components' import { QueryError } from '~/components/GQL' import { analytics, mergeConnections } from '~/common/utils' -import { AllAuthors } from './__generated__/AllAuthors' - -const ALL_AUTHORSS = gql` - query AllAuthors($after: String) { - viewer { - id - recommendation { - authors(input: { first: 20, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ...UserDigestRichUserPublic - ...UserDigestRichUserPrivate - } - } - } - } - } - } - ${UserDigest.Rich.fragments.user.public} - ${UserDigest.Rich.fragments.user.private} -` +import { ALL_AUTHORS_PRIVATE, ALL_AUTHORS_PUBLIC } from './gql' -const Authors = () => { - const { data, loading, error, fetchMore, refetch } = useQuery( - ALL_AUTHORSS - ) +import { AllAuthorsPublic } from './__generated__/AllAuthorsPublic' - if (loading) { - return - } +const Authors = () => { + const viewer = useContext(ViewerContext) - if (error) { - return - } + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(ALL_AUTHORS_PUBLIC) + // pagination const connectionPath = 'viewer.recommendation.authors' const { edges, pageInfo } = data?.viewer?.recommendation.authors || {} - if (!edges || edges.length <= 0 || !pageInfo) { - return ( - } - /> - ) + // private data + const loadPrivate = (publicData?: AllAuthorsPublic) => { + if (!viewer.id || !publicData) { + return + } + + const publiceEdges = publicData?.viewer?.recommendation.authors.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: ALL_AUTHORS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) } - const loadMore = () => { + // fetch private data for first page + useEffect(() => { + loadPrivate(data) + }, [!!edges, viewer.id]) + + // load next page + const loadMore = async () => { analytics.trackEvent('load_more', { type: 'all_authors', - location: edges.length, + location: edges?.length || 0, }) - return fetchMore({ + + const { data: newData } = await fetchMore({ variables: { - after: pageInfo.endCursor, + after: pageInfo?.endCursor, }, updateQuery: (previousResult, { fetchMoreResult }) => mergeConnections({ @@ -84,6 +80,33 @@ const Authors = () => { dedupe: true, }), }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + + /** + * Render + */ + if (loading) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } + /> + ) } return ( diff --git a/src/views/Follow/FollowFeed/ArticlesFeed/index.tsx b/src/views/Follow/FollowFeed/ArticlesFeed/index.tsx new file mode 100644 index 0000000000..34145f5c99 --- /dev/null +++ b/src/views/Follow/FollowFeed/ArticlesFeed/index.tsx @@ -0,0 +1,113 @@ +import { useQuery } from '@apollo/react-hooks' +import gql from 'graphql-tag' + +import { + ArticleDigestFeed, + EmptyArticle, + InfiniteScroll, + List, + Spinner, +} from '~/components' +import { QueryError } from '~/components/GQL' + +import { analytics, mergeConnections } from '~/common/utils' + +import { FollowArticlesFeed } from './__generated__/FollowArticlesFeed' + +const FOLLOW_ARTICLES = gql` + query FollowArticlesFeed($after: String) { + viewer { + id + recommendation { + followeeArticles(input: { first: 10, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + __typename + ... on Article { + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate + } + } + } + } + } + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +const ArticlesFeed = () => { + const { data, loading, error, fetchMore, refetch } = useQuery< + FollowArticlesFeed + >(FOLLOW_ARTICLES) + + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.recommendation.followeeArticles' + const { edges, pageInfo } = + data?.viewer?.recommendation.followeeArticles || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent('load_more', { + type: 'follow', + location: edges.length, + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + } + + return ( + + + {edges.map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: 'follow-article', + contentType: 'article', + styleType: 'no_cover', + location: i, + }) + } + inFollowFeed + /> + + ))} + + + ) +} + +export default ArticlesFeed diff --git a/src/views/Follow/FollowFeed/CommentsFeed/index.tsx b/src/views/Follow/FollowFeed/CommentsFeed/index.tsx new file mode 100644 index 0000000000..0921aa0062 --- /dev/null +++ b/src/views/Follow/FollowFeed/CommentsFeed/index.tsx @@ -0,0 +1,106 @@ +import { useQuery } from '@apollo/react-hooks' +import gql from 'graphql-tag' + +import { EmptyArticle, InfiniteScroll, List, Spinner } from '~/components' +import { QueryError } from '~/components/GQL' + +import { analytics, mergeConnections } from '~/common/utils' + +import FollowComment from '../FollowComment' + +import { FollowCommentsFeed } from './__generated__/FollowCommentsFeed' + +const FOLLOW_COMMENTS = gql` + query FollowCommentsFeed($after: String) { + viewer { + id + recommendation { + followeeComments(input: { first: 10, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + __typename + ... on Comment { + ...FollowComment + } + } + } + } + } + } + } + ${FollowComment.fragments.comment} +` + +const CommentsFeed = () => { + const { data, loading, error, fetchMore, refetch } = useQuery< + FollowCommentsFeed + >(FOLLOW_COMMENTS) + + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.recommendation.followeeComments' + const { edges, pageInfo } = + data?.viewer?.recommendation.followeeComments || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent('load_more', { + type: 'follow', + location: edges.length, + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + } + + return ( + + + {edges.map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: 'follow-comment', + contentType: 'comment', + styleType: 'card', + location: i, + }) + } + /> + + ))} + + + ) +} + +export default CommentsFeed diff --git a/src/views/Follow/FollowFeed/FeedType/index.tsx b/src/views/Follow/FollowFeed/FeedType/index.tsx index cdefea6164..74a5c32ecd 100644 --- a/src/views/Follow/FollowFeed/FeedType/index.tsx +++ b/src/views/Follow/FollowFeed/FeedType/index.tsx @@ -1,6 +1,6 @@ import { Tabs, Translate } from '~/components' -export type FollowFeedType = 'article' | 'comment' +export type FollowFeedType = 'article' | 'comment' | 'tag' interface FeedTypeProps { type: FollowFeedType @@ -10,6 +10,7 @@ interface FeedTypeProps { const FeedType: React.FC = ({ type, setFeedType }) => { const isArticle = type === 'article' const isComment = type === 'comment' + const isTag = type === 'tag' return ( @@ -20,6 +21,10 @@ const FeedType: React.FC = ({ type, setFeedType }) => { setFeedType('comment')} selected={isComment}> + + setFeedType('tag')} selected={isTag}> + + ) } diff --git a/src/views/Follow/FollowFeed/TagsFeed/index.tsx b/src/views/Follow/FollowFeed/TagsFeed/index.tsx new file mode 100644 index 0000000000..7d91a7e386 --- /dev/null +++ b/src/views/Follow/FollowFeed/TagsFeed/index.tsx @@ -0,0 +1,199 @@ +import { useQuery } from '@apollo/react-hooks' +import gql from 'graphql-tag' +import _find from 'lodash/find' +import _intersection from 'lodash/intersection' +import _reverse from 'lodash/reverse' +import _sortBy from 'lodash/sortBy' + +import { + ArticleDigestFeed, + EmptyFollowingTag, + InfiniteScroll, + List, + Spinner, + Tag, + Translate, +} from '~/components' +import { QueryError } from '~/components/GQL' + +import { analytics, mergeConnections } from '~/common/utils' + +import styles from './styles.css' + +import { + FollowingTagsArticlesFeed, + FollowingTagsArticlesFeed_viewer_recommendation_followingTagsArticles_edges_node as FollowingTagsArticlesFeedNode, +} from './__generated__/FollowingTagsArticlesFeed' +import { FollowingTagsFeed } from './__generated__/FollowingTagsFeed' + +const FOLLOWING_TAGS = gql` + query FollowingTagsFeed { + viewer { + id + recommendation { + followingTags(input: { first: null }) { + edges { + node { + ... on Tag { + id + } + } + } + } + } + } + } +` + +const FOLLOWING_TAGS_ARTICLES = gql` + query FollowingTagsArticlesFeed($after: String) { + viewer { + id + recommendation { + followingTagsArticles(input: { first: 10, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + __typename + ... on Article { + tags { + ...DigestTag + createdAt + } + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate + } + } + } + } + } + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} + ${Tag.fragments.tag} +` + +const TagsArticles = ({ tagIds }: { tagIds: string[] }) => { + const { data, loading, error, fetchMore, refetch } = useQuery< + FollowingTagsArticlesFeed + >(FOLLOWING_TAGS_ARTICLES) + + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.recommendation.followingTagsArticles' + const { edges, pageInfo } = + data?.viewer?.recommendation.followingTagsArticles || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent('load_more', { + type: 'follow', + location: edges.length, + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + } + + const TagComponent = ({ node }: { node: FollowingTagsArticlesFeedNode }) => { + if (!node || !node.tags || node.tags.length <= 0) { + return null + } + + const tags = _sortBy(node?.tags || [], ['createdAt']) + const matches = _intersection( + tags.map(({ id }) => id), + tagIds + ) + if (!matches || matches.length <= 0) { + return null + } + const tag = _find(tags, { id: matches[0] }) + if (!tag) { + return null + } + return ( +
+ + + + + +
+ ) + } + + return ( + + + {edges.map(({ node, cursor }, i) => ( + + + + analytics.trackEvent('click_feed', { + type: 'follow-article', + contentType: 'article', + styleType: 'no_cover', + location: i, + }) + } + /> + + ))} + + + ) +} + +const TagsFeed = () => { + const { data, loading, error } = useQuery(FOLLOWING_TAGS) + + if (loading) { + return + } + + if (error) { + return + } + + const { edges } = data?.viewer?.recommendation.followingTags || {} + + if (!edges || edges.length <= 0) { + return + } + + const tagIds = edges.map(({ node }) => node.id) + + return +} + +export default TagsFeed diff --git a/src/views/Follow/FollowFeed/TagsFeed/styles.css b/src/views/Follow/FollowFeed/TagsFeed/styles.css new file mode 100644 index 0000000000..fb78df594b --- /dev/null +++ b/src/views/Follow/FollowFeed/TagsFeed/styles.css @@ -0,0 +1,12 @@ +.tag { + @mixin flex-center-start; + + width: 100%; + margin: var(--spacing-base) 0 var(--spacing-xx-tight) var(--spacing-base); + + & span { + margin-left: var(--spacing-x-tight); + font-size: var(--font-size-sm); + color: var(--color-grey); + } +} diff --git a/src/views/Follow/FollowFeed/index.tsx b/src/views/Follow/FollowFeed/index.tsx index 7de40bc854..3b4ddd5242 100644 --- a/src/views/Follow/FollowFeed/index.tsx +++ b/src/views/Follow/FollowFeed/index.tsx @@ -1,213 +1,14 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' -import { - ArticleDigestFeed, - EmptyArticle, - Head, - InfiniteScroll, - List, - Spinner, -} from '~/components' -import { QueryError } from '~/components/GQL' +import { Head } from '~/components' import CLIENT_PREFERENCE from '~/components/GQL/queries/clientPreference' -import { analytics, mergeConnections } from '~/common/utils' - +import ArticlesFeed from './ArticlesFeed' +import CommentsFeed from './CommentsFeed' import FeedType, { FollowFeedType } from './FeedType' -import FollowComment from './FollowComment' +import TagsFeed from './TagsFeed' import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' -import { FollowArticleFeed } from './__generated__/FollowArticleFeed' -import { FollowCommentFeed } from './__generated__/FollowCommentFeed' - -const queries = { - article: gql` - query FollowArticleFeed($after: String) { - viewer { - id - recommendation { - followeeArticles(input: { first: 10, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - __typename - ... on Article { - ...ArticleDigestFeedArticle - } - } - } - } - } - } - } - ${ArticleDigestFeed.fragments.article} - `, - comment: gql` - query FollowCommentFeed($after: String) { - viewer { - id - recommendation { - followeeComments(input: { first: 10, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - __typename - ... on Comment { - ...FollowComment - } - } - } - } - } - } - } - ${FollowComment.fragments.comment} - `, -} - -const ArticlesFeed = () => { - const { data, loading, error, fetchMore, refetch } = useQuery< - FollowArticleFeed - >(queries.article) - - if (loading) { - return - } - - if (error) { - return - } - - const connectionPath = 'viewer.recommendation.followeeArticles' - const { edges, pageInfo } = - data?.viewer?.recommendation.followeeArticles || {} - - if (!edges || edges.length <= 0 || !pageInfo) { - return - } - - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'follow', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - - return ( - - - {edges.map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: 'follow-article', - contentType: 'article', - styleType: 'no_cover', - location: i, - }) - } - inFollowFeed - /> - - ))} - - - ) -} - -const CommentsFeed = () => { - const { data, loading, error, fetchMore, refetch } = useQuery< - FollowCommentFeed - >(queries.comment) - - if (loading) { - return - } - - if (error) { - return - } - - const connectionPath = 'viewer.recommendation.followeeComments' - const { edges, pageInfo } = - data?.viewer?.recommendation.followeeComments || {} - - if (!edges || edges.length <= 0 || !pageInfo) { - return - } - - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'follow', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - - return ( - - - {edges.map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: 'follow-comment', - contentType: 'comment', - styleType: 'card', - location: i, - }) - } - /> - - ))} - - - ) -} const FollowFeed = () => { const { data, client } = useQuery(CLIENT_PREFERENCE, { @@ -236,6 +37,7 @@ const FollowFeed = () => { {followFeedType === 'article' && } {followFeedType === 'comment' && } + {followFeedType === 'tag' && } ) } diff --git a/src/views/Follow/PickAuthors/AuthorPicker/index.tsx b/src/views/Follow/PickAuthors/AuthorPicker/index.tsx index a6b3de15dd..d773869d7d 100644 --- a/src/views/Follow/PickAuthors/AuthorPicker/index.tsx +++ b/src/views/Follow/PickAuthors/AuthorPicker/index.tsx @@ -59,7 +59,7 @@ export const AuthorPicker = ({ title }: { title: React.ReactNode }) => { size={[null, '1.25rem']} spacing={[0, 'xtight']} bgActiveColor="grey-lighter" - onClick={() => refetch()} + onClick={refetch} > } color="grey"> diff --git a/src/views/Follow/index.tsx b/src/views/Follow/index.tsx index d8ba2d5b39..3400928269 100644 --- a/src/views/Follow/index.tsx +++ b/src/views/Follow/index.tsx @@ -45,7 +45,7 @@ const BaseFollow = () => { } }, []) - if (loading) { + if (loading || !viewer.privateFetched) { return } diff --git a/src/views/Home/Feed/Authors/gql.ts b/src/views/Home/Feed/Authors/gql.ts new file mode 100644 index 0000000000..e4a4142147 --- /dev/null +++ b/src/views/Home/Feed/Authors/gql.ts @@ -0,0 +1,39 @@ +import gql from 'graphql-tag' +import _chunk from 'lodash/chunk' + +import { UserDigest } from '~/components' + +export const FEED_AUTHORS_PUBLIC = gql` + query FeedAuthorsPublic { + viewer @connection(key: "viewerFeedAuthors") { + id + recommendation { + authors( + input: { first: 9, filter: { random: true, followed: false } } + ) { + edges { + cursor + node { + ...UserDigestRichUserPublic + ...UserDigestRichUserPrivate + } + } + } + } + } + } + ${UserDigest.Rich.fragments.user.public} + ${UserDigest.Rich.fragments.user.private} +` + +export const FEED_AUTHORS_PRIVATE = gql` + query FeedAuthorsPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on User { + ...UserDigestRichUserPrivate + } + } + } + ${UserDigest.Rich.fragments.user.private} +` diff --git a/src/views/Home/Feed/Authors/index.tsx b/src/views/Home/Feed/Authors/index.tsx index f5daa1379e..d0c0857bee 100644 --- a/src/views/Home/Feed/Authors/index.tsx +++ b/src/views/Home/Feed/Authors/index.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import _chunk from 'lodash/chunk' +import { useContext, useEffect } from 'react' import { Button, @@ -10,45 +10,61 @@ import { TextIcon, Translate, UserDigest, + ViewerContext, } from '~/components' import { QueryError } from '~/components/GQL' import { analytics } from '~/common/utils' import SectionHeader from '../../SectionHeader' +import { FEED_AUTHORS_PRIVATE, FEED_AUTHORS_PUBLIC } from './gql' -import { FeedAuthors as FeedAuthorsType } from './__generated__/FeedAuthors' - -const FEED_AUTHORS = gql` - query FeedAuthors { - viewer { - id - recommendation { - authors( - input: { first: 9, filter: { random: true, followed: false } } - ) { - edges { - cursor - node { - ...UserDigestRichUserPublic - ...UserDigestRichUserPrivate - } - } - } - } - } - } - ${UserDigest.Rich.fragments.user.public} - ${UserDigest.Rich.fragments.user.private} -` +import { FeedAuthorsPublic } from './__generated__/FeedAuthorsPublic' const FeedAuthors = () => { - const { data, loading, error, refetch } = useQuery( - FEED_AUTHORS, - { notifyOnNetworkStatusChange: true } - ) + const viewer = useContext(ViewerContext) + + /** + * Data Fetching + */ + // public data + const { data, loading, error, refetch: refetchPublic, client } = useQuery< + FeedAuthorsPublic + >(FEED_AUTHORS_PUBLIC, { notifyOnNetworkStatusChange: true }) + const edges = data?.viewer?.recommendation.authors.edges + // private data + const loadPrivate = (publicData?: FeedAuthorsPublic) => { + if (!viewer.id || !publicData) { + return + } + + const publiceEdges = publicData?.viewer?.recommendation.authors.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: FEED_AUTHORS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + if (loading || !edges) { + return + } + + loadPrivate(data) + }, [!!edges, viewer.id]) + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + if (error) { return } @@ -65,7 +81,7 @@ const FeedAuthors = () => { size={[null, '1.25rem']} spacing={[0, 'xtight']} bgActiveColor="grey-lighter" - onClick={() => refetch()} + onClick={refetch} > } diff --git a/src/views/Home/Feed/Tags/index.tsx b/src/views/Home/Feed/Tags/index.tsx index 5b9d57551e..978a5b3792 100644 --- a/src/views/Home/Feed/Tags/index.tsx +++ b/src/views/Home/Feed/Tags/index.tsx @@ -9,11 +9,11 @@ import { analytics } from '~/common/utils' import SectionHeader from '../../SectionHeader' import TagFeedDigest from './TagFeedDigest' -import { FeedTags } from './__generated__/FeedTags' +import { FeedTagsPublic } from './__generated__/FeedTagsPublic' const FEED_TAGS = gql` - query FeedTags { - viewer { + query FeedTagsPublic { + viewer @connection(key: "viewerFeedTags") { id recommendation { tags(input: { first: 5 }) { @@ -31,7 +31,7 @@ const FEED_TAGS = gql` ` const TagsFeed = () => { - const { data, loading, error } = useQuery(FEED_TAGS) + const { data, loading, error } = useQuery(FEED_TAGS) const edges = data?.viewer?.recommendation.tags.edges if (error) { diff --git a/src/views/Home/Feed/gql.ts b/src/views/Home/Feed/gql.ts new file mode 100644 index 0000000000..345d0e7a57 --- /dev/null +++ b/src/views/Home/Feed/gql.ts @@ -0,0 +1,102 @@ +import gql from 'graphql-tag' + +import { ArticleDigestFeed } from '~/components' + +const feedFragment = gql` + fragment FeedArticleConnection on ArticleConnection { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate + } + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +export const FEED_ARTICLES_PUBLIC = { + hottest: gql` + query HottestFeedPublic($after: String) { + viewer @connection(key: "viewerFeedHottest") { + id + recommendation { + feed: hottest(input: { first: 10, after: $after }) { + ...FeedArticleConnection + } + } + } + } + ${feedFragment} + `, + valued: gql` + query ValuedFeedPublic($after: String) { + viewer @connection(key: "viewerFeedValued") { + id + recommendation { + feed: valued(input: { first: 10, after: $after }) { + ...FeedArticleConnection + } + } + } + } + ${feedFragment} + `, + newest: gql` + query NewestFeedPublic($after: String) { + viewer @connection(key: "viewerFeedNewest") { + id + recommendation { + feed: newest(input: { first: 10, after: $after }) { + ...FeedArticleConnection + } + } + } + } + ${feedFragment} + `, + icymi: gql` + query IcymiFeedPublic($after: String) { + viewer @connection(key: "viewerFeedIcymi") { + id + recommendation { + feed: icymi(input: { first: 10, after: $after }) { + ...FeedArticleConnection + } + } + } + } + ${feedFragment} + `, + topics: gql` + query TopicsFeedPublic($after: String) { + viewer @connection(key: "viewerFeedTopics") { + id + recommendation { + feed: topics(input: { first: 10, after: $after }) { + ...FeedArticleConnection + } + } + } + } + ${feedFragment} + `, +} + +export const FEED_ARTICLES_PRIVATE = gql` + query FeedArticlesPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Article { + ...ArticleDigestFeedArticlePrivate + } + } + } + ${ArticleDigestFeed.fragments.article.private} +` diff --git a/src/views/Home/Feed/index.tsx b/src/views/Home/Feed/index.tsx index 000f56d07f..1a7c75aa40 100644 --- a/src/views/Home/Feed/index.tsx +++ b/src/views/Home/Feed/index.tsx @@ -1,7 +1,6 @@ import { useQuery } from '@apollo/react-hooks' import { NetworkStatus } from 'apollo-client' -import gql from 'graphql-tag' -import { useContext } from 'react' +import { useContext, useEffect, useRef } from 'react' import { ArticleDigestFeed, @@ -18,153 +17,167 @@ import CLIENT_PREFERENCE from '~/components/GQL/queries/clientPreference' import { analytics, mergeConnections } from '~/common/utils' import Authors from './Authors' +import { FEED_ARTICLES_PRIVATE, FEED_ARTICLES_PUBLIC } from './gql' import SortBy, { SortByType } from './SortBy' import styles from './styles.css' import Tags from './Tags' import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' import { - HottestFeed, - HottestFeed_viewer_recommendation_feed_edges, -} from './__generated__/HottestFeed' + HottestFeedPublic, + HottestFeedPublic_viewer_recommendation_feed_edges, +} from './__generated__/HottestFeedPublic' import { - NewestFeed, - NewestFeed_viewer_recommendation_feed_edges, -} from './__generated__/NewestFeed' + IcymiFeedPublic, + IcymiFeedPublic_viewer_recommendation_feed_edges, +} from './__generated__/IcymiFeedPublic' +import { + NewestFeedPublic, + NewestFeedPublic_viewer_recommendation_feed_edges, +} from './__generated__/NewestFeedPublic' +import { + TopicsFeedPublic, + TopicsFeedPublic_viewer_recommendation_feed_edges, +} from './__generated__/TopicsFeedPublic' +import { + ValuedFeedPublic, + ValuedFeedPublic_viewer_recommendation_feed_edges, +} from './__generated__/ValuedFeedPublic' + +type FeedArticlesPublic = + | HottestFeedPublic + | NewestFeedPublic + | IcymiFeedPublic + | ValuedFeedPublic + | TopicsFeedPublic type HorizontalFeed = React.FC<{ after?: string; first?: number }> -interface FeedEdge { +interface HorizontalFeedEdge { __typename: 'HorizontalFeed' Feed: HorizontalFeed } +type FeedEdge = + | HorizontalFeedEdge + | HottestFeedPublic_viewer_recommendation_feed_edges + | IcymiFeedPublic_viewer_recommendation_feed_edges + | NewestFeedPublic_viewer_recommendation_feed_edges + | TopicsFeedPublic_viewer_recommendation_feed_edges + | ValuedFeedPublic_viewer_recommendation_feed_edges + interface FeedLocation { [key: number]: HorizontalFeed } +interface MainFeedProps { + feedSortType: SortByType + viewMode: string | null +} + const horizontalFeeds: FeedLocation = { 2: () => , 5: () => , } -const feedFragment = gql` - fragment FeedArticleConnection on ArticleConnection { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ...ArticleDigestFeedArticle - } - } - } - ${ArticleDigestFeed.fragments.article} -` - -export const queries = { - hottest: gql` - query ValuedFeed($after: String) { - viewer { - id - recommendation { - feed: valued(input: { first: 10, after: $after }) { - ...FeedArticleConnection - } - } - } - } - ${feedFragment} - `, - newest: gql` - query NewestFeed($after: String) { - viewer { - id - recommendation { - feed: newest(input: { first: 10, after: $after }) { - ...FeedArticleConnection - } - } - } - } - ${feedFragment} - `, - icymi: gql` - query IcymiFeed($after: String) { - viewer { - id - recommendation { - feed: icymi(input: { first: 10, after: $after }) { - ...FeedArticleConnection - } - } - } - } - ${feedFragment} - `, - topics: gql` - query TopicsFeed($after: String) { - viewer { - id - recommendation { - feed: topics(input: { first: 10, after: $after }) { - ...FeedArticleConnection - } - } - } - } - ${feedFragment} - `, -} - -const MainFeed = ({ feedSortType: sortBy }: { feedSortType: SortByType }) => { +const MainFeed = ({ feedSortType: sortBy, viewMode }: MainFeedProps) => { + const viewer = useContext(ViewerContext) const isLargeUp = useResponsive('lg-up') const isHottestFeed = sortBy === 'hottest' - const viewer = useContext(ViewerContext) + /** + * Data Fetching + */ + let query = FEED_ARTICLES_PUBLIC[sortBy] // split out group b if in hottest feed and user is logged in - if (isHottestFeed && viewer.id && viewer.info.group === 'b') { - queries.hottest = gql` - query HottestFeed($after: String) { - viewer { - id - recommendation { - feed: hottest(input: { first: 10, after: $after }) { - ...FeedArticleConnection - } - } - } - } - ${feedFragment} - ` + if (isHottestFeed && (!viewer.id || viewer.info.group !== 'b')) { + query = FEED_ARTICLES_PUBLIC.valued } + // public data const { data, error, loading, - fetchMore: fetchMoreMainFeed, + fetchMore, networkStatus, - refetch, - } = useQuery(queries[sortBy], { + refetch: refetchPublic, + client, + } = useQuery(query, { notifyOnNetworkStatusChange: true, }) + // pagination const connectionPath = 'viewer.recommendation.feed' const result = data?.viewer?.recommendation.feed const { edges, pageInfo } = result || {} const isNewLoading = networkStatus === NetworkStatus.loading - const { data: localCache } = useQuery(CLIENT_PREFERENCE, { - variables: { id: 'local' }, - }) + // private data + const loadPrivate = (publicData?: FeedArticlesPublic) => { + if (!viewer.id || !publicData) { + return + } - const { viewMode } = localCache?.clientPreference || { viewMode: 'default' } + const publiceEdges = publicData.viewer?.recommendation?.feed.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + client.query({ + query: FEED_ARTICLES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + const fetchedPrviateSortsRef = useRef([]) + useEffect(() => { + const fetched = fetchedPrviateSortsRef.current.indexOf(sortBy) >= 0 + if (loading || !edges || fetched || !viewer.id) { + return + } + + loadPrivate(data) + fetchedPrviateSortsRef.current = [...fetchedPrviateSortsRef.current, sortBy] + }, [!!edges, loading, sortBy, viewer.id]) + + // load next page + const loadMore = async () => { + if (loading || isNewLoading) { + return + } + + analytics.trackEvent('load_more', { + type: sortBy, + location: edges?.length || 0, + }) + + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + dedupe: true, + }), + }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + + /** + * Render + */ if (loading && (!result || isNewLoading)) { if (process.browser) { window.scrollTo(0, 0) @@ -181,34 +194,8 @@ const MainFeed = ({ feedSortType: sortBy }: { feedSortType: SortByType }) => { return } - const loadMore = () => { - analytics.trackEvent('load_more', { - type: sortBy, - location: edges.length, - }) - return fetchMoreMainFeed({ - variables: { - after: pageInfo.endCursor, - }, - // previousResult could be undefined when scrolling before loading finishes, reason unknown - updateQuery: (previousResult, { fetchMoreResult }) => - previousResult - ? mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - dedupe: true, - }) - : fetchMoreResult, - }) - } - // insert other feeds - let mixFeed: Array< - | FeedEdge - | HottestFeed_viewer_recommendation_feed_edges - | NewestFeed_viewer_recommendation_feed_edges - > = edges + let mixFeed: FeedEdge[] = edges if (!isLargeUp && isHottestFeed) { // get copy @@ -240,28 +227,28 @@ const MainFeed = ({ feedSortType: sortBy }: { feedSortType: SortByType }) => { if (edge.__typename === 'HorizontalFeed') { const { Feed } = edge return - } else { - return ( - - - analytics.trackEvent('click_feed', { - type: sortBy, - styleType: - viewMode === 'default' - ? 'small_cover' - : viewMode === 'compact' - ? 'no_cover' - : 'large_cover', - contentType: 'article', - location: i, - }) - } - /> - - ) } + + return ( + + + analytics.trackEvent('click_feed', { + type: sortBy, + styleType: + viewMode === 'default' + ? 'small_cover' + : viewMode === 'compact' + ? 'no_cover' + : 'large_cover', + contentType: 'article', + location: i, + }) + } + /> + + ) })} @@ -272,6 +259,7 @@ const HomeFeed = () => { const { data, client } = useQuery(CLIENT_PREFERENCE, { variables: { id: 'local' }, }) + const { viewMode } = data?.clientPreference || { viewMode: 'default' } const { feedSortType } = data?.clientPreference || { feedSortType: 'hottest', } @@ -290,7 +278,7 @@ const HomeFeed = () => { - + diff --git a/src/views/Home/SectionHeader/index.tsx b/src/views/Home/SectionHeader/index.tsx index fda2388d0e..c67597e84b 100644 --- a/src/views/Home/SectionHeader/index.tsx +++ b/src/views/Home/SectionHeader/index.tsx @@ -5,20 +5,16 @@ import { PATHS } from '~/common/enums' import styles from './styles.css' interface SidebarHeaderProps { - type: 'icymi' | 'authors' | 'tags' | 'topics' + type: 'authors' | 'tags' rightButton?: React.ReactNode } const FeedHeader = ({ type, rightButton }: SidebarHeaderProps) => { const pathMap = { - icymi: PATHS.ICYMI, - topics: PATHS.TOPICS, authors: PATHS.AUTHORS, tags: PATHS.TAGS, } const titleMap = { - icymi: , - topics: , authors: , tags: , } diff --git a/src/views/Home/Sidebar/Authors/gql.ts b/src/views/Home/Sidebar/Authors/gql.ts new file mode 100644 index 0000000000..381746ebf6 --- /dev/null +++ b/src/views/Home/Sidebar/Authors/gql.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +import { UserDigest } from '~/components' + +export const SIDEBAR_AUTHORS_PUBLIC = gql` + query SidebarAuthorsPublic { + viewer @connection(key: "viewerSidebarAuthors") { + id + recommendation { + authors( + input: { first: 5, filter: { random: true, followed: false } } + ) { + edges { + cursor + node { + ...UserDigestRichUserPublic + ...UserDigestRichUserPrivate + } + } + } + } + } + } + ${UserDigest.Rich.fragments.user.public} + ${UserDigest.Rich.fragments.user.private} +` + +export const SIDEBAR_AUTHORS_PRIVATE = gql` + query SidebarAuthorsPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on User { + ...UserDigestRichUserPrivate + } + } + } + ${UserDigest.Rich.fragments.user.private} +` diff --git a/src/views/Home/Sidebar/Authors/index.tsx b/src/views/Home/Sidebar/Authors/index.tsx index 53f170009e..459d3fdbf6 100644 --- a/src/views/Home/Sidebar/Authors/index.tsx +++ b/src/views/Home/Sidebar/Authors/index.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' +import { useContext, useEffect } from 'react' import { Button, @@ -9,45 +9,60 @@ import { TextIcon, Translate, UserDigest, + ViewerContext, } from '~/components' import { QueryError } from '~/components/GQL' import { analytics } from '~/common/utils' import SectionHeader from '../../SectionHeader' +import { SIDEBAR_AUTHORS_PRIVATE, SIDEBAR_AUTHORS_PUBLIC } from './gql' -import { SidebarAuthors } from './__generated__/SidebarAuthors' - -const SIDEBAR_AUTHORS = gql` - query SidebarAuthors { - viewer { - id - recommendation { - authors( - input: { first: 5, filter: { random: true, followed: false } } - ) { - edges { - cursor - node { - ...UserDigestRichUserPublic - ...UserDigestRichUserPrivate - } - } - } - } - } - } - ${UserDigest.Rich.fragments.user.public} - ${UserDigest.Rich.fragments.user.private} -` +import { SidebarAuthorsPublic } from './__generated__/SidebarAuthorsPublic' const Authors = () => { - const { data, loading, error, refetch } = useQuery( - SIDEBAR_AUTHORS, - { notifyOnNetworkStatusChange: true } - ) + const viewer = useContext(ViewerContext) + + /** + * Data Fetching + */ + // public data + const { data, loading, error, refetch: refetchPublic, client } = useQuery< + SidebarAuthorsPublic + >(SIDEBAR_AUTHORS_PUBLIC, { notifyOnNetworkStatusChange: true }) const edges = data?.viewer?.recommendation.authors.edges + // private data + const loadPrivate = (publicData?: SidebarAuthorsPublic) => { + if (!viewer.id || !publicData) { + return + } + + const publiceEdges = publicData?.viewer?.recommendation.authors.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: SIDEBAR_AUTHORS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + if (loading || !edges) { + return + } + + loadPrivate(data) + }, [!!edges, viewer.id]) + + // refetch + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + if (error) { return } @@ -65,7 +80,7 @@ const Authors = () => { size={[null, '1.25rem']} spacing={[0, 'xtight']} bgActiveColor="grey-lighter" - onClick={() => refetch()} + onClick={refetch} > } diff --git a/src/views/Home/Sidebar/Tags/index.tsx b/src/views/Home/Sidebar/Tags/index.tsx index 2d2d0188c4..624e71a91e 100644 --- a/src/views/Home/Sidebar/Tags/index.tsx +++ b/src/views/Home/Sidebar/Tags/index.tsx @@ -8,11 +8,11 @@ import { analytics, toPath } from '~/common/utils' import SectionHeader from '../../SectionHeader' -import { SidebarTags } from './__generated__/SidebarTags' +import { SidebarTagsPublic } from './__generated__/SidebarTagsPublic' const SIDEBAR_TAGS = gql` - query SidebarTags { - viewer { + query SidebarTagsPublic { + viewer @connection(key: "viewerSidebarTags") { id recommendation { tags(input: { first: 5 }) { @@ -30,7 +30,7 @@ const SIDEBAR_TAGS = gql` ` const Tags = () => { - const { data, loading, error } = useQuery(SIDEBAR_TAGS) + const { data, loading, error } = useQuery(SIDEBAR_TAGS) const edges = data?.viewer?.recommendation.tags.edges if (error) { diff --git a/src/views/Me/Bookmarks/index.tsx b/src/views/Me/Bookmarks/index.tsx index 7994f074a1..bd1386e066 100644 --- a/src/views/Me/Bookmarks/index.tsx +++ b/src/views/Me/Bookmarks/index.tsx @@ -29,13 +29,15 @@ const ME_BOOKMARK_FEED = gql` edges { cursor node { - ...ArticleDigestFeedArticle + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate } } } } } - ${ArticleDigestFeed.fragments.article} + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} ` const MeBookmarks = () => { diff --git a/src/views/Me/DraftDetail/PublishState/PendingState.tsx b/src/views/Me/DraftDetail/PublishState/PendingState.tsx index 87c60974fa..f76257245d 100644 --- a/src/views/Me/DraftDetail/PublishState/PendingState.tsx +++ b/src/views/Me/DraftDetail/PublishState/PendingState.tsx @@ -1,27 +1,21 @@ import { useQuery } from '@apollo/react-hooks' -import { Toast, Translate, useCountdown } from '~/components' +import { Toast, Translate } from '~/components' import DRAFT_PUBLISH_STATE from '~/components/GQL/queries/draftPublishState' -import { TEXT } from '~/common/enums' - import { PublishStateDraft } from '~/components/GQL/fragments/__generated__/PublishStateDraft' import { DraftPublishState } from '~/components/GQL/queries/__generated__/DraftPublishState' const PendingState = ({ draft }: { draft: PublishStateDraft }) => { const scheduledAt = draft.scheduledAt - const { - countdown: { timeLeft }, - formattedTimeLeft, - } = useCountdown({ timeLeft: Date.parse(scheduledAt) - Date.now() }) - const isPublishing = !scheduledAt || !timeLeft || timeLeft <= 0 + const isPublishing = !scheduledAt || Date.parse(scheduledAt) <= Date.now() useQuery(DRAFT_PUBLISH_STATE, { variables: { id: draft.id }, pollInterval: 1000 * 2, errorPolicy: 'none', fetchPolicy: 'network-only', - skip: !process.browser || !isPublishing, + skip: !process.browser, }) return ( @@ -31,10 +25,7 @@ const PendingState = ({ draft }: { draft: PublishStateDraft }) => { isPublishing ? ( ) : ( - + ) } subDescription={ diff --git a/src/views/Me/DraftDetail/PublishState/RetryButton.tsx b/src/views/Me/DraftDetail/PublishState/RetryButton.tsx index aed19919e1..65ba2da319 100644 --- a/src/views/Me/DraftDetail/PublishState/RetryButton.tsx +++ b/src/views/Me/DraftDetail/PublishState/RetryButton.tsx @@ -33,7 +33,7 @@ const RetryButton = ({ id }: { id: string }) => { size={[null, '1.25rem']} spacing={[0, 'xtight']} bgActiveColor="red" - onClick={() => retry()} + onClick={retry} > { }) return ( - } defaultCollapsed={false} > @@ -130,7 +130,7 @@ const AddCover = ({ draft, ...props }: AddCover) => { - + ) } diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx b/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx deleted file mode 100644 index d7632cb3dd..0000000000 --- a/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import classNames from 'classnames' -import gql from 'graphql-tag' -import _uniq from 'lodash/uniq' - -import { Translate } from '~/components' -import { useMutation } from '~/components/GQL' - -import Collapsable from '../Collapsable' -import SearchTags from './SearchTags' -import styles from './styles.css' -import Tag from './Tag' - -import { AddTagsDraft } from './__generated__/AddTagsDraft' -import { UpdateDraftTags } from './__generated__/UpdateDraftTags' - -const fragments = { - draft: gql` - fragment AddTagsDraft on Draft { - id - tags - publishState - } - `, -} - -const UPDATE_TAGS = gql` - mutation UpdateDraftTags($id: ID!, $tags: [String]!) { - putDraft(input: { id: $id, tags: $tags }) { - id - ...AddTagsDraft - } - } - ${fragments.draft} -` - -interface AddTagsProps { - draft: AddTagsDraft - setSaveStatus: (status: 'saved' | 'saving' | 'saveFailed') => void -} - -const AddTags = ({ draft, setSaveStatus }: AddTagsProps) => { - const [updateTags] = useMutation(UPDATE_TAGS) - // const draftId = draft.id - const tags = draft.tags || [] - const hasTags = tags.length > 0 - const isPending = draft.publishState === 'pending' - const isPublished = draft.publishState === 'published' - const tagsContainerClasses = classNames({ - 'tags-container': true, - 'u-area-disable': isPending || isPublished, - }) - const addTag = async (tag: string) => { - setSaveStatus('saving') - try { - await updateTags({ - variables: { id: draft.id, tags: _uniq(tags.concat(tag)) }, - }) - setSaveStatus('saved') - } catch (e) { - setSaveStatus('saveFailed') - } - } - const deleteTag = async (tag: string) => { - setSaveStatus('saving') - try { - await updateTags({ - variables: { id: draft.id, tags: tags.filter((it) => it !== tag) }, - }) - setSaveStatus('saved') - } catch (e) { - setSaveStatus('saveFailed') - } - } - - return ( - } defaultCollapsed={!hasTags}> -

- -

- -
- {tags.map((tag) => ( - - ))} - -
- - -
- ) -} - -AddTags.fragments = fragments - -export default AddTags diff --git a/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx b/src/views/Me/DraftDetail/Sidebar/EditCollection.tsx similarity index 58% rename from src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx rename to src/views/Me/DraftDetail/Sidebar/EditCollection.tsx index 19ba63b76f..825b941bd1 100644 --- a/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/EditCollection.tsx @@ -1,31 +1,19 @@ import { useQuery } from '@apollo/react-hooks' -import classNames from 'classnames' import gql from 'graphql-tag' import _uniq from 'lodash/uniq' -import dynamic from 'next/dynamic' -import { ArticleDigestDropdown, Spinner, Translate } from '~/components' +import { ArticleDigestDropdown, Spinner } from '~/components' +import SidebarCollection from '~/components/Editor/Sidebar/Collection' import { QueryError, useMutation } from '~/components/GQL' -import Collapsable from '../Collapsable' -import styles from './styles.css' - import { ArticleDigestDropdownArticle } from '~/components/ArticleDigest/Dropdown/__generated__/ArticleDigestDropdownArticle' -import { CollectArticlesDraft } from './__generated__/CollectArticlesDraft' import { DraftCollectionQuery } from './__generated__/DraftCollectionQuery' +import { EditCollectionDraft } from './__generated__/EditCollectionDraft' import { SetDraftCollection } from './__generated__/SetDraftCollection' -const DynamicCollectionEditor = dynamic( - () => import('~/components/CollectionEditor'), - { - ssr: false, - loading: Spinner, - } -) - const fragments = { draft: gql` - fragment CollectArticlesDraft on Draft { + fragment EditCollectionDraft on Draft { id publishState collection(input: { first: 0 }) { @@ -69,20 +57,17 @@ const SET_DRAFT_COLLECTION = gql` ${ArticleDigestDropdown.fragments.article} ` -interface CollectArticlesProps { - draft: CollectArticlesDraft +interface EditCollectionProps { + draft: EditCollectionDraft setSaveStatus: (status: 'saved' | 'saving' | 'saveFailed') => void } -const CollectArticles = ({ draft, setSaveStatus }: CollectArticlesProps) => { +const EditCollection = ({ draft, setSaveStatus }: EditCollectionProps) => { const draftId = draft.id const isPending = draft.publishState === 'pending' const isPublished = draft.publishState === 'published' - const containerClasses = classNames({ - container: true, - 'u-area-disable': isPending || isPublished, - }) - const handleCollectionChange = () => async ( + + const handleCollectionChange = async ( articles: ArticleDigestDropdownArticle[] ) => { setSaveStatus('saving') @@ -113,34 +98,23 @@ const CollectArticles = ({ draft, setSaveStatus }: CollectArticlesProps) => { data.node.collection && data.node.collection.edges - return ( - } - defaultCollapsed={draft.collection.totalCount <= 0} - > -

- -

- -
- {loading && } - - {error && } + if (loading) { + return + } - node)) || []} - onEdit={handleCollectionChange()} - /> -
+ if (error) { + return + } - -
+ return ( + node)) || []} + onEdit={handleCollectionChange} + disabled={isPending || isPublished} + /> ) } -CollectArticles.fragments = fragments +EditCollection.fragments = fragments -export default CollectArticles +export default EditCollection diff --git a/src/views/Me/DraftDetail/Sidebar/EditTags.tsx b/src/views/Me/DraftDetail/Sidebar/EditTags.tsx new file mode 100644 index 0000000000..c12ea6bc62 --- /dev/null +++ b/src/views/Me/DraftDetail/Sidebar/EditTags.tsx @@ -0,0 +1,76 @@ +import gql from 'graphql-tag' +import _uniq from 'lodash/uniq' + +import SidebarTags from '~/components/Editor/Sidebar/Tags' +import { useMutation } from '~/components/GQL' + +import { EditTagsDraft } from './__generated__/EditTagsDraft' +import { UpdateDraftTags } from './__generated__/UpdateDraftTags' + +const fragments = { + draft: gql` + fragment EditTagsDraft on Draft { + id + tags + publishState + } + `, +} + +const UPDATE_TAGS = gql` + mutation UpdateDraftTags($id: ID!, $tags: [String]!) { + putDraft(input: { id: $id, tags: $tags }) { + id + ...EditTagsDraft + } + } + ${fragments.draft} +` + +interface EditTagsProps { + draft: EditTagsDraft + setSaveStatus: (status: 'saved' | 'saving' | 'saveFailed') => void +} + +const EditTags = ({ draft, setSaveStatus }: EditTagsProps) => { + const [updateTags] = useMutation(UPDATE_TAGS) + const tags = draft.tags || [] + const isPending = draft.publishState === 'pending' + const isPublished = draft.publishState === 'published' + + const addTag = async (tag: string) => { + setSaveStatus('saving') + try { + await updateTags({ + variables: { id: draft.id, tags: _uniq(tags.concat(tag)) }, + }) + setSaveStatus('saved') + } catch (e) { + setSaveStatus('saveFailed') + } + } + const deleteTag = async (tag: string) => { + setSaveStatus('saving') + try { + await updateTags({ + variables: { id: draft.id, tags: tags.filter((it) => it !== tag) }, + }) + setSaveStatus('saved') + } catch (e) { + setSaveStatus('saveFailed') + } + } + + return ( + + ) +} + +EditTags.fragments = fragments + +export default EditTags diff --git a/src/views/Me/DraftDetail/Sidebar/index.tsx b/src/views/Me/DraftDetail/Sidebar/index.tsx index da8370f804..f751e92239 100644 --- a/src/views/Me/DraftDetail/Sidebar/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/index.tsx @@ -1,8 +1,8 @@ import gql from 'graphql-tag' import AddCover from './AddCover' -import AddTags from './AddTags' -import CollectArticles from './CollectArticles' +import EditCollection from './EditCollection' +import EditTags from './EditTags' import { DraftSidebarDraft } from './__generated__/DraftSidebarDraft' @@ -14,8 +14,8 @@ interface SidebarProps { const Sidebar = ({ draft, setSaveStatus }: SidebarProps) => ( <> - - + + ) @@ -24,12 +24,12 @@ Sidebar.fragments = { fragment DraftSidebarDraft on Draft { id ...AddCoverDraft - ...AddTagsDraft - ...CollectArticlesDraft + ...EditTagsDraft + ...EditCollectionDraft } ${AddCover.fragments.draft} - ${AddTags.fragments.draft} - ${CollectArticles.fragments.draft} + ${EditTags.fragments.draft} + ${EditCollection.fragments.draft} `, } diff --git a/src/views/Me/History/index.tsx b/src/views/Me/History/index.tsx index 7887e1ea36..9ff281d5cb 100644 --- a/src/views/Me/History/index.tsx +++ b/src/views/Me/History/index.tsx @@ -31,7 +31,8 @@ const ME_HISTORY_FEED = gql` cursor node { article { - ...ArticleDigestFeedArticle + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate } } } @@ -39,7 +40,8 @@ const ME_HISTORY_FEED = gql` } } } - ${ArticleDigestFeed.fragments.article} + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} ` const MeHistory = () => { diff --git a/src/views/Me/Settings/Settings/UI/ViewMode.tsx b/src/views/Me/Settings/Settings/UI/ViewMode.tsx index 8048233833..9e8fdc5973 100644 --- a/src/views/Me/Settings/Settings/UI/ViewMode.tsx +++ b/src/views/Me/Settings/Settings/UI/ViewMode.tsx @@ -16,7 +16,7 @@ import { STORE_KEY_VIEW_MODE } from '~/common/enums' import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' -type ViewMode = 'default' | 'comfortable' | 'compact' +export type ViewModeType = 'default' | 'comfortable' | 'compact' const ViewMode = () => { const { data, client } = useQuery(CLIENT_PREFERENCE, { @@ -27,7 +27,7 @@ const ViewMode = () => { const isComfortableMode = viewMode === 'comfortable' const isCompactMode = viewMode === 'compact' - const setViewMode = (mode: ViewMode) => { + const setViewMode = (mode: ViewModeType) => { if (client) { client.writeData({ id: 'ClientPreference:local', diff --git a/src/views/OAuth/Authorize/index.tsx b/src/views/OAuth/Authorize/index.tsx index 6765861f4e..b4506b894b 100644 --- a/src/views/OAuth/Authorize/index.tsx +++ b/src/views/OAuth/Authorize/index.tsx @@ -136,14 +136,12 @@ const OAuthAuthorize = () => { - {/* FIXME: only render at CSR to get correct `appendTarget` */} - {process.browser && ( - - - - - - )} + + + + + +

diff --git a/src/views/Search/AggregateResults/Articles.tsx b/src/views/Search/AggregateResults/Articles.tsx index c5ebadb0f1..a0266727c2 100644 --- a/src/views/Search/AggregateResults/Articles.tsx +++ b/src/views/Search/AggregateResults/Articles.tsx @@ -1,52 +1,46 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' -import { ArticleDigestTitle, Card, List, Spinner } from '~/components' +import { + ArticleDigestTitle, + Card, + List, + Spinner, + usePullToRefresh, +} from '~/components' import { analytics, getQuery, toPath } from '~/common/utils' +import { SEARCH_AGGREGATE_ARTICLES_PUBLIC } from './gql' import styles from './styles.css' import ViewMoreButton from './ViewMoreButton' -import { SeachAggregateArticles } from './__generated__/SeachAggregateArticles' - -const SEARCH_AGGREGATE_ARTICLES = gql` - query SeachAggregateArticles($key: String!) { - search(input: { key: $key, type: Article, first: 4 }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on Article { - ...ArticleDigestTitleArticle - } - } - } - } - } - ${ArticleDigestTitle.fragments.article} -` +import { SeachAggregateArticlesPublic } from './__generated__/SeachAggregateArticlesPublic' const AggregateArticleResults = () => { const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading } = useQuery( - SEARCH_AGGREGATE_ARTICLES, + /** + * Data Fetching + */ + // public data + const { data, loading, refetch } = useQuery( + SEARCH_AGGREGATE_ARTICLES_PUBLIC, { variables: { key: q } } ) + const { edges, pageInfo } = data?.search || {} + + usePullToRefresh.Handler(refetch) + + /** + * Render + */ if (loading) { return } - const { edges, pageInfo } = data?.search || {} - if (!edges || edges.length <= 0 || !pageInfo) { return null } diff --git a/src/views/Search/AggregateResults/Tags.tsx b/src/views/Search/AggregateResults/Tags.tsx index d9adad25b5..e29cd8d3c0 100644 --- a/src/views/Search/AggregateResults/Tags.tsx +++ b/src/views/Search/AggregateResults/Tags.tsx @@ -1,52 +1,40 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' -import { Card, List, Spinner, Tag } from '~/components' +import { Card, List, Spinner, Tag, usePullToRefresh } from '~/components' import { analytics, getQuery, toPath } from '~/common/utils' +import { SEARCH_AGGREGATE_TAGS_PUBLIC } from './gql' import styles from './styles.css' import ViewMoreButton from './ViewMoreButton' -import { SeachAggregateTags } from './__generated__/SeachAggregateTags' - -const SEARCH_AGGREGATE_TAGS = gql` - query SeachAggregateTags($key: String!) { - search(input: { key: $key, type: Tag, first: 3 }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on Tag { - ...DigestTag - } - } - } - } - } - ${Tag.fragments.tag} -` +import { SeachAggregateTagsPublic } from './__generated__/SeachAggregateTagsPublic' const AggregateTagResults = () => { const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading } = useQuery( - SEARCH_AGGREGATE_TAGS, + /** + * Data Fetching + */ + // public data + const { data, loading, refetch } = useQuery( + SEARCH_AGGREGATE_TAGS_PUBLIC, { variables: { key: q } } ) + const { edges, pageInfo } = data?.search || {} + + usePullToRefresh.Handler(refetch) + + /** + * Render + */ if (loading) { return } - const { edges, pageInfo } = data?.search || {} - if (!edges || edges.length <= 0 || !pageInfo) { return null } diff --git a/src/views/Search/AggregateResults/Users.tsx b/src/views/Search/AggregateResults/Users.tsx index 552ed8b4b0..63595c4507 100644 --- a/src/views/Search/AggregateResults/Users.tsx +++ b/src/views/Search/AggregateResults/Users.tsx @@ -1,52 +1,40 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' -import { Card, List, Spinner, UserDigest } from '~/components' +import { Card, List, Spinner, usePullToRefresh, UserDigest } from '~/components' import { analytics, getQuery, toPath } from '~/common/utils' +import { SEARCH_AGGREGATE_USERS_PUBLIC } from './gql' import styles from './styles.css' import ViewMoreButton from './ViewMoreButton' -import { SeachAggregateUsers } from './__generated__/SeachAggregateUsers' - -const SEARCH_AGGREGATE_USERS = gql` - query SeachAggregateUsers($key: String!) { - search(input: { key: $key, type: User, first: 3 }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on User { - ...UserDigestMiniUser - } - } - } - } - } - ${UserDigest.Mini.fragments.user} -` +import { SeachAggregateUsersPublic } from './__generated__/SeachAggregateUsersPublic' const AggregateUserResults = () => { const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading } = useQuery( - SEARCH_AGGREGATE_USERS, + /** + * Data Fetching + */ + // public data + const { data, loading, refetch } = useQuery( + SEARCH_AGGREGATE_USERS_PUBLIC, { variables: { key: q } } ) + const { edges, pageInfo } = data?.search || {} + + usePullToRefresh.Handler(refetch) + + /** + * Render + */ if (loading) { return } - const { edges, pageInfo } = data?.search || {} - if (!edges || edges.length <= 0 || !pageInfo) { return null } diff --git a/src/views/Search/AggregateResults/gql.ts b/src/views/Search/AggregateResults/gql.ts new file mode 100644 index 0000000000..b236f6c521 --- /dev/null +++ b/src/views/Search/AggregateResults/gql.ts @@ -0,0 +1,66 @@ +import gql from 'graphql-tag' + +import { ArticleDigestTitle, Tag, UserDigest } from '~/components' + +export const SEARCH_AGGREGATE_ARTICLES_PUBLIC = gql` + query SeachAggregateArticlesPublic($key: String!) { + search(input: { key: $key, type: Article, first: 4 }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on Article { + ...ArticleDigestTitleArticle + } + } + } + } + } + ${ArticleDigestTitle.fragments.article} +` + +export const SEARCH_AGGREGATE_TAGS_PUBLIC = gql` + query SeachAggregateTagsPublic($key: String!) { + search(input: { key: $key, type: Tag, first: 3 }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on Tag { + ...DigestTag + } + } + } + } + } + ${Tag.fragments.tag} +` + +export const SEARCH_AGGREGATE_USERS_PUBLIC = gql` + query SeachAggregateUsersPublic($key: String!) { + search(input: { key: $key, type: User, first: 3 }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on User { + ...UserDigestMiniUser + } + } + } + } + } + ${UserDigest.Mini.fragments.user} +` diff --git a/src/views/Search/AggregateResults/index.tsx b/src/views/Search/AggregateResults/index.tsx index 781d3e5071..cbde900a97 100644 --- a/src/views/Search/AggregateResults/index.tsx +++ b/src/views/Search/AggregateResults/index.tsx @@ -1,8 +1,12 @@ +import { usePullToRefresh } from '~/components' + import Articles from './Articles' import Tags from './Tags' import Users from './Users' const AggregateResults = () => { + usePullToRefresh.Register() + return ( <> diff --git a/src/views/Search/SearchArticles/gql.ts b/src/views/Search/SearchArticles/gql.ts new file mode 100644 index 0000000000..615228118b --- /dev/null +++ b/src/views/Search/SearchArticles/gql.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +import { ArticleDigestFeed } from '~/components' + +export const SEARCH_ARTICLES_PUBLIC = gql` + query SeachArticlesPublic($key: String!, $first: Int!, $after: String) { + search(input: { key: $key, type: Article, first: $first, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on Article { + ...ArticleDigestFeedArticlePublic + ...ArticleDigestFeedArticlePrivate + } + } + } + } + } + ${ArticleDigestFeed.fragments.article.public} + ${ArticleDigestFeed.fragments.article.private} +` + +export const SEARCH_ARTICLES_PRIVATE = gql` + query SeachArticlesPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Article { + ...ArticleDigestFeedArticlePrivate + } + } + } + ${ArticleDigestFeed.fragments.article.private} +` diff --git a/src/views/Search/SearchArticles/index.tsx b/src/views/Search/SearchArticles/index.tsx index 8ff731334d..c6c304ee84 100644 --- a/src/views/Search/SearchArticles/index.tsx +++ b/src/views/Search/SearchArticles/index.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@apollo/react-hooks' import { NetworkStatus } from 'apollo-client' -import gql from 'graphql-tag' import { useRouter } from 'next/router' +import { useContext, useEffect } from 'react' import { ArticleDigestFeed, @@ -9,69 +9,78 @@ import { List, Spinner, Translate, - usePullToRefresh, + ViewerContext, } from '~/components' import { analytics, getQuery, mergeConnections } from '~/common/utils' import EmptySearch from '../EmptySearch' +import { SEARCH_ARTICLES_PRIVATE, SEARCH_ARTICLES_PUBLIC } from './gql' -import { SeachArticles } from './__generated__/SeachArticles' - -const SEARCH_ARTICLES = gql` - query SeachArticles($key: String!, $first: Int!, $after: String) { - search(input: { key: $key, type: Article, first: $first, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on Article { - ...ArticleDigestFeedArticle - } - } - } - } - } - ${ArticleDigestFeed.fragments.article} -` +import { SeachArticlesPublic } from './__generated__/SeachArticlesPublic' const SearchArticles = () => { + const viewer = useContext(ViewerContext) const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading, fetchMore, networkStatus, refetch } = useQuery< - SeachArticles - >(SEARCH_ARTICLES, { + /** + * Data Fetching + */ + // public data + const { + data, + loading, + fetchMore, + networkStatus, + refetch: refetchPublic, + client, + } = useQuery(SEARCH_ARTICLES_PUBLIC, { variables: { key: q, first: 10 }, notifyOnNetworkStatusChange: true, }) const isNewLoading = networkStatus === NetworkStatus.setVariables - usePullToRefresh.Handler(refetch) - - if (loading && (!data?.search || isNewLoading)) { - return - } - + // pagination const connectionPath = 'search' const { edges, pageInfo } = data?.search || {} - if (!edges || edges.length <= 0 || !pageInfo) { - return } /> + // private data + const loadPrivate = (publicData?: SeachArticlesPublic) => { + if (!viewer.id || !publicData) { + return + } + + const publicIds = (publicData?.search.edges || []) + .filter(({ node }) => node.__typename === 'Article') + .map(({ node }) => node.__typename === 'Article' && node.id) + + client.query({ + query: SEARCH_ARTICLES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) } - const loadMore = () => { + // fetch private data for first page + useEffect(() => { + if (loading || !edges) { + return + } + + loadPrivate(data) + }, [!!edges, viewer.id]) + + // load next page + const loadMore = async () => { analytics.trackEvent('load_more', { type: 'search_article', - location: edges.length, + location: edges?.length || 0, }) - return fetchMore({ + + const { data: newData } = await fetchMore({ variables: { - after: pageInfo.endCursor, + after: pageInfo?.endCursor, }, updateQuery: (previousResult, { fetchMoreResult }) => mergeConnections({ @@ -80,10 +89,33 @@ const SearchArticles = () => { path: connectionPath, }), }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + + /** + * Render + */ + if (loading && (!data?.search || isNewLoading)) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return } /> } return ( - + {edges.map( ({ node, cursor }, i) => diff --git a/src/views/Search/SearchTags/gql.ts b/src/views/Search/SearchTags/gql.ts new file mode 100644 index 0000000000..8f6ec02b57 --- /dev/null +++ b/src/views/Search/SearchTags/gql.ts @@ -0,0 +1,24 @@ +import gql from 'graphql-tag' + +import { Tag } from '~/components' + +export const SEARCH_TAGS_PUBLIC = gql` + query SeachTagsPublic($key: String!, $after: String) { + search(input: { key: $key, type: Tag, first: 20, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on Tag { + ...DigestTag + } + } + } + } + } + ${Tag.fragments.tag} +` diff --git a/src/views/Search/SearchTags/index.tsx b/src/views/Search/SearchTags/index.tsx index 85ba662f74..700c9e23ce 100644 --- a/src/views/Search/SearchTags/index.tsx +++ b/src/views/Search/SearchTags/index.tsx @@ -1,5 +1,4 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' import { @@ -9,68 +8,43 @@ import { Spinner, Tag, Translate, - usePullToRefresh, } from '~/components' import { analytics, getQuery, mergeConnections, toPath } from '~/common/utils' import EmptySearch from '../EmptySearch' +import { SEARCH_TAGS_PUBLIC } from './gql' -import { SeachTags } from './__generated__/SeachTags' - -const SEARCH_TAGS = gql` - query SeachTags($key: String!, $after: String) { - search(input: { key: $key, type: Tag, first: 20, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on Tag { - ...DigestTag - } - } - } - } - } - ${Tag.fragments.tag} -` +import { SeachTagsPublic } from './__generated__/SeachTagsPublic' const SearchTag = () => { const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading, fetchMore, refetch } = useQuery( - SEARCH_TAGS, + /** + * Data Fetching + */ + // public data + const { data, loading, fetchMore, refetch } = useQuery( + SEARCH_TAGS_PUBLIC, { variables: { key: q }, } ) - usePullToRefresh.Handler(refetch) - - if (loading) { - return - } - + // pagination const connectionPath = 'search' const { edges, pageInfo } = data?.search || {} - if (!edges || edges.length <= 0 || !pageInfo) { - return null - } - + // load next page const loadMore = () => { analytics.trackEvent('load_more', { type: 'search_tag', - location: edges.length, + location: edges?.length || 0, }) return fetchMore({ variables: { - after: pageInfo.endCursor, + after: pageInfo?.endCursor, }, updateQuery: (previousResult, { fetchMoreResult }) => mergeConnections({ @@ -81,12 +55,27 @@ const SearchTag = () => { }) } + /** + * Render + */ + if (loading) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return null + } + if (edges.length <= 0) { return } /> } return ( - + {edges.map( ({ node, cursor }, i) => diff --git a/src/views/Search/SearchUsers/gql.ts b/src/views/Search/SearchUsers/gql.ts new file mode 100644 index 0000000000..83bf024d19 --- /dev/null +++ b/src/views/Search/SearchUsers/gql.ts @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +import { UserDigest } from '~/components' + +export const SEARCH_USERS_PUBLIC = gql` + query SeachUsersPublic($key: String!, $after: String) { + search(input: { key: $key, type: User, first: 20, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ... on User { + ...UserDigestRichUserPublic + ...UserDigestRichUserPrivate + } + } + } + } + } + ${UserDigest.Rich.fragments.user.public} + ${UserDigest.Rich.fragments.user.private} +` + +export const SEARCH_USERS_PRIVATE = gql` + query SeachUsersPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on User { + ...UserDigestRichUserPrivate + } + } + } + ${UserDigest.Rich.fragments.user.private} +` diff --git a/src/views/Search/SearchUsers/index.tsx b/src/views/Search/SearchUsers/index.tsx index 5c06068761..5aab87a95b 100644 --- a/src/views/Search/SearchUsers/index.tsx +++ b/src/views/Search/SearchUsers/index.tsx @@ -1,77 +1,78 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' +import { useContext, useEffect } from 'react' import { InfiniteScroll, List, Spinner, Translate, - usePullToRefresh, UserDigest, + ViewerContext, } from '~/components' import { analytics, getQuery, mergeConnections } from '~/common/utils' import EmptySearch from '../EmptySearch' +import { SEARCH_USERS_PRIVATE, SEARCH_USERS_PUBLIC } from './gql' -import { SeachUsers } from './__generated__/SeachUsers' - -const SEARCH_USERS = gql` - query SeachUsers($key: String!, $after: String) { - search(input: { key: $key, type: User, first: 20, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ... on User { - ...UserDigestRichUserPublic - ...UserDigestRichUserPrivate - } - } - } - } - } - ${UserDigest.Rich.fragments.user.public} - ${UserDigest.Rich.fragments.user.private} -` +import { SeachUsersPublic } from './__generated__/SeachUsersPublic' const SearchUser = () => { + const viewer = useContext(ViewerContext) const router = useRouter() const q = getQuery({ router, key: 'q' }) - const { data, loading, fetchMore, refetch } = useQuery( - SEARCH_USERS, - { - variables: { key: q }, + /** + * Data Fetching + */ + // public data + const { data, loading, fetchMore, refetch: refetchPublic, client } = useQuery< + SeachUsersPublic + >(SEARCH_USERS_PUBLIC, { + variables: { key: q }, + }) + + // pagination + const connectionPath = 'search' + const { edges, pageInfo } = data?.search || {} + + // private data + const loadPrivate = (publicData?: SeachUsersPublic) => { + if (!viewer.id || !publicData) { + return } - ) - usePullToRefresh.Handler(refetch) + const publicIds = (publicData?.search.edges || []) + .filter(({ node }) => node.__typename === 'User') + .map(({ node }) => node.__typename === 'User' && node.id) - if (loading) { - return + client.query({ + query: SEARCH_USERS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) } - const connectionPath = 'search' - const { edges, pageInfo } = data?.search || {} + // fetch private data for first page + useEffect(() => { + if (loading || !edges) { + return + } - if (!edges || edges.length <= 0 || !pageInfo) { - return } /> - } + loadPrivate(data) + }, [!!edges, viewer.id]) - const loadMore = () => { + // load next page + const loadMore = async () => { analytics.trackEvent('load_more', { type: 'search_user', - location: edges.length, + location: edges?.length || 0, }) - return fetchMore({ + + const { data: newData } = await fetchMore({ variables: { - after: pageInfo.endCursor, + after: pageInfo?.endCursor, }, updateQuery: (previousResult, { fetchMoreResult }) => mergeConnections({ @@ -80,10 +81,33 @@ const SearchUser = () => { path: connectionPath, }), }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + + /** + * Render + */ + if (loading) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return } /> } return ( - + {edges.map( ({ node, cursor }, i) => diff --git a/src/views/TagDetail/Articles/index.tsx b/src/views/TagDetail/Articles/index.tsx new file mode 100644 index 0000000000..15ed2c20c9 --- /dev/null +++ b/src/views/TagDetail/Articles/index.tsx @@ -0,0 +1,188 @@ +import { useQuery } from '@apollo/react-hooks' +import { NetworkStatus } from 'apollo-client' +import { useContext, useEffect } from 'react' + +import { + ArticleDigestFeed, + EmptyTagArticles, + InfiniteScroll, + List, + Spinner, + useEventListener, + usePullToRefresh, + ViewerContext, +} from '~/components' +import { QueryError } from '~/components/GQL' +import { + TAG_ARTICLES_PRIVATE, + TAG_ARTICLES_PUBLIC, +} from '~/components/GQL/queries/tagArticles' + +import { REFETCH_TAG_DETAIL_ARTICLES } from '~/common/enums' +import { analytics, mergeConnections } from '~/common/utils' + +import { TagArticlesPublic } from '~/components/GQL/queries/__generated__/TagArticlesPublic' + +interface TagArticlesProps { + tagId: string + selected?: boolean +} + +const TagDetailArticles = ({ tagId, selected }: TagArticlesProps) => { + const viewer = useContext(ViewerContext) + + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + networkStatus, + client, + } = useQuery(TAG_ARTICLES_PUBLIC, { + variables: { id: tagId, selected }, + notifyOnNetworkStatusChange: true, + }) + + // pagination + const connectionPath = 'node.articles' + const { edges, pageInfo } = + (data?.node?.__typename === 'Tag' && + data.node.articles && + data.node.articles) || + {} + const isNewLoading = networkStatus === NetworkStatus.loading + + // private data + const loadPrivate = (publicData?: TagArticlesPublic) => { + if (!viewer.id || !publicData) { + return + } + + const publiceEdges = + publicData?.node?.__typename === 'Tag' + ? publicData.node.articles.edges || [] + : [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: TAG_ARTICLES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + if (loading || !edges) { + return + } + + loadPrivate(data) + }, [!!edges, loading, selected, viewer.id]) + + // load next page + const loadMore = async () => { + analytics.trackEvent('load_more', { + type: selected ? 'tag_detail_selected' : 'tag_detail_latest', + location: edges ? edges.length : 0, + }) + + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + // refetch, sync & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + + const sync = async ({ + event, + differences = 0, + }: { + event: 'add' | 'delete' + differences?: number + }) => { + const count = (edges || []).length + switch (event) { + case 'add': + const { data: addData } = await refetchPublic({ + variables: { + id: tagId, + first: count + differences, + }, + }) + loadPrivate(addData) + break + case 'delete': + const { data: deleteData } = await refetchPublic({ + variables: { + id: tagId, + first: Math.max(count - 1, 0), + }, + }) + loadPrivate(deleteData) + break + } + } + + useEventListener(REFETCH_TAG_DETAIL_ARTICLES, sync) + usePullToRefresh.Handler(refetch) + + /** + * Render + */ + if (loading && isNewLoading) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + return ( + + + {(edges || []).map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: selected ? 'tag_detail_selected' : 'tag_detail_latest', + contentType: 'article', + styleType: 'title', + location: i, + }) + } + inTagDetailLatest + /> + + ))} + + + ) +} + +export default TagDetailArticles diff --git a/src/views/TagDetail/ArticlesCount/index.tsx b/src/views/TagDetail/ArticlesCount/index.tsx new file mode 100644 index 0000000000..b75d214ebf --- /dev/null +++ b/src/views/TagDetail/ArticlesCount/index.tsx @@ -0,0 +1,39 @@ +import { useQuery } from '@apollo/react-hooks' + +import { Translate } from '~/components' +import TAG_ARTICLES_COUNT from '~/components/GQL/queries/tagArticlesCount' + +import { numAbbr } from '~/common/utils' + +import styles from './styles.css' + +import { TagArticlesCount } from '~/components/GQL/queries/__generated__/TagArticlesCount' + +interface ArticlesCountProps { + id: string +} + +const ArticlesCount = ({ id }: ArticlesCountProps) => { + const { data } = useQuery(TAG_ARTICLES_COUNT, { + variables: { id }, + }) + + if (!data || !data.node || data.node.__typename !== 'Tag') { + return null + } + + const { totalCount } = data?.node?.articles || { totalCount: 0 } + + return ( +
+ {numAbbr(totalCount)} + + + + + +
+ ) +} + +export default ArticlesCount diff --git a/src/views/TagDetail/ArticlesCount/styles.css b/src/views/TagDetail/ArticlesCount/styles.css new file mode 100644 index 0000000000..0393892aac --- /dev/null +++ b/src/views/TagDetail/ArticlesCount/styles.css @@ -0,0 +1,8 @@ +.container { + font-size: var(--font-size-sm); + color: var(--color-grey-darker); + + & b { + color: var(--color-black); + } +} diff --git a/src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/Content.tsx b/src/views/TagDetail/Buttons/AddButton/TagArticleDialog/Content.tsx similarity index 79% rename from src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/Content.tsx rename to src/views/TagDetail/Buttons/AddButton/TagArticleDialog/Content.tsx index 84470d6752..50180280f8 100644 --- a/src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/Content.tsx +++ b/src/views/TagDetail/Buttons/AddButton/TagArticleDialog/Content.tsx @@ -12,22 +12,27 @@ import { LanguageContext, Translate, useResponsive, + ViewerContext, } from '~/components' import { useMutation } from '~/components/GQL' import SEARCH_ARTICLES from '~/components/GQL/queries/searchArticles' +import updateTagArticlesCount from '~/components/GQL/updates/tagArticlesCount' import { ADD_TOAST, REFETCH_TAG_DETAIL_ARTICLES, TEXT } from '~/common/enums' import { parseFormSubmitErrors, translate } from '~/common/utils' import styles from './styles.css' -import { PutArticlesTags } from './__generated__/PutArticlesTags' +import { TagDetailPublic_node_Tag } from '../../../__generated__/TagDetailPublic' +import { AddArticlesTags } from './__generated__/AddArticlesTags' -const PUT_ARTICLES_TAGS = gql` - mutation PutArticlesTags($id: ID!, $articles: [ID!]) { - putArticlesTags(input: { id: $id, articles: $articles }) { +const ADD_ARTICLES_TAGS = gql` + mutation AddArticlesTags($id: ID!, $articles: [ID!], $selected: Boolean) { + addArticlesTags( + input: { id: $id, articles: $articles, selected: $selected } + ) { id - articles(input: { first: 0, selected: true }) { + articles(input: { first: 0, selected: $selected }) { totalCount } } @@ -58,8 +63,9 @@ const DropdownContent = ({ } interface TagArticleDialogContentProps { - id?: string closeDialog: () => void + forSelected?: boolean + tag: TagDetailPublic_node_Tag } interface FormValues { @@ -69,12 +75,14 @@ interface FormValues { const TagArticleDialogContent: React.FC = ({ closeDialog, - id, + forSelected = false, + tag, }) => { const isSmallUp = useResponsive('sm-up') const [selectedArticles, setSelectedArticles] = useState([]) - const [update] = useMutation(PUT_ARTICLES_TAGS) + const [add] = useMutation(ADD_ARTICLES_TAGS) const { lang } = useContext(LanguageContext) + const viewer = useContext(ViewerContext) const formId = 'put-article-tag-form' @@ -106,11 +114,25 @@ const TagArticleDialogContent: React.FC = ({ }, onSubmit: async ({ name, articles }, { setFieldError, setSubmitting }) => { try { - if (!id) { + if (!tag.id) { return } - await update({ variables: { id, articles } }) + await add({ + variables: { id: tag.id, articles, selected: forSelected }, + update: (cache, { data }) => { + if (forSelected) { + const newCount = data?.addArticlesTags?.articles?.totalCount || 0 + const oldCount = tag.articles.totalCount || 0 + updateTagArticlesCount({ + cache, + id: tag.id, + count: newCount - oldCount, + type: 'increment', + }) + } + }, + }) setSubmitting(false) @@ -179,6 +201,7 @@ const TagArticleDialogContent: React.FC = ({ dropdownCallback={onClickMenuItem} DropdownContent={DropdownContent} query={SEARCH_ARTICLES} + queryFilter={forSelected ? undefined : { authorId: viewer.id }} />
    @@ -226,7 +249,7 @@ const TagArticleDialogContent: React.FC = ({ return ( <> diff --git a/src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/index.tsx b/src/views/TagDetail/Buttons/AddButton/TagArticleDialog/index.tsx similarity index 69% rename from src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/index.tsx rename to src/views/TagDetail/Buttons/AddButton/TagArticleDialog/index.tsx index cdb856edb6..278d65ec98 100644 --- a/src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/index.tsx +++ b/src/views/TagDetail/Buttons/AddButton/TagArticleDialog/index.tsx @@ -4,12 +4,19 @@ import { Dialog } from '~/components' import Content from './Content' +import { TagDetailPublic_node_Tag } from '../../../__generated__/TagDetailPublic' + interface TagArticleDialogProps { - id?: string children: ({ open }: { open: () => void }) => React.ReactNode + forSelected?: boolean + tag: TagDetailPublic_node_Tag } -const TagArticleDialog = ({ id, children }: TagArticleDialogProps) => { +const TagArticleDialog = ({ + children, + forSelected, + tag, +}: TagArticleDialogProps) => { const [showDialog, setShowDialog] = useState(true) const open = () => setShowDialog(true) const close = () => setShowDialog(false) @@ -19,7 +26,7 @@ const TagArticleDialog = ({ id, children }: TagArticleDialogProps) => { {children({ open })} - + ) diff --git a/src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/styles.css b/src/views/TagDetail/Buttons/AddButton/TagArticleDialog/styles.css similarity index 100% rename from src/views/TagDetail/TagDetailButtons/AddArticleButton/TagArticleDialog/styles.css rename to src/views/TagDetail/Buttons/AddButton/TagArticleDialog/styles.css diff --git a/src/views/TagDetail/Buttons/AddButton/index.tsx b/src/views/TagDetail/Buttons/AddButton/index.tsx new file mode 100644 index 0000000000..7c3e64b040 --- /dev/null +++ b/src/views/TagDetail/Buttons/AddButton/index.tsx @@ -0,0 +1,209 @@ +import { useContext } from 'react' + +import { + Button, + DropdownDialog, + IconAddMedium, + IconHashTag, + IconPen, + IconSpinner, + LanguageContext, + LikeCoinDialog, + Menu, + TextIcon, + Translate, + ViewerContext, +} from '~/components' +import { useMutation } from '~/components/GQL' +import CREATE_DRAFT from '~/components/GQL/mutations/createDraft' + +import { ADD_TOAST } from '~/common/enums' +import { + analytics, + parseFormSubmitErrors, + routerPush, + toPath, + translate, +} from '~/common/utils' + +import TagArticleDialog from './TagArticleDialog' + +import { CreateDraft } from '~/components/GQL/mutations/__generated__/CreateDraft' +import { TagDetailPublic_node_Tag } from '../../__generated__/TagDetailPublic' + +interface DropdownActionsProps { + isMaintainer: boolean + tag: TagDetailPublic_node_Tag +} + +interface DialogProps { + openTagSelectedArticleDialog: () => void + openTagArticleDialog: () => void +} + +type BaseDropdownActionsProps = DropdownActionsProps & DialogProps + +const BaseCreateDraftMenuItem = ({ onClick }: { onClick: () => any }) => ( + + } size="md" spacing="base"> + + + +) + +const CreateDraftMenuItem = ({ + putDraft, +}: { + putDraft: () => Promise +}) => { + const { lang } = useContext(LanguageContext) + const viewer = useContext(ViewerContext) + + if (viewer.shouldSetupLikerID) { + return ( + + {({ open }) => } + + ) + } + + return ( + { + try { + if (viewer.isInactive) { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: , + }, + }) + ) + return + } + + analytics.trackEvent('click_button', { + type: 'write', + }) + const result = await putDraft() + const { slug, id } = result?.data?.putDraft || {} + + if (slug && id) { + const path = toPath({ page: 'draftDetail', slug, id }) + routerPush(path.href, path.as) + } + } catch (error) { + const [messages, codes] = parseFormSubmitErrors(error, lang) + + if (!messages[codes[0]]) { + return null + } + + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: messages[codes[0]], + }, + }) + ) + } + }} + /> + ) +} + +const BaseDropdownActions = ({ + isMaintainer, + tag, + openTagSelectedArticleDialog, + openTagArticleDialog, +}: BaseDropdownActionsProps) => { + const { lang } = useContext(LanguageContext) + const [putDraft, { loading }] = useMutation(CREATE_DRAFT, { + variables: { + title: translate({ id: 'untitle', lang }), + tags: [tag.content], + }, + }) + + const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( + + {isMaintainer && ( + <> + + } + size="md" + spacing="base" + > + + + + + + )} + + + } size="md" spacing="base"> + + + + + ) + + return ( + , + placement: 'bottom-end', + }} + dialog={{ + content: , + title: 'moreActions', + }} + > + {({ open, ref }) => ( + + )} + + ) +} + +const DropdownActions = (props: DropdownActionsProps) => { + return ( + + {({ open: openTagSelectedArticleDialog }) => ( + + {({ open: openTagArticleDialog }) => ( + + )} + + )} + + ) +} + +export default DropdownActions diff --git a/src/views/TagDetail/Buttons/FollowButton/Follow.tsx b/src/views/TagDetail/Buttons/FollowButton/Follow.tsx new file mode 100644 index 0000000000..ff901c1d8a --- /dev/null +++ b/src/views/TagDetail/Buttons/FollowButton/Follow.tsx @@ -0,0 +1,62 @@ +import _isNil from 'lodash/isNil' +import { useContext } from 'react' + +import { + Button, + IconAdd, + TextIcon, + Translate, + ViewerContext, +} from '~/components' +import { useMutation } from '~/components/GQL' +import TOGGLE_FOLLOW_TAG from '~/components/GQL/mutations/toggleFollowTag' +import updateTagFollowers from '~/components/GQL/updates/tagFollowers' + +import { ToggleFollowTag } from '~/components/GQL/mutations/__generated__/ToggleFollowTag' +import { FollowButtonTagPrivate } from './__generated__/FollowButtonTagPrivate' + +interface FollowProps { + tag: FollowButtonTagPrivate +} + +const Follow = ({ tag }: FollowProps) => { + const viewer = useContext(ViewerContext) + const [follow] = useMutation(TOGGLE_FOLLOW_TAG, { + variables: { id: tag.id, enabled: true }, + optimisticResponse: + !_isNil(tag.id) && !_isNil(tag.isFollower) + ? { + toggleFollowTag: { + id: tag.id, + isFollower: true, + __typename: 'Tag', + }, + } + : undefined, + update: (cache) => { + updateTagFollowers({ + cache, + id: tag.id, + type: 'follow', + viewer, + }) + }, + }) + + return ( + + ) +} + +export default Follow diff --git a/src/views/TagDetail/Buttons/FollowButton/Unfollow.tsx b/src/views/TagDetail/Buttons/FollowButton/Unfollow.tsx new file mode 100644 index 0000000000..4c65de9cc7 --- /dev/null +++ b/src/views/TagDetail/Buttons/FollowButton/Unfollow.tsx @@ -0,0 +1,58 @@ +import _isNil from 'lodash/isNil' +import { useContext, useState } from 'react' + +import { Button, TextIcon, Translate, ViewerContext } from '~/components' +import { useMutation } from '~/components/GQL' +import TOGGLE_FOLLOW_TAG from '~/components/GQL/mutations/toggleFollowTag' +import updateTagFollowers from '~/components/GQL/updates/tagFollowers' + +import { ToggleFollowTag } from '~/components/GQL/mutations/__generated__/ToggleFollowTag' +import { FollowButtonTagPrivate } from './__generated__/FollowButtonTagPrivate' + +interface UnfollowTagProps { + tag: FollowButtonTagPrivate +} + +const Unfollow = ({ tag }: UnfollowTagProps) => { + const viewer = useContext(ViewerContext) + const [hover, setHover] = useState(false) + const [unfollow] = useMutation(TOGGLE_FOLLOW_TAG, { + variables: { id: tag.id, enabled: false }, + optimisticResponse: + !_isNil(tag.id) && !_isNil(tag.isFollower) + ? { + toggleFollowTag: { + id: tag.id, + isFollower: false, + __typename: 'Tag', + }, + } + : undefined, + update: (cache) => { + updateTagFollowers({ + cache, + type: 'unfollow', + id: tag.id, + viewer, + }) + }, + }) + + return ( + + ) +} + +export default Unfollow diff --git a/src/views/TagDetail/Buttons/FollowButton/index.tsx b/src/views/TagDetail/Buttons/FollowButton/index.tsx new file mode 100644 index 0000000000..5e375fb6c3 --- /dev/null +++ b/src/views/TagDetail/Buttons/FollowButton/index.tsx @@ -0,0 +1,33 @@ +import gql from 'graphql-tag' + +import Follow from './Follow' +import Unfollow from './Unfollow' + +import { FollowButtonTagPrivate } from './__generated__/FollowButtonTagPrivate' + +interface FollowButtonProps { + tag: FollowButtonTagPrivate +} + +const fragments = { + tag: { + private: gql` + fragment FollowButtonTagPrivate on Tag { + id + isFollower + } + `, + }, +} + +const FollowButton = ({ tag }: FollowButtonProps) => { + if (tag.isFollower) { + return + } else { + return + } +} + +FollowButton.fragments = fragments + +export default FollowButton diff --git a/src/views/TagDetail/Buttons/index.tsx b/src/views/TagDetail/Buttons/index.tsx new file mode 100644 index 0000000000..7c8d964ce8 --- /dev/null +++ b/src/views/TagDetail/Buttons/index.tsx @@ -0,0 +1,7 @@ +import AddButton from './AddButton' +import FollowButton from './FollowButton' + +export const TagDetailButtons = { + AddButton, + FollowButton, +} diff --git a/src/views/TagDetail/DropdownActions/index.tsx b/src/views/TagDetail/DropdownActions/index.tsx new file mode 100644 index 0000000000..7dc6c40793 --- /dev/null +++ b/src/views/TagDetail/DropdownActions/index.tsx @@ -0,0 +1,99 @@ +import { + Button, + DropdownDialog, + IconEdit, + IconMoreLarge, + IconShare, + Menu, + ShareDialog, + TagDialog, + TextIcon, + Translate, +} from '~/components' + +import { TEXT } from '~/common/enums' + +interface DropdownActionsProps { + id: string + content?: string + description?: string + isMaintainer: boolean +} + +interface DialogProps { + openShareDialog: () => void + openTagDialog: () => void +} + +type BaseDropdownActionsProps = DropdownActionsProps & DialogProps + +const BaseDropdownActions = ({ + id, + content, + description, + isMaintainer, + openShareDialog, + openTagDialog, +}: BaseDropdownActionsProps) => { + const Content = ({ isInDropdown }: { isInDropdown?: boolean }) => ( + + + } size="md" spacing="base"> + + + + {isMaintainer && ( + + } size="md" spacing="base"> + + + + )} + + ) + + return ( + , + placement: 'bottom-end', + }} + dialog={{ + content: , + title: 'moreActions', + }} + > + {({ open, ref }) => ( + + )} + + ) +} + +const DropdownActions = (props: DropdownActionsProps) => { + return ( + + {({ open: openShareDialog }) => ( + + {({ open: openTagDialog }) => ( + + )} + + )} + + ) +} + +export default DropdownActions diff --git a/src/views/TagDetail/Followers/index.tsx b/src/views/TagDetail/Followers/index.tsx new file mode 100644 index 0000000000..77f3bacc59 --- /dev/null +++ b/src/views/TagDetail/Followers/index.tsx @@ -0,0 +1,58 @@ +import { useQuery } from '@apollo/react-hooks' + +import { Translate } from '~/components' +import { Avatar } from '~/components/Avatar' +import TAG_FOLLOWERS from '~/components/GQL/queries/tagFollowers' + +import { IMAGE_PIXEL } from '~/common/enums' +import { numAbbr } from '~/common/utils' + +import styles from './styles.css' + +import { TagFollowers } from '~/components/GQL/queries/__generated__/TagFollowers' + +interface FollowersProps { + id: string +} + +const Followers = ({ id }: FollowersProps) => { + const { data } = useQuery(TAG_FOLLOWERS, { variables: { id } }) + + if (!data || !data.node || data.node.__typename !== 'Tag') { + return null + } + + const { edges, totalCount } = data?.node?.followers || { + edges: [], + totalCount: 0, + } + const followers = ( + edges?.map(({ node }) => node).filter((user) => !!user) || [] + ).slice(0, 5) + + return ( +
    +
    + {followers.map((user, index) => ( + + ))} +
    + +
    + {numAbbr(totalCount)} + + + +
    + + +
    + ) +} + +export default Followers diff --git a/src/views/TagDetail/Followers/styles.css b/src/views/TagDetail/Followers/styles.css new file mode 100644 index 0000000000..71fd6de2b8 --- /dev/null +++ b/src/views/TagDetail/Followers/styles.css @@ -0,0 +1,51 @@ +.container { + @mixin flex-center-start; + + margin: var(--spacing-base) 0; +} + +.count { + @mixin flex-center-all; + + padding-left: var(--spacing-x-tight); + font-size: var(--font-size-sm); + color: var(--color-grey-darker); + + & b { + margin-right: var(--spacing-xx-tight); + color: var(--color-black); + } + + & span { + margin-top: 2px; + } +} + +.avatar-list { + @mixin flex-center-start; + + & :global(> *) { + position: relative; + box-shadow: 0 0 0 2px var(--color-white); + + &:nth-child(1) { + z-index: 5; + } + + &:nth-child(2) { + z-index: 4; + } + + &:nth-child(3) { + z-index: 3; + } + + &:nth-child(4) { + z-index: 2; + } + + &:nth-child(5) { + z-index: 1; + } + } +} diff --git a/src/views/TagDetail/TagDetailArticles/Latest/index.tsx b/src/views/TagDetail/TagDetailArticles/Latest/index.tsx deleted file mode 100644 index c3aaf2f89d..0000000000 --- a/src/views/TagDetail/TagDetailArticles/Latest/index.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import { NetworkStatus } from 'apollo-client' -import _get from 'lodash/get' - -import { - ArticleDigestFeed, - EmptyTagArticles, - InfiniteScroll, - List, - Spinner, - useEventListener, - usePullToRefresh, -} from '~/components' -import { QueryError } from '~/components/GQL' -import TAG_ARTICLES from '~/components/GQL/queries/tagArticles' - -import { REFETCH_TAG_DETAIL_ARTICLES } from '~/common/enums' -import { analytics, mergeConnections } from '~/common/utils' - -import { - TagArticles, - TagArticles_node_Tag_articles, -} from '~/components/GQL/queries/__generated__/TagArticles' - -const LatestArticles = ({ id }: { id: string }) => { - const { data, loading, error, fetchMore, refetch, networkStatus } = useQuery< - TagArticles - >(TAG_ARTICLES, { - variables: { id }, - fetchPolicy: 'cache-and-network', - notifyOnNetworkStatusChange: true, - }) - - const connectionPath = 'node.articles' - const articles = _get(data, connectionPath) as TagArticles_node_Tag_articles - const { edges, pageInfo } = articles || { edges: [], pageInfo: {} } - const isNewLoading = networkStatus === NetworkStatus.loading - const hasArticles = edges && edges.length > 0 && pageInfo - - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'tag_detail_selected', - location: edges ? edges.length : 0, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - - const sync = ({ - event, - differences = 0, - }: { - event: 'add' | 'delete' - differences?: number - }) => { - const { edges: items } = _get(data, connectionPath, { edges: [] }) - switch (event) { - case 'add': - refetch({ - variables: { - id, - first: items.length + differences, - }, - }) - break - case 'delete': - refetch({ - variables: { - id, - first: Math.max(items.length - 1, 0), - }, - }) - break - } - } - - useEventListener(REFETCH_TAG_DETAIL_ARTICLES, sync) - usePullToRefresh.Handler(refetch) - - if (loading && (!articles || isNewLoading)) { - return - } - - if (error) { - return - } - - if (!data || !data.node || data.node.__typename !== 'Tag') { - return - } - - if (!hasArticles) { - return - } - - return ( - - - {(edges || []).map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: 'tag_detail_latest', - contentType: 'article', - styleType: 'title', - location: i, - }) - } - inTagDetailLatest - /> - - ))} - - - ) -} - -export default LatestArticles diff --git a/src/views/TagDetail/TagDetailArticles/Selected/index.tsx b/src/views/TagDetail/TagDetailArticles/Selected/index.tsx deleted file mode 100644 index 8c5214074b..0000000000 --- a/src/views/TagDetail/TagDetailArticles/Selected/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import { NetworkStatus } from 'apollo-client' -import _get from 'lodash/get' - -import { - ArticleDigestFeed, - EmptyTagArticles, - InfiniteScroll, - List, - Spinner, - useEventListener, - usePullToRefresh, -} from '~/components' -import { QueryError } from '~/components/GQL' -import TAG_ARTICLES from '~/components/GQL/queries/tagArticles' - -import { REFETCH_TAG_DETAIL_ARTICLES } from '~/common/enums' -import { analytics, mergeConnections } from '~/common/utils' - -import { - TagArticles, - TagArticles_node_Tag_articles, -} from '~/components/GQL/queries/__generated__/TagArticles' - -const SelectedArticles = ({ id }: { id: string }) => { - const { data, loading, error, fetchMore, refetch, networkStatus } = useQuery< - TagArticles - >(TAG_ARTICLES, { - variables: { id, selected: true }, - fetchPolicy: 'cache-and-network', - notifyOnNetworkStatusChange: true, - }) - - const connectionPath = 'node.articles' - const articles = _get(data, connectionPath) as TagArticles_node_Tag_articles - const { edges, pageInfo } = articles || { edges: [], pageInfo: {} } - const isNewLoading = networkStatus === NetworkStatus.loading - const hasArticles = edges && edges.length > 0 && pageInfo - - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'tag_detail_selected', - location: edges ? edges.length : 0, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - const sync = ({ - event, - differences = 0, - }: { - event: 'add' | 'delete' - differences?: number - }) => { - const { edges: items } = _get(data, 'node.articles', { edges: [] }) - switch (event) { - case 'add': - refetch({ - variables: { - id, - first: items.length + differences, - }, - }) - break - case 'delete': - refetch({ - variables: { - id, - first: Math.max(items.length - 1, 0), - }, - }) - break - } - } - - useEventListener(REFETCH_TAG_DETAIL_ARTICLES, sync) - usePullToRefresh.Handler(refetch) - - if (loading && (!articles || isNewLoading)) { - return - } - - if (error) { - return - } - - if (!data || !data.node || data.node.__typename !== 'Tag') { - return - } - - if (!hasArticles) { - return - } - - return ( - - - {(edges || []).map(({ node, cursor }, i) => ( - - - analytics.trackEvent('click_feed', { - type: 'tag_detail_selected', - styleType: 'title', - contentType: 'article', - location: i, - }) - } - inTagDetailSelected - /> - - ))} - - - ) -} - -export default SelectedArticles diff --git a/src/views/TagDetail/TagDetailArticles/index.tsx b/src/views/TagDetail/TagDetailArticles/index.tsx deleted file mode 100644 index 1a13bcd48e..0000000000 --- a/src/views/TagDetail/TagDetailArticles/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Latest from './Latest' -import Selected from './Selected' - -export const TagDetailArticles = { - Latest, - Selected, -} diff --git a/src/views/TagDetail/TagDetailButtons/AddArticleButton/index.tsx b/src/views/TagDetail/TagDetailButtons/AddArticleButton/index.tsx deleted file mode 100644 index 816e36c305..0000000000 --- a/src/views/TagDetail/TagDetailButtons/AddArticleButton/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, IconAdd, TextIcon, Translate } from '~/components' - -import TagArticleDialog from './TagArticleDialog' - -interface AddArticleButtonProps { - id?: string -} - -const AddArticleButton: React.FC = ({ id }) => { - return ( - - {({ open }) => ( - - )} - - ) -} - -export default AddArticleButton diff --git a/src/views/TagDetail/TagDetailButtons/EditTagButton/index.tsx b/src/views/TagDetail/TagDetailButtons/EditTagButton/index.tsx deleted file mode 100644 index 9d16a7c2ad..0000000000 --- a/src/views/TagDetail/TagDetailButtons/EditTagButton/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button, IconEdit, TagDialog, TextIcon, Translate } from '~/components' - -interface EditTagButtonProps { - id?: string - content?: string - description?: string -} - -const EditTagButton = (props: EditTagButtonProps) => ( - - {({ open }) => ( - - )} - -) - -export default EditTagButton diff --git a/src/views/TagDetail/TagDetailButtons/index.tsx b/src/views/TagDetail/TagDetailButtons/index.tsx deleted file mode 100644 index bb4dfe3432..0000000000 --- a/src/views/TagDetail/TagDetailButtons/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import AddArticleButton from './AddArticleButton' -import EditTagButton from './EditTagButton' - -export const TagDetailButtons = { - AddArticleButton, - EditTagButton, -} diff --git a/src/views/TagDetail/gql.ts b/src/views/TagDetail/gql.ts new file mode 100644 index 0000000000..373209e08b --- /dev/null +++ b/src/views/TagDetail/gql.ts @@ -0,0 +1,43 @@ +import gql from 'graphql-tag' + +import { UserDigest } from '~/components/UserDigest' + +import { TagDetailButtons } from './Buttons' + +export const TAG_DETAIL_PUBLIC = gql` + query TagDetailPublic($id: ID!) { + node(input: { id: $id }) { + ... on Tag { + id + content + creator { + id + ...UserDigestMiniUser + } + description + editors { + id + ...UserDigestMiniUser + } + articles(input: { first: 0, selected: true }) { + totalCount + } + ...FollowButtonTagPrivate + } + } + } + ${UserDigest.Mini.fragments.user} + ${TagDetailButtons.FollowButton.fragments.tag.private} +` + +export const TAG_DETAIL_PRIVATE = gql` + query TagDetailPrivate($id: ID!) { + node(input: { id: $id }) { + ... on Tag { + id + ...FollowButtonTagPrivate + } + } + } + ${TagDetailButtons.FollowButton.fragments.tag.private} +` diff --git a/src/views/TagDetail/index.tsx b/src/views/TagDetail/index.tsx index 80dda324a0..5459eb2f79 100644 --- a/src/views/TagDetail/index.tsx +++ b/src/views/TagDetail/index.tsx @@ -1,13 +1,12 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import _find from 'lodash/find' -import _get from 'lodash/get' import _some from 'lodash/some' import { useRouter } from 'next/router' -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import { EmptyTag, + Expandable, Head, Layout, PullToRefresh, @@ -15,7 +14,9 @@ import { Spinner, Tabs, Throw404, + Title, Translate, + usePullToRefresh, ViewerContext, } from '~/components' import { getErrorCodes, QueryError } from '~/components/GQL' @@ -24,37 +25,21 @@ import { UserDigest } from '~/components/UserDigest' import { ERROR_CODES } from '~/common/enums' import { getQuery } from '~/common/utils' +import TagDetailArticles from './Articles' +import ArticlesCount from './ArticlesCount' +import { TagDetailButtons } from './Buttons' +import DropdownActions from './DropdownActions' +import Followers from './Followers' +import { TAG_DETAIL_PRIVATE, TAG_DETAIL_PUBLIC } from './gql' import styles from './styles.css' -import { TagDetailArticles } from './TagDetailArticles' -import { TagDetailButtons } from './TagDetailButtons' - -import { TagDetail as TagDetailType } from './__generated__/TagDetail' - -const TAG_DETAIL = gql` - query TagDetail($id: ID!) { - node(input: { id: $id }) { - ... on Tag { - id - content - creator { - id - ...UserDigestMiniUser - } - description - editors { - id - ...UserDigestMiniUser - } - articles(input: { first: 0, selected: true }) { - totalCount - } - } - } - } - ${UserDigest.Mini.fragments.user} -` -type TagFeed = 'latest' | 'selected' +import { + TagDetailPublic, + TagDetailPublic_node_Tag, + TagDetailPublic_node_Tag_editors, +} from './__generated__/TagDetailPublic' + +type TagFeedType = 'latest' | 'selected' const EmptyLayout: React.FC = ({ children }) => ( @@ -63,100 +48,109 @@ const EmptyLayout: React.FC = ({ children }) => ( ) -const TagDetail = ({ data }: { data: TagDetailType }) => { +const TagDetail = ({ tag }: { tag: TagDetailPublic_node_Tag }) => { const viewer = useContext(ViewerContext) - const hasSelected = _get(data, 'node.articles.totalCount', 0) - const [feed, setFeed] = useState(hasSelected ? 'selected' : 'latest') - if (!data || !data.node || data.node.__typename !== 'Tag') { - return - } + // feed type + const hasSelected = (tag?.articles.totalCount || 0) > 0 + const [feed, setFeed] = useState( + hasSelected ? 'selected' : 'latest' + ) + const isSelected = feed === 'selected' - if (hasSelected === 0 && feed === 'selected') { - setFeed('latest') - } + useEffect(() => { + if (!hasSelected && isSelected) { + setFeed('latest') + } + }) - const editors = data.node.editors || [] - const maintainer = _find( - editors, - (editor) => (editor.displayName || '').toLowerCase() !== 'matty' - ) - const isEditor = _some(editors, (editor) => editor.id === viewer.id) - const isCreator = data.node.creator?.id === viewer.id - const canEdit = - isEditor || isCreator || viewer.info.email === 'hi@matters.news' + // define permission + const filter = ({ displayName }: TagDetailPublic_node_Tag_editors) => + (displayName || '').toLowerCase() !== 'matty' + const editors = tag?.editors || [] + const owner = _find(editors, filter) + const normalEditors = editors.filter(filter) + const isEditor = _some(editors, (editor) => editor.id === viewer.id) + const isCreator = tag?.creator?.id === viewer.id + const isMaintainer = + isEditor || + (normalEditors.length === 0 && isCreator) || + viewer.info.email === 'hi@matters.news' + + /** + * Render + */ return ( } right={ <> - + - {canEdit && ( -
    - - -
    - )} + } /> - + - {maintainer && ( -
    - - - - +
    + {owner && ( +
    + + + + +
    + )} + + #{tag.content} + + {tag.description && ( + +

    {tag.description}

    +
    + )} + +
    + +
    - )} - {data.node.description && ( -

    {data.node.description}

    - )} +
    + + +
    +
    - {hasSelected > 0 && ( - setFeed('selected')} - > + {hasSelected && ( + setFeed('selected')}> )} - setFeed('latest')} - > + setFeed('latest')}> - {feed === 'selected' ? ( - - ) : ( - - )} + @@ -165,12 +159,49 @@ const TagDetail = ({ data }: { data: TagDetailType }) => { } const TagDetailContainer = () => { + const viewer = useContext(ViewerContext) const router = useRouter() const tagId = getQuery({ router, key: 'tagId' }) - const { data, loading, error } = useQuery(TAG_DETAIL, { + + /** + * Data Fetching + */ + // public data + const { data, loading, error, refetch: refetchPublic, client } = useQuery< + TagDetailPublic + >(TAG_DETAIL_PUBLIC, { variables: { id: tagId }, }) + // private data + const loadPrivate = () => { + if (!viewer.id || !tagId) { + return + } + + client.query({ + query: TAG_DETAIL_PRIVATE, + fetchPolicy: 'network-only', + variables: { id: tagId }, + }) + } + + // fetch private data for first page + useEffect(() => { + loadPrivate() + }, [tagId, viewer.id]) + + // refetch & pull to refresh + const refetch = async () => { + await refetchPublic() + loadPrivate() + } + usePullToRefresh.Register() + usePullToRefresh.Handler(refetch) + + /** + * Render + */ if (loading) { return ( @@ -205,7 +236,7 @@ const TagDetailContainer = () => { ) } - return + return } export default TagDetailContainer diff --git a/src/views/TagDetail/styles.css b/src/views/TagDetail/styles.css index d0ec9a93a4..27889f2a42 100644 --- a/src/views/TagDetail/styles.css +++ b/src/views/TagDetail/styles.css @@ -1,13 +1,14 @@ -.buttons { - & :global(> * + *) { - margin-left: var(--spacing-base); - } +.info { + @mixin border-bottom-grey; + + padding: 0 var(--spacing-base) var(--spacing-loose); + margin-bottom: var(--spacing-x-tight); } -.maintainer { +.owner { @mixin flex-center-start; - padding: 0 var(--spacing-base) var(--spacing-base); + padding: 0 0 var(--spacing-base); & span { margin-left: var(--spacing-x-tight); @@ -17,8 +18,19 @@ } .description { - padding: 0 var(--spacing-base); - font-size: var(--font-size-sm); + padding-top: 1rem; + font-size: var(--font-size-md-s); + line-height: 1.33333333; color: var(--color-grey-darker); white-space: pre-wrap; } + +.statistics { + @mixin flex-center-space-between; +} + +.buttons { + @mixin flex-center-space-between; + + padding-top: 1rem; +} diff --git a/src/views/Tags/index.tsx b/src/views/Tags/index.tsx index 450c20e8ea..b95a6aba9f 100644 --- a/src/views/Tags/index.tsx +++ b/src/views/Tags/index.tsx @@ -22,11 +22,11 @@ import { QueryError } from '~/components/GQL' import { analytics, mergeConnections, toPath } from '~/common/utils' -import { AllTags } from './__generated__/AllTags' +import { AllTagsPublic } from './__generated__/AllTagsPublic' const ALL_TAGS = gql` - query AllTags($after: String) { - viewer { + query AllTagsPublic($after: String) { + viewer @connection(key: "viewerTags") { id recommendation { tags(input: { first: 20, after: $after }) { @@ -74,7 +74,7 @@ const CreateTagButton = () => { } const Tags = () => { - const { data, loading, error, fetchMore, refetch } = useQuery( + const { data, loading, error, fetchMore, refetch } = useQuery( ALL_TAGS ) diff --git a/src/views/User/Articles/UserArticles.tsx b/src/views/User/Articles/UserArticles.tsx index 67dcc085ab..67580992b8 100644 --- a/src/views/User/Articles/UserArticles.tsx +++ b/src/views/User/Articles/UserArticles.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/react-hooks' import { useRouter } from 'next/router' -import { useContext } from 'react' +import { useContext, useEffect } from 'react' import { ArticleDigestFeed, @@ -15,7 +15,11 @@ import { ViewerContext, } from '~/components' import { QueryError } from '~/components/GQL' -import USER_ARTICLES from '~/components/GQL/queries/userArticles' +import { + USER_ARTICLES_PRIVATE, + USER_ARTICLES_PUBLIC, + VIEWER_ARTICLES, +} from '~/components/GQL/queries/userArticles' import { analytics, getQuery, mergeConnections } from '~/common/utils' @@ -25,11 +29,11 @@ import UserTabs from '../UserTabs' import styles from './styles.css' import { - UserArticles as UserArticlesTypes, - UserArticles_user, -} from '~/components/GQL/queries/__generated__/UserArticles' + UserArticlesPublic, + UserArticlesPublic_user, +} from '~/components/GQL/queries/__generated__/UserArticlesPublic' -const ArticleSummaryInfo = ({ user }: { user: UserArticles_user }) => { +const ArticleSummaryInfo = ({ user }: { user: UserArticlesPublic_user }) => { const { articleCount: articles, totalWordCount: words } = user.status || { articleCount: 0, totalWordCount: 0, @@ -56,15 +60,87 @@ const UserArticles = () => { const viewer = useContext(ViewerContext) const router = useRouter() const userName = getQuery({ router, key: 'userName' }) + const isViewer = viewer.userName === userName + + let query = USER_ARTICLES_PUBLIC + if (isViewer) { + query = VIEWER_ARTICLES + } - const { data, loading, error, fetchMore, refetch } = useQuery< - UserArticlesTypes - >(USER_ARTICLES, { variables: { userName } }) + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(query, { + variables: { userName }, + }) + + // pagination + const connectionPath = 'user.articles' const user = data?.user + const { edges, pageInfo } = user?.articles || {} + + // private data + const loadPrivate = (publicData?: UserArticlesPublic) => { + if (!viewer.id || isViewer || !publicData || !user) { + return + } + + const publiceEdges = publicData.user?.articles?.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: USER_ARTICLES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + loadPrivate(data) + }, [user?.id, viewer.id]) + + // load next page + const loadMore = async () => { + analytics.trackEvent('load_more', { + type: 'user_article', + location: edges?.length || 0, + }) + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } usePullToRefresh.Register() usePullToRefresh.Handler(refetch) + /** + * Render + */ if (loading) { return } @@ -77,9 +153,6 @@ const UserArticles = () => { return null } - const connectionPath = 'user.articles' - const { edges, pageInfo } = user.articles - const CustomHead = () => ( { ) } - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'user_article', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } + const articleEdges = edges.filter( + ({ node }) => node.articleState === 'active' || viewer.id === node.author.id + ) return ( <> @@ -129,31 +188,22 @@ const UserArticles = () => { - {edges.map(({ node, cursor }, i) => { - if ( - node.articleState !== 'active' && - viewer.id !== node.author.id - ) { - return null - } - - return ( - - - analytics.trackEvent('click_feed', { - type: 'user_article', - contentType: 'article', - styleType: 'no_cover', - location: i, - }) - } - /> - - ) - })} + {articleEdges.map(({ node, cursor }, i) => ( + + + analytics.trackEvent('click_feed', { + type: 'user_article', + contentType: 'article', + styleType: 'no_cover', + location: i, + }) + } + /> + + ))} diff --git a/src/views/User/Comments/UserComments.tsx b/src/views/User/Comments/UserComments.tsx new file mode 100644 index 0000000000..781277a624 --- /dev/null +++ b/src/views/User/Comments/UserComments.tsx @@ -0,0 +1,226 @@ +import { useQuery } from '@apollo/react-hooks' +import _flatten from 'lodash/flatten' +import { useRouter } from 'next/router' +import { useContext, useEffect } from 'react' + +import { + ArticleDigestTitle, + Card, + Comment, + EmptyComment, + Head, + InfiniteScroll, + List, + Spinner, + usePullToRefresh, + ViewerContext, +} from '~/components' +import { QueryError } from '~/components/GQL' + +import { + filterComments, + getQuery, + mergeConnections, + toPath, +} from '~/common/utils' + +import IMAGE_LOGO_192 from '@/public/static/icon-192x192.png?url' + +import UserTabs from '../UserTabs' +import { USER_COMMENTS_PRIVATE, USER_COMMENTS_PUBLIC, USER_ID } from './gql' + +import { + UserCommentsPublic, + UserCommentsPublic_node_User_commentedArticles_edges_node_comments_edges_node, +} from './__generated__/UserCommentsPublic' +import { UserIdUser } from './__generated__/UserIdUser' + +type CommentedArticleComment = UserCommentsPublic_node_User_commentedArticles_edges_node_comments_edges_node + +const UserComments = () => { + const router = useRouter() + const userName = getQuery({ router, key: 'userName' }) + + const { data, loading, error } = useQuery(USER_ID, { + variables: { userName }, + }) + const user = data?.user + + if (loading) { + return + } + + if (error) { + return + } + + if (!user || user?.status?.state === 'archived') { + return null + } + + return ( + <> + + + + + ) +} + +const BaseUserComments = ({ user }: UserIdUser) => { + const viewer = useContext(ViewerContext) + + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(USER_COMMENTS_PUBLIC, { + variables: { id: user?.id }, + }) + + // pagination + const connectionPath = 'node.commentedArticles' + const { edges, pageInfo } = + (data?.node?.__typename === 'User' && + data.node.commentedArticles && + data.node.commentedArticles) || + {} + + // private data + const loadPrivate = (publicData?: UserCommentsPublic) => { + if (!viewer.id || !publicData || !user) { + return + } + + const articles = + publicData?.node?.__typename === 'User' + ? publicData.node.commentedArticles.edges || [] + : [] + const publiceNodes = _flatten( + articles.map(({ node }) => + (node.comments.edges || []).map(({ node: comment }) => comment) + ) + ) + const publicIds = publiceNodes.map((comment) => comment.id) + + client.query({ + query: USER_COMMENTS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + loadPrivate(data) + }, [user?.id, viewer.id]) + + // load next page + const loadMore = async () => { + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } + usePullToRefresh.Register() + usePullToRefresh.Handler(refetch) + + /** + * Render + */ + if (!user || !user.id) { + return null + } + + if (loading) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const articleEdges = edges + .map((edge) => { + const commentEdges = edge.node.comments.edges || [] + const comments = filterComments( + commentEdges.map(({ node }) => node) + ) + return { ...edge, comments } + }) + .filter(({ comments }) => comments.length > 0) + + return ( + + + {articleEdges.map(({ cursor, node, comments }) => ( + + + + + + + {comments.map((comment) => ( + + + + + + ))} + + + ))} + + + ) +} + +export default UserComments diff --git a/src/views/User/Comments/UserComments/index.tsx b/src/views/User/Comments/UserComments/index.tsx deleted file mode 100644 index b607779efb..0000000000 --- a/src/views/User/Comments/UserComments/index.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' -import { useRouter } from 'next/router' - -import { - ArticleDigestTitle, - Card, - Comment, - EmptyComment, - Head, - InfiniteScroll, - List, - Spinner, - usePullToRefresh, -} from '~/components' -import { QueryError } from '~/components/GQL' - -import { - filterComments, - getQuery, - mergeConnections, - toPath, -} from '~/common/utils' - -import IMAGE_LOGO_192 from '@/public/static/icon-192x192.png?url' - -import UserTabs from '../../UserTabs' - -import { - UserCommentFeed, - UserCommentFeed_node_User_commentedArticles_edges_node_comments_edges_node, -} from './__generated__/UserCommentFeed' -import { UserIdUser } from './__generated__/UserIdUser' - -const USER_ID = gql` - query UserIdUser($userName: String!) { - user(input: { userName: $userName }) { - id - displayName - info { - description - } - status { - state - } - } - } -` - -const USER_COMMENT_FEED = gql` - query UserCommentFeed($id: ID!, $after: String) { - node(input: { id: $id }) { - ... on User { - id - commentedArticles(input: { first: 5, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - id - ...ArticleDigestTitleArticle - comments(input: { filter: { author: $id }, first: null }) { - edges { - cursor - node { - ...FeedCommentPublic - ...FeedCommentPrivate - } - } - } - } - } - } - } - } - } - ${ArticleDigestTitle.fragments.article} - ${Comment.Feed.fragments.comment.public} - ${Comment.Feed.fragments.comment.private} -` - -const UserCommentsWrap = () => { - const router = useRouter() - const userName = getQuery({ router, key: 'userName' }) - - const { data, loading, error } = useQuery(USER_ID, { - variables: { userName }, - }) - const user = data?.user - - if (loading) { - return - } - - if (error) { - return - } - - if (!user || user?.status?.state === 'archived') { - return null - } - - return ( - <> - - - - - ) -} - -const UserComments = ({ user }: UserIdUser) => { - const { data, loading, error, fetchMore, refetch } = useQuery< - UserCommentFeed - >(USER_COMMENT_FEED, { - variables: { id: user?.id }, - }) - - usePullToRefresh.Register() - usePullToRefresh.Handler(refetch) - - if (!user || !user.id) { - return null - } - - if (loading) { - return - } - - if (error) { - return - } - - const connectionPath = 'node.commentedArticles' - const { edges, pageInfo } = - (data && - data.node && - data.node.__typename === 'User' && - data.node.commentedArticles && - data.node.commentedArticles) || - {} - - if (!edges || edges.length <= 0 || !pageInfo) { - return - } - - const loadMore = () => - fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - - return ( - - - {edges.map((articleEdge) => { - const commentEdges = articleEdge.node.comments.edges - const filteredComments = filterComments( - (commentEdges || []).map(({ node }) => node) - ) as UserCommentFeed_node_User_commentedArticles_edges_node_comments_edges_node[] - - if (filteredComments.length <= 0) { - return null - } - - return ( - - - - - - - {filteredComments.map((comment) => ( - - - - - - ))} - - - ) - })} - - - ) -} - -export default UserCommentsWrap diff --git a/src/views/User/Comments/gql.ts b/src/views/User/Comments/gql.ts new file mode 100644 index 0000000000..c8e199400b --- /dev/null +++ b/src/views/User/Comments/gql.ts @@ -0,0 +1,66 @@ +import gql from 'graphql-tag' + +import { ArticleDigestTitle, Comment } from '~/components' + +export const USER_ID = gql` + query UserIdUser($userName: String!) { + user(input: { userName: $userName }) { + id + displayName + info { + description + } + status { + state + } + } + } +` + +export const USER_COMMENTS_PUBLIC = gql` + query UserCommentsPublic($id: ID!, $after: String) { + node(input: { id: $id }) { + ... on User { + id + commentedArticles(input: { first: 5, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + id + ...ArticleDigestTitleArticle + comments(input: { filter: { author: $id }, first: null }) { + edges { + cursor + node { + ...FeedCommentPublic + ...FeedCommentPrivate + } + } + } + } + } + } + } + } + } + ${ArticleDigestTitle.fragments.article} + ${Comment.Feed.fragments.comment.public} + ${Comment.Feed.fragments.comment.private} +` + +export const USER_COMMENTS_PRIVATE = gql` + query UserCommentsPrivate($ids: [ID!]!) { + nodes(input: { ids: $ids }) { + id + ... on Comment { + ...FeedCommentPrivate + } + } + } + ${Comment.Feed.fragments.comment.private} +` diff --git a/src/views/User/Followees/UserFollowees.tsx b/src/views/User/Followees/UserFollowees.tsx index 0306a1cb00..d0ed7b8099 100644 --- a/src/views/User/Followees/UserFollowees.tsx +++ b/src/views/User/Followees/UserFollowees.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@apollo/react-hooks' -import gql from 'graphql-tag' import { useRouter } from 'next/router' +import { useContext, useEffect } from 'react' import { EmptyWarning, @@ -10,52 +10,96 @@ import { Spinner, Translate, usePullToRefresh, + ViewerContext, } from '~/components' import { QueryError } from '~/components/GQL' import { UserDigest } from '~/components/UserDigest' import { analytics, getQuery, mergeConnections } from '~/common/utils' -import { UserFolloweeFeed } from './__generated__/UserFolloweeFeed' - -const USER_FOLLOWEES_FEED = gql` - query UserFolloweeFeed($userName: String!, $after: String) { - user(input: { userName: $userName }) { - id - displayName - followees(input: { first: 20, after: $after }) { - pageInfo { - startCursor - endCursor - hasNextPage - } - edges { - cursor - node { - ...UserDigestRichUserPublic - ...UserDigestRichUserPrivate - } - } - } - } - } - ${UserDigest.Rich.fragments.user.public} - ${UserDigest.Rich.fragments.user.private} -` +import { USER_FOLLOWEES_PRIVATE, USER_FOLLOWEES_PUBLIC } from './gql' + +import { UserFolloweePublic } from './__generated__/UserFolloweePublic' const UserFollowees = () => { + const viewer = useContext(ViewerContext) const router = useRouter() const userName = getQuery({ router, key: 'userName' }) - const { data, loading, error, fetchMore, refetch } = useQuery< - UserFolloweeFeed - >(USER_FOLLOWEES_FEED, { + + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(USER_FOLLOWEES_PUBLIC, { variables: { userName }, }) + // pagination + const user = data?.user + const connectionPath = 'user.followees' + const { edges, pageInfo } = user?.followees || {} + + // private data + const loadPrivate = (publicData?: UserFolloweePublic) => { + if (!viewer.id || !publicData || !user) { + return + } + + const publiceEdges = publicData.user?.followees.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: USER_FOLLOWEES_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + loadPrivate(data) + }, [user?.id, viewer.id]) + + // load next page + const loadMore = async () => { + analytics.trackEvent('load_more', { + type: 'followee', + location: edges?.length || 0, + }) + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } usePullToRefresh.Register() usePullToRefresh.Handler(refetch) - if (loading || !data || !data.user) { + /** + * Render + */ + if (loading || !data || !user) { return } @@ -63,10 +107,6 @@ const UserFollowees = () => { return } - const user = data.user - const connectionPath = 'user.followees' - const { edges, pageInfo } = user.followees - if (!edges || edges.length <= 0 || !pageInfo) { return ( { ) } - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'followee', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - return ( <> { + const viewer = useContext(ViewerContext) const router = useRouter() const userName = getQuery({ router, key: 'userName' }) - const { data, loading, error, fetchMore, refetch } = useQuery< - UserFollowerFeed - >(USER_FOLLOWERS_FEED, { + + /** + * Data Fetching + */ + // public data + const { + data, + loading, + error, + fetchMore, + refetch: refetchPublic, + client, + } = useQuery(USER_FOLLOWERS_PUBLIC, { variables: { userName }, }) + // pagination + const user = data?.user + const connectionPath = 'user.followers' + const { edges, pageInfo } = user?.followers || {} + + // private data + const loadPrivate = (publicData?: UserFollowerPublic) => { + if (!viewer.id || !publicData || !user) { + return + } + + const publiceEdges = publicData.user?.followers.edges || [] + const publicIds = publiceEdges.map(({ node }) => node.id) + + client.query({ + query: USER_FOLLOWERS_PRIVATE, + fetchPolicy: 'network-only', + variables: { ids: publicIds }, + }) + } + + // fetch private data for first page + useEffect(() => { + loadPrivate(data) + }, [user?.id, viewer.id]) + + // load next page + const loadMore = async () => { + analytics.trackEvent('load_more', { + type: 'follower', + location: edges?.length || 0, + }) + const { data: newData } = await fetchMore({ + variables: { + after: pageInfo?.endCursor, + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath, + }), + }) + + loadPrivate(newData) + } + + // refetch & pull to refresh + const refetch = async () => { + const { data: newData } = await refetchPublic() + loadPrivate(newData) + } usePullToRefresh.Register() usePullToRefresh.Handler(refetch) - if (loading || !data || !data.user) { + /** + * Render + */ + + if (loading || !data || !user) { return } @@ -63,10 +108,6 @@ const UserFollowers = () => { return } - const user = data.user - const connectionPath = 'user.followers' - const { edges, pageInfo } = user.followers - if (!edges || edges.length <= 0 || !pageInfo) { return ( { ) } - const loadMore = () => { - analytics.trackEvent('load_more', { - type: 'follower', - location: edges.length, - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor, - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath, - }), - }) - } - return ( <>