diff --git a/.vscode/settings.json b/.vscode/settings.json index df7560e217..ddc6cfc49b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,6 @@ "editor.codeActionsOnSave": { "source.fixAll.tslint": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 28923d0106..d6b8792c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2019-10-29 + +### Added + +- Block User #481 + +### Changed + +- Apollo with React Hooks! #450 +- Show userName in profile area #479 +- Error Codes: remove `USER_FOLLOW_FAILED`, add `ACTION_FAILED` #480 + ## [2.0.1] - 2019-10-21 ### Changed diff --git a/package-lock.json b/package-lock.json index 264639590c..aea94fef7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "1.14.1", + "version": "2.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -61,27 +61,60 @@ } }, "@apollo/react-components": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.2.tgz", - "integrity": "sha512-D1habJ8IvylC8KpgzlM6yYskYGcTYuyOU5cPgtluamTc4ro6P/98bILMO4qHDDj0zkBARIgHrf2QV6oityTfvA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@apollo/react-components/-/react-components-3.1.3.tgz", + "integrity": "sha512-H0l2JKDQMz+LkM93QK7j3ThbNXkWQCduN3s3eKxFN3Rdg7rXsrikJWvx2wQ868jmqy0VhwJbS1vYdRLdh114uQ==", "requires": { - "@apollo/react-common": "^3.1.2", - "@apollo/react-hooks": "^3.1.2", + "@apollo/react-common": "^3.1.3", + "@apollo/react-hooks": "^3.1.3", "prop-types": "^15.7.2", "ts-invariant": "^0.4.4", "tslib": "^1.10.0" + }, + "dependencies": { + "@apollo/react-common": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.3.tgz", + "integrity": "sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg==", + "requires": { + "ts-invariant": "^0.4.4", + "tslib": "^1.10.0" + } + }, + "@apollo/react-hooks": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@apollo/react-hooks/-/react-hooks-3.1.3.tgz", + "integrity": "sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA==", + "requires": { + "@apollo/react-common": "^3.1.3", + "@wry/equality": "^0.1.9", + "ts-invariant": "^0.4.4", + "tslib": "^1.10.0" + } + } } }, "@apollo/react-hoc": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.2.tgz", - "integrity": "sha512-VbykBrxPBurt/yIAK8oFg7ZHL5ls2QI1y93AtLqJNwe4oM0m3oJC2jGIr2jobSVhbmGdzIGWshKJTbtrRowQ3g==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@apollo/react-hoc/-/react-hoc-3.1.3.tgz", + "integrity": "sha512-oCPma0uBVPTcYTR5sOvtMbpaWll4xDBvYfKr6YkDorUcQVeNzFu1LK1kmQjJP64bKsaziKYji5ibFaeCnVptmA==", "requires": { - "@apollo/react-common": "^3.1.2", - "@apollo/react-components": "^3.1.2", + "@apollo/react-common": "^3.1.3", + "@apollo/react-components": "^3.1.3", "hoist-non-react-statics": "^3.3.0", "ts-invariant": "^0.4.4", "tslib": "^1.10.0" + }, + "dependencies": { + "@apollo/react-common": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@apollo/react-common/-/react-common-3.1.3.tgz", + "integrity": "sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg==", + "requires": { + "ts-invariant": "^0.4.4", + "tslib": "^1.10.0" + } + } } }, "@apollo/react-hooks": { @@ -109,6 +142,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@apollo/react-testing/-/react-testing-3.1.2.tgz", "integrity": "sha512-nADLe8ju6K7LinKy5bJnN8cXMeje/s7zNIfKESviQz09+AOORFHtDPlgQPwkaogNor4yTKFwrrwtvokhRu8tnQ==", + "dev": true, "requires": { "@apollo/react-common": "^3.1.2", "fast-json-stable-stringify": "^2.0.0", @@ -1683,59 +1717,59 @@ } }, "@sentry/browser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.7.0.tgz", - "integrity": "sha512-hbybYP5onstb8PfqjCubMuXkoXQBjZ3RCaxrOFLOIqpIxajrQ2zmbnaCzfBPWWwKeHa9P+i625OT973OhhHFMA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.7.1.tgz", + "integrity": "sha512-K0x1XhsHS8PPdtlVOLrKZyYvi5Vexs9WApdd214bO6KaGF296gJvH1mG8XOY0+7aA5i2A7T3ttcaJNDYS49lzw==", "requires": { - "@sentry/core": "5.7.0", - "@sentry/types": "5.7.0", - "@sentry/utils": "5.7.0", + "@sentry/core": "5.7.1", + "@sentry/types": "5.7.1", + "@sentry/utils": "5.7.1", "tslib": "^1.9.3" } }, "@sentry/core": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.7.0.tgz", - "integrity": "sha512-gQel0d7LBSWJGHc7gfZllYAu+RRGD9GcYGmkRfemurmDyDGQDf/sfjiBi8f9QxUc2iFTHnvIR5nMTyf0U3yl3Q==", - "requires": { - "@sentry/hub": "5.7.0", - "@sentry/minimal": "5.7.0", - "@sentry/types": "5.7.0", - "@sentry/utils": "5.7.0", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.7.1.tgz", + "integrity": "sha512-AOn3k3uVWh2VyajcHbV9Ta4ieDIeLckfo7UMLM+CTk2kt7C89SayDGayJMSsIrsZlL4qxBoLB9QY4W2FgAGJrg==", + "requires": { + "@sentry/hub": "5.7.1", + "@sentry/minimal": "5.7.1", + "@sentry/types": "5.7.1", + "@sentry/utils": "5.7.1", "tslib": "^1.9.3" } }, "@sentry/hub": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.7.0.tgz", - "integrity": "sha512-qNdYheJ6j4P9Sk0eqIINpJohImmu/+trCwFb4F8BGLQth5iGMVQD6D0YUrgjf4ZaQwfhw9tv4W6VEfF5tyASoA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.7.1.tgz", + "integrity": "sha512-evGh323WR073WSBCg/RkhlUmCQyzU0xzBzCZPscvcoy5hd4SsLE6t9Zin+WACHB9JFsRQIDwNDn+D+pj3yKsig==", "requires": { - "@sentry/types": "5.7.0", - "@sentry/utils": "5.7.0", + "@sentry/types": "5.7.1", + "@sentry/utils": "5.7.1", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.7.0.tgz", - "integrity": "sha512-0sizE2prS9nmfLyVUKmVzFFFqRNr9iorSCCejwnlRe3crqKqjf84tuRSzm6NkZjIyYj9djuuo9l9XN12NLQ/4A==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.7.1.tgz", + "integrity": "sha512-nS/Dg+jWAZtcxQW8wKbkkw4dYvF6uyY/vDiz/jFCaux0LX0uhgXAC9gMOJmgJ/tYBLJ64l0ca5LzpZa7BMJQ0g==", "requires": { - "@sentry/hub": "5.7.0", - "@sentry/types": "5.7.0", + "@sentry/hub": "5.7.1", + "@sentry/types": "5.7.1", "tslib": "^1.9.3" } }, "@sentry/types": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.7.0.tgz", - "integrity": "sha512-bFRVortg713dE2yJXNFgNe6sNBVVSkpoELLkGPatdVQi0dYc6OggIIX4UZZvkynFx72GwYqO1NOrtUcJY2gmMg==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.7.1.tgz", + "integrity": "sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==" }, "@sentry/utils": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.7.0.tgz", - "integrity": "sha512-XmwQpLqea9mj8x1N7P/l4JvnEb0Rn5Py5OtBgl0ctk090W+GB1uM8rl9mkMf6698o1s1Z8T/tI/QY0yFA5uZXg==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.7.1.tgz", + "integrity": "sha512-nhirUKj/qFLsR1i9kJ5BRvNyzdx/E2vorIsukuDrbo8e3iZ11JMgCOVrmC8Eq9YkHBqgwX4UnrPumjFyvGMZ2Q==", "requires": { - "@sentry/types": "5.7.0", + "@sentry/types": "5.7.1", "tslib": "^1.9.3" } }, @@ -1775,12 +1809,12 @@ } }, "@tippy.js/react": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tippy.js/react/-/react-3.0.1.tgz", - "integrity": "sha512-mLm+8LTyidXB6X2E9KVoL76Fp5JW16EmWOSwwNV8T7q9+eOFwLf8+cXtMSMCP6qY8gaO1oRCKcRr5tFFS0tIxA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@tippy.js/react/-/react-3.1.0.tgz", + "integrity": "sha512-UhhgqyYAZG8XSHrSlgmlUj+el9Rk48LYMPOI+Fcbg5w0ZF714HJ+32/xdwqANlDNuYxdF+pZQDitzd4eUDTEMQ==", "requires": { "prop-types": "^15.6.2", - "tippy.js": "^5.0.1" + "tippy.js": "^5.0.2" } }, "@types/anymatch": { @@ -1935,9 +1969,9 @@ } }, "@types/jest": { - "version": "24.0.18", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.18.tgz", - "integrity": "sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==", + "version": "24.0.19", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.19.tgz", + "integrity": "sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg==", "dev": true, "requires": { "@types/jest-diff": "*" @@ -2063,9 +2097,9 @@ "dev": true }, "@types/react": { - "version": "16.9.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz", - "integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==", + "version": "16.9.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.9.tgz", + "integrity": "sha512-L+AudFJkDukk+ukInYvpoAPyJK5q1GanFOINOJnM0w6tUgITuWvJ4jyoBPFL7z4/L8hGLd+K/6xR5uUjXu0vVg==", "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -2081,9 +2115,9 @@ } }, "@types/react-dom": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.1.tgz", - "integrity": "sha512-1S/akvkKr63qIUWVu5IKYou2P9fHLb/P2VAwyxVV85JGaGZTcUniMiTuIqM3lXFB25ej6h+CYEQ27ERVwi6eGA==", + "version": "16.9.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.2.tgz", + "integrity": "sha512-hgPbBoI1aTSTvZwo8HYw35UaTldW6n2ETLvHAcfcg1FaOuBV3olmyCe5eMpx2WybWMBPv0MdU2t5GOcQhP+3zA==", "dev": true, "requires": { "@types/react": "*" @@ -2733,9 +2767,9 @@ }, "dependencies": { "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", + "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==", "dev": true }, "node-fetch": { @@ -2790,9 +2824,9 @@ }, "dependencies": { "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", + "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==", "dev": true } } @@ -2963,9 +2997,9 @@ }, "dependencies": { "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true } } @@ -5669,9 +5703,9 @@ } }, "date-fns": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.4.1.tgz", - "integrity": "sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.5.0.tgz", + "integrity": "sha512-I6Tkis01//nRcmvMQw/MRE1HAtcuA5Ie6jGPb8bJZJub7494LGOObqkV3ParnsSVviAjk5C8mNKDqYVBzCopWg==" }, "date-now": { "version": "0.1.4", @@ -6093,9 +6127,9 @@ } }, "dotenv": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz", - "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", "dev": true }, "download": { @@ -8332,20 +8366,20 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "husky": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.8.tgz", - "integrity": "sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz", + "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==", "dev": true, "requires": { "chalk": "^2.4.2", + "ci-info": "^2.0.0", "cosmiconfig": "^5.2.1", "execa": "^1.0.0", "get-stdin": "^7.0.0", - "is-ci": "^2.0.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.1.1", + "read-pkg": "^5.2.0", "run-node": "^1.0.0", "slash": "^3.0.0" }, @@ -11156,18 +11190,18 @@ } }, "nodemon": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.3.tgz", - "integrity": "sha512-TBNKRmJykEbxpTniZBusqRrUTHIEqa2fpecbTQDQj1Gxjth7kKAPP296ztR0o5gPUWsiYbuEbt73/+XMYab1+w==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", + "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", "dev": true, "requires": { - "chokidar": "^2.1.5", - "debug": "^3.1.0", + "chokidar": "^2.1.8", + "debug": "^3.2.6", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", - "pstree.remy": "^1.1.6", - "semver": "^5.5.0", - "supports-color": "^5.2.0", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.2", "update-notifier": "^2.5.0" @@ -13275,12 +13309,9 @@ } }, "react-content-loader": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-4.3.1.tgz", - "integrity": "sha512-K8QZq3B+MQXuzbz10cujfHWr8wmhpSjZsBswFJJpBnp5L5jR+QBVRjroPfUtdXLcbdeNDD2j9VS00a1fs3jQjg==", - "requires": { - "react-native-svg": "9.6.4" - } + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-4.3.2.tgz", + "integrity": "sha512-Af2RW2G57+mFRXsiSXROtgvz3KmPz0lATRHNUpJ57DyVw6SRzDRNRXo04I2xhcwmwVnXsfx4s2hsHrU+Lq5jRw==" }, "react-dom": { "version": "16.10.2", @@ -13313,11 +13344,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" }, - "react-native-svg": { - "version": "9.6.4", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-9.6.4.tgz", - "integrity": "sha512-6SlbGx0vlXHyDPQXSpX+8o6bNjxKFNJsISoboAkR7YWW6hdnkMg/HJXCgT6oJC0/ClKtSO7ZPrQcK4HR65kDNg==" - }, "react-quill": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-1.3.3.tgz", @@ -13517,9 +13543,9 @@ } }, "recast": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.3.tgz", - "integrity": "sha512-J76CWndZodsOsvhpxhlDCp75qVPuohbqPmh9NYMVDkNDp3JbyB7UKeoKo3KoL63sA1MyPJljRMjilR6DnIP7EQ==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.18.5.tgz", + "integrity": "sha512-sD1WJrpLQAkXGyQZyGzTM75WJvyAd98II5CHdK3IYbt/cZlU0UzCRVU11nUFNXX9fBVEt4E9ajkMjBlUlG+Oog==", "dev": true, "requires": { "ast-types": "0.13.2", @@ -15528,9 +15554,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "tippy.js": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.0.2.tgz", - "integrity": "sha512-Zj7ihX2/uImDudNkfxw9jgcbtg9sUKT3QRmuH9WJtKkX6M96SwMG8FPdiObholc4SJP6wlnqk0nqByjXb8QZSA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-5.0.3.tgz", + "integrity": "sha512-6OVy2/pWuzcphHdEAK/2GPOOsT75AY9D7Xhk9U6WHB/dT737avXtgW1K6ch8jrp81PxbXxHgdmeHRPBnqunwpQ==", "requires": { "popper.js": "^1.15.0" } @@ -15797,6 +15823,12 @@ } } }, + "tslint-react-hooks": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tslint-react-hooks/-/tslint-react-hooks-2.2.1.tgz", + "integrity": "sha512-bqIg2uZe+quJMfSOGc4OOZ4awo6TP1ejGDGS6IKg2WIrS0XnWfhUJ99i3B8rUpnZhuD4vRSvyYIbXPUmEqQxxQ==", + "dev": true + }, "tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -16215,6 +16247,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-debounce": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-3.1.0.tgz", + "integrity": "sha512-DEf3L/ZKkOSTARk/DHlC6KAAJKwMqpck8Zx06SM2Wr+LfU1TzhO8hZRzB/qbpSQqREYWQes24n1q9doeTMqF4g==" + }, "use-memo-one": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", diff --git a/package.json b/package.json index efd6cb957d..df50f8ee5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matters-web", - "version": "2.0.1", + "version": "2.1.0", "description": "codebase of Matters' website", "author": "", "engines": { @@ -31,10 +31,9 @@ "gen:fragmentTypes": "node bin/buildFragmentTypes.js" }, "dependencies": { - "@apollo/react-testing": "^3.1.2", "@matters/apollo-upload-client": "^11.1.0", - "@sentry/browser": "^5.7.0", - "@tippy.js/react": "^3.0.1", + "@sentry/browser": "^5.7.1", + "@tippy.js/react": "^3.1.0", "@types/jump.js": "^1.0.2", "apollo-cache-inmemory": "^1.6.3", "apollo-cache-persist": "^0.1.1", @@ -47,7 +46,7 @@ "apollo-utilities": "^1.3.2", "body-scroll-lock": "^2.6.4", "classnames": "^2.2.6", - "date-fns": "^2.4.1", + "date-fns": "^2.5.0", "express": "^4.17.1", "formik": "^1.5.8", "graphql": "^14.5.8", @@ -64,29 +63,31 @@ "react": "^16.10.2", "react-apollo": "^3.1.2", "react-beautiful-dnd": "^11.0.5", - "react-content-loader": "^4.3.1", + "react-content-loader": "^4.3.2", "react-dom": "^16.10.2", "react-quill": "^1.3.3", "react-responsive": "^8.0.1", "react-waypoint": "^9.0.2", "subscriptions-transport-ws": "^0.9.16", + "use-debounce": "^3.1.0", "validator": "^11.1.0" }, "devDependencies": { + "@apollo/react-testing": "3.1.2", "@testing-library/react": "^8.0.9", "@types/body-scroll-lock": "^2.6.1", "@types/classnames": "^2.2.9", "@types/dotenv": "^6.1.1", "@types/express": "^4.17.1", "@types/helmet": "0.0.44", - "@types/jest": "^24.0.18", + "@types/jest": "^24.0.19", "@types/lodash": "^4.14.144", "@types/next-server": "^8.1.2", "@types/nprogress": "0.2.0", "@types/quill": "^2.0.3", - "@types/react": "^16.9.5", + "@types/react": "^16.9.9", "@types/react-beautiful-dnd": "^11.0.3", - "@types/react-dom": "^16.9.1", + "@types/react-dom": "^16.9.2", "@types/react-responsive": "^8.0.1", "@types/segment-analytics": "0.0.32", "@types/styled-jsx": "^2.2.8", @@ -97,8 +98,8 @@ "babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-module-resolver": "^3.2.0", "babel-polyfill": "^6.26.0", - "dotenv": "^8.1.0", - "husky": "^3.0.8", + "dotenv": "^8.2.0", + "husky": "^3.0.9", "identity-obj-proxy": "^3.0.0", "imagemin-mozjpeg": "^8.0.0", "imagemin-optipng": "^7.1.0", @@ -108,7 +109,7 @@ "next-compose-plugins": "^2.2.0", "next-offline": "^4.0.6", "next-optimized-images": "^2.5.3", - "nodemon": "^1.19.3", + "nodemon": "^1.19.4", "npm-run-all": "^4.1.5", "postcss-calc": "^7.0.1", "postcss-color-function": "^4.1.0", @@ -118,12 +119,13 @@ "postcss-preset-env": "^6.7.0", "prettier": "^1.18.2", "styled-jsx-plugin-postcss": "^2.0.1", - "svg-sprite-loader": "^4.1.3", + "svg-sprite-loader": "4.1.3", "ts-jest": "^24.1.0", "ts-node": "^8.4.1", "tslint": "^5.20.0", "tslint-config-prettier": "^1.18.0", "tslint-react": "^4.1.0", + "tslint-react-hooks": "^2.2.1", "typescript": "^3.6.4" }, "_moduleAliases": { diff --git a/public/static/icons/block.svg b/public/static/icons/block.svg new file mode 100644 index 0000000000..e3d5551948 --- /dev/null +++ b/public/static/icons/block.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/static/icons/unblock.svg b/public/static/icons/unblock.svg new file mode 100644 index 0000000000..c0891ee4fb --- /dev/null +++ b/public/static/icons/unblock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/enums/analytics.ts b/src/common/enums/analytics.ts index bdc6c5a3e7..f1b7b9a4c7 100644 --- a/src/common/enums/analytics.ts +++ b/src/common/enums/analytics.ts @@ -63,6 +63,7 @@ export const FEED_TYPE = { FOLLOWER: 'follower', APPRECIATOR: 'appreciator', SEARCH_USER: 'search-user', + BLOCK_LIST: 'block-list', // tags TAGS: 'tags', ALL_TAGS: 'all-tags', diff --git a/src/common/enums/errorCode.ts b/src/common/enums/errorCode.ts index 8e2d335df6..6501ae64a6 100644 --- a/src/common/enums/errorCode.ts +++ b/src/common/enums/errorCode.ts @@ -4,7 +4,9 @@ export const ERROR_CODES = { NETWORK_ERROR: 'NETWORK_ERROR', INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', BAD_USER_INPUT: 'BAD_USER_INPUT', + ACTION_LIMIT_EXCEEDED: 'ACTION_LIMIT_EXCEEDED', + ACTION_FAILED: 'ACTION_FAILED', // Auth UNAUTHENTICATED: 'UNAUTHENTICATED', @@ -34,11 +36,6 @@ export const ERROR_CODES = { USER_USERNAME_INVALID: 'USER_USERNAME_INVALID', USER_USERNAME_EXISTS: 'USER_USERNAME_EXISTS', USER_DISPLAYNAME_INVALID: 'USER_DISPLAYNAME_INVALID', - USER_FOLLOW_FAILED: 'USER_FOLLOW_FAILED', - USER_INVITE_FAILED: 'USER_INVITE_FAILED', - USER_INVITE_STATE_INVALID: 'USER_INVITE_STATE_INVALID', - USER_INVITE_EMAIL_REGISTERED: 'USER_INVITE_EMAIL_REGISTERED', - USER_INVITE_EMAIL_INVITED: 'USER_INVITE_EMAIL_INVITED', // Verification Code CODE_INVALID: 'CODE_INVALID', diff --git a/src/common/enums/route.ts b/src/common/enums/route.ts index eddb47f703..a5bd70a02a 100644 --- a/src/common/enums/route.ts +++ b/src/common/enums/route.ts @@ -32,6 +32,7 @@ type ROUTE_KEY = | 'ME_NOTIFICATIONS' | 'ME_SETTINGS_ACCOUNT' | 'ME_SETTINGS_NOTIFICATION' + | 'ME_SETTINGS_BLOCKED' | 'ME_DRAFT_DETAIL' // | 'EDITOR' | 'AUTH_LOGIN' @@ -210,6 +211,11 @@ export const ROUTES: Array<{ href: '/MeSettingsNotification', as: '/me/settings/notification' }, + { + key: 'ME_SETTINGS_BLOCKED', + href: '/MeSettingsBlocked', + as: '/me/settings/blocked' + }, // Draft { diff --git a/src/common/enums/text.ts b/src/common/enums/text.ts index a744dc5a36..49a249c996 100644 --- a/src/common/enums/text.ts +++ b/src/common/enums/text.ts @@ -121,6 +121,7 @@ export const TEXT = { setting: '設定', accountSetting: '帳戶設定', notificationSetting: '通知設定', + blockedSetting: '封鎖用戶', walletSetting: '錢包設定', uiSetting: '介面設定', userProfile: '個人簡介', @@ -130,6 +131,11 @@ export const TEXT = { articleFingerprint: '作品指紋', copySuccess: '複製成功', copy: '複製', + block: '封鎖', + blockUser: '封鎖用戶', + unblockUser: '取消封鎖', + blockSuccess: '封鎖成功', + unblockSuccess: '已取消封鎖。該用戶現在可以評論你的作品。', pin: '喜歡回應', unpin: '取消精選', emptySearchResults: '沒有找到你搜索的內容', @@ -161,11 +167,7 @@ export const TEXT = { USER_USERNAME_INVALID: 'Matters ID 不正確', USER_USERNAME_EXISTS: '該 Matters ID 已被其他使用者使用', USER_DISPLAYNAME_INVALID: '姓名不正確', - USER_FOLLOW_FAILED: '追蹤用戶失敗,請稍候重試', - // USER_INVITE_FAILED: '', - // USER_INVITE_STATE_INVALID: '', - // USER_INVITE_EMAIL_REGISTERED: '', - // USER_INVITE_EMAIL_INVITED: '', + ACTION_FAILED: '操作失敗,請稍候重試', CODE_INVALID: '驗證碼不正確', CODE_EXPIRED: '驗證碼已過期' } as { [key: string]: string } @@ -293,6 +295,7 @@ export const TEXT = { setting: '设定', accountSetting: '账户设定', notificationSetting: '通知设定', + blockedSetting: '屏蔽用户', walletSetting: '钱包设定', uiSetting: '界面设定', userProfile: '个人简介', @@ -302,6 +305,11 @@ export const TEXT = { articleFingerprint: '作品指纹', copySuccess: '复制成功', copy: '复制', + block: '屏蔽', + blockUser: '屏蔽用户', + unblockUser: '取消屏蔽', + blockSuccess: '屏蔽成功', + unblockSuccess: '已取消屏蔽。该用户现在可以评论你的作品。', pin: '喜欢回应', unpin: '取消精选', emptySearchResults: '没有找到你搜寻的内容', @@ -333,11 +341,7 @@ export const TEXT = { USER_USERNAME_INVALID: 'Matters ID 不正确', USER_USERNAME_EXISTS: '该 Matters ID 已被其他用户使用', USER_DISPLAYNAME_INVALID: '姓名不正确', - USER_FOLLOW_FAILED: '追踪用户失败,请稍候重试', - // USER_INVITE_FAILED: '', - // USER_INVITE_STATE_INVALID: '', - // USER_INVITE_EMAIL_REGISTERED: '', - // USER_INVITE_EMAIL_INVITED: '', + ACTION_FAILED: '操作失败,请稍候重试', CODE_INVALID: '验证码不正确', CODE_EXPIRED: '验证码已过期' } as { [key: string]: string } diff --git a/src/common/enums/time.ts b/src/common/enums/time.ts index 3994da8556..5dd2cab18c 100644 --- a/src/common/enums/time.ts +++ b/src/common/enums/time.ts @@ -1 +1,5 @@ export const POLL_INTERVAL = 1000 * 10 + +export const INPUT_DEBOUNCE = 300 + +export const APPRECIATE_DEBOUNCE = 1000 diff --git a/src/common/utils/route.ts b/src/common/utils/route.ts index 0cb8fb3141..515b597bf2 100644 --- a/src/common/utils/route.ts +++ b/src/common/utils/route.ts @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import Router, { NextRouter } from 'next/router' import queryString from 'query-string' diff --git a/src/components/Analytics/AnalyticsProvider.tsx b/src/components/Analytics/AnalyticsProvider.tsx index 5beca88d54..0e669dfdcd 100644 --- a/src/components/Analytics/AnalyticsProvider.tsx +++ b/src/components/Analytics/AnalyticsProvider.tsx @@ -1,6 +1,6 @@ import getConfig from 'next/config' import Router from 'next/router' -import React, { FC, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { analytics } from '~/common/utils' @@ -8,8 +8,9 @@ const { publicRuntimeConfig: { SEGMENT_KEY } } = getConfig() -export const AnalyticsProvider: FC = ({ children }) => { +export const AnalyticsProvider: React.FC = ({ children }) => { const [sessionStarted, setSessionStarted] = useState(false) + useEffect(() => { // injects analytics var into global scope // ref: https://github.com/segmentio/analytics-react#%EF%B8%8F-step-1-copy-the-snippet @@ -77,7 +78,7 @@ export const AnalyticsProvider: FC = ({ children }) => { analytics.load(SEGMENT_KEY || '3gE20MjzN9qncFqlKV0pDvNO7Cp2gWU3') } })() - }) + }, []) useEffect(() => { // initial @@ -89,7 +90,7 @@ export const AnalyticsProvider: FC = ({ children }) => { Router.events.on('routeChangeComplete', (path: string) => { analytics.trackPage({ path }) }) - }) + }, []) return <>{children} } diff --git a/src/components/ArticleDigest/Actions/Appreciation.tsx b/src/components/ArticleDigest/Actions/Appreciation.tsx index c1f517fef3..ab1d0a02ec 100644 --- a/src/components/ArticleDigest/Actions/Appreciation.tsx +++ b/src/components/ArticleDigest/Actions/Appreciation.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { Icon, TextIcon } from '~/components' @@ -27,7 +26,7 @@ const Appreciation = ({ icon={} color="grey" weight="medium" - text={numAbbr(_get(article, 'appreciationsReceivedTotal', 0))} + text={numAbbr((article && article.appreciationsReceivedTotal) || 0)} size={size === 'small' ? 'sm' : 'xs'} spacing="xtight" /> diff --git a/src/components/ArticleDigest/Actions/CommentCount.tsx b/src/components/ArticleDigest/Actions/CommentCount.tsx index be284884ef..9463c4dea2 100644 --- a/src/components/ArticleDigest/Actions/CommentCount.tsx +++ b/src/components/ArticleDigest/Actions/CommentCount.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import Link from 'next/link' import { Icon, TextIcon } from '~/components' @@ -65,7 +64,7 @@ const CommentCount = ({ } color="grey" weight="medium" - text={numAbbr(_get(article, 'commentCount', 0))} + text={numAbbr(article.commentCount || 0)} size={size === 'default' ? 'sm' : 'xs'} spacing="xxtight" /> diff --git a/src/components/ArticleDigest/Actions/ResponseCount.tsx b/src/components/ArticleDigest/Actions/ResponseCount.tsx index 94f258d92f..5fad06bd5b 100644 --- a/src/components/ArticleDigest/Actions/ResponseCount.tsx +++ b/src/components/ArticleDigest/Actions/ResponseCount.tsx @@ -67,7 +67,7 @@ const ResponseCount = ({ } color="grey" weight="medium" - text={numAbbr(_get(article, 'responseCount', 0))} + text={numAbbr(article.responseCount || 0)} size={size === 'small' ? 'sm' : 'xs'} spacing="xxtight" /> diff --git a/src/components/ArticleDigest/Actions/TopicScore.tsx b/src/components/ArticleDigest/Actions/TopicScore.tsx index 8cd162137b..fcf4a8188d 100644 --- a/src/components/ArticleDigest/Actions/TopicScore.tsx +++ b/src/components/ArticleDigest/Actions/TopicScore.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { Icon, TextIcon, Translate } from '~/components' diff --git a/src/components/ArticleDigest/Actions/index.tsx b/src/components/ArticleDigest/Actions/index.tsx index a3db2d67e9..86a1f2da33 100644 --- a/src/components/ArticleDigest/Actions/index.tsx +++ b/src/components/ArticleDigest/Actions/index.tsx @@ -154,9 +154,11 @@ const Actions = ({ )} + {hasDateTime && 'createdAt' in article && isResponseMode && ( )} + ) diff --git a/src/components/ArticleDigest/DropdownActions/ArchiveButton.tsx b/src/components/ArticleDigest/DropdownActions/ArchiveButton.tsx index 864b361766..add12ae554 100644 --- a/src/components/ArticleDigest/DropdownActions/ArchiveButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/ArchiveButton.tsx @@ -1,7 +1,8 @@ import gql from 'graphql-tag' import { Icon, TextIcon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { ArchiveArticle } from '~/components/GQL/mutations/__generated__/ArchiveArticle' import ARCHIVE_ARTICLE from '~/components/GQL/mutations/archiveArticle' import updateUserArticles from '~/components/GQL/updates/userArticles' @@ -30,51 +31,49 @@ const ArchiveButton = ({ article: ArchiveButtonArticle hideDropdown: () => void }) => { + const [archiveArticle] = useMutation(ARCHIVE_ARTICLE, { + variables: { id: article.id }, + optimisticResponse: { + archiveArticle: { + id: article.id, + state: 'archived' as any, + sticky: false, + __typename: 'Article' + } + }, + update: cache => { + updateUserArticles({ + cache, + articleId: article.id, + userName: article.author.userName, + type: 'archive' + }) + } + }) + return ( - { - updateUserArticles({ - cache, - articleId: article.id, - userName: article.author.userName, - type: 'archive' - }) + - )} - + + } + spacing="tight" + > + + + + + ) } diff --git a/src/components/ArticleDigest/DropdownActions/StickyButton.tsx b/src/components/ArticleDigest/DropdownActions/StickyButton.tsx index 47c3ac254e..4f6cdce3f9 100644 --- a/src/components/ArticleDigest/DropdownActions/StickyButton.tsx +++ b/src/components/ArticleDigest/DropdownActions/StickyButton.tsx @@ -1,13 +1,14 @@ import gql from 'graphql-tag' import { Icon, TextIcon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import updateUserArticles from '~/components/GQL/updates/userArticles' import ICON_PIN_TO_TOP from '~/static/icons/pin-to-top.svg?sprite' import ICON_UNSTICKY from '~/static/icons/unsticky.svg?sprite' import { StickyButtonArticle } from './__generated__/StickyButtonArticle' +import { UpdateArticleInfo } from './__generated__/UpdateArticleInfo' import styles from './styles.css' const UPDATE_ARTICLE_INFO = gql` @@ -69,39 +70,37 @@ const StickyButton = ({ article: StickyButtonArticle hideDropdown: () => void }) => { + const [update] = useMutation(UPDATE_ARTICLE_INFO, { + variables: { id: article.id, sticky: !article.sticky }, + optimisticResponse: { + updateArticleInfo: { + id: article.id, + sticky: !article.sticky, + __typename: 'Article' + } + }, + update: cache => { + updateUserArticles({ + cache, + articleId: article.id, + userName: article.author.userName, + type: article.sticky ? 'unsticky' : 'sticky' + }) + } + }) + return ( - { - updateUserArticles({ - cache, - articleId: article.id, - userName: article.author.userName, - type: article.sticky ? 'unsticky' : 'sticky' - }) + - )} - + {article.sticky ? : } + + + ) } diff --git a/src/components/ArticleDigest/DropdownActions/index.tsx b/src/components/ArticleDigest/DropdownActions/index.tsx index aa4e345ed1..8ce2317c96 100644 --- a/src/components/ArticleDigest/DropdownActions/index.tsx +++ b/src/components/ArticleDigest/DropdownActions/index.tsx @@ -75,6 +75,7 @@ const DropdownActions = ({ article }: { article: DropdownActionsArticle }) => { /> + ) diff --git a/src/components/ArticleDigest/RelatedDigest/index.tsx b/src/components/ArticleDigest/RelatedDigest/index.tsx index 31280e849e..8a6e320746 100644 --- a/src/components/ArticleDigest/RelatedDigest/index.tsx +++ b/src/components/ArticleDigest/RelatedDigest/index.tsx @@ -81,6 +81,7 @@ const RelatedDigest = ({ )} +
@@ -91,6 +92,7 @@ const RelatedDigest = ({
+ {!cover && (
@@ -98,10 +100,12 @@ const RelatedDigest = ({
)} +
+ ) diff --git a/src/components/Avatar/index.tsx b/src/components/Avatar/index.tsx index 4277748dd2..550a3c4124 100644 --- a/src/components/Avatar/index.tsx +++ b/src/components/Avatar/index.tsx @@ -45,6 +45,7 @@ export const Avatar = ({ style={{ backgroundImage: `url(${source})` }} aria-hidden="true" /> + ) diff --git a/src/components/Button/BlockUser/Dropdown/index.tsx b/src/components/Button/BlockUser/Dropdown/index.tsx new file mode 100644 index 0000000000..9d27016494 --- /dev/null +++ b/src/components/Button/BlockUser/Dropdown/index.tsx @@ -0,0 +1,127 @@ +import { useContext } from 'react' + +import { Icon, TextIcon, Translate } from '~/components' +import { useMutation } from '~/components/GQL' +import { BlockUser } from '~/components/GQL/fragments/__generated__/BlockUser' +import userFragments from '~/components/GQL/fragments/user' +import { UnblockUser } from '~/components/GQL/mutations/__generated__/UnblockUser' +import UNBLOCK_USER from '~/components/GQL/mutations/unblockUser' +import { LanguageContext } from '~/components/Language' +import BlocKUserModal from '~/components/Modal/BlockUserModal' +import { ModalInstance, ModalSwitch } from '~/components/ModalManager' + +import { ADD_TOAST, TEXT } from '~/common/enums' +import { translate } from '~/common/utils' +import ICON_BLOCK from '~/static/icons/block.svg?sprite' +import ICON_UNBLOCK from '~/static/icons/unblock.svg?sprite' + +import styles from './styles.css' + +const fragments = { + user: userFragments.block +} + +const TextIconBlock = () => ( + } + spacing="tight" + > + + +) + +const TextIconUnblock = () => ( + + } + spacing="tight" + > + + +) + +const BlockUserButton = ({ + user, + isShown, + hideDropdown +}: { + user: BlockUser + isShown: boolean + hideDropdown: () => void +}) => { + const { lang } = useContext(LanguageContext) + const [unblockUser] = useMutation(UNBLOCK_USER, { + variables: { id: user.id }, + optimisticResponse: { + unblockUser: { + id: user.id, + isBlocked: false, + __typename: 'User' + } + } + }) + + return ( + <> + {user.isBlocked && ( + + )} + + {!user.isBlocked && ( + + {(open: any) => ( + + )} + + )} + + {isShown && ( + + {(props: ModalInstanceProps) => ( + + )} + + )} + + ) +} + +BlockUserButton.fragments = fragments + +export default BlockUserButton diff --git a/src/components/Button/BlockUser/Dropdown/styles.css b/src/components/Button/BlockUser/Dropdown/styles.css new file mode 100644 index 0000000000..5dbf2fe43e --- /dev/null +++ b/src/components/Button/BlockUser/Dropdown/styles.css @@ -0,0 +1,3 @@ +button { + font-size: var(--font-size-sm); +} diff --git a/src/components/Button/BlockUser/Unblock/index.tsx b/src/components/Button/BlockUser/Unblock/index.tsx new file mode 100644 index 0000000000..7d27421afd --- /dev/null +++ b/src/components/Button/BlockUser/Unblock/index.tsx @@ -0,0 +1,77 @@ +import gql from 'graphql-tag' +import { useContext } from 'react' + +import { Button, Translate } from '~/components' +import { useMutation } from '~/components/GQL' +import { UnblockUser } from '~/components/GQL/mutations/__generated__/UnblockUser' +import UNBLOCK_USER from '~/components/GQL/mutations/unblockUser' +import { LanguageContext } from '~/components/Language' + +import { ADD_TOAST, ANALYTICS_EVENTS, TEXT } from '~/common/enums' +import { analytics, translate } from '~/common/utils' + +import { UnblockButtonUser } from './__generated__/UnblockButtonUser' + +const fragments = { + user: gql` + fragment UnblockButtonUser on User { + id + isBlocked + } + ` +} + +const Unblock = ({ + user, + size = 'small' +}: { + user: UnblockButtonUser + size?: 'small' | 'default' +}) => { + const { lang } = useContext(LanguageContext) + const [unblockUser] = useMutation(UNBLOCK_USER, { + variables: { id: user.id }, + optimisticResponse: { + unblockUser: { + id: user.id, + isBlocked: false, + __typename: 'User' + } + } + }) + + return ( + + ) +} + +Unblock.fragments = fragments + +export default Unblock diff --git a/src/components/Button/Bookmark/Subscribe.tsx b/src/components/Button/Bookmark/Subscribe.tsx index 099a66b1bc..f6b2799d7a 100644 --- a/src/components/Button/Bookmark/Subscribe.tsx +++ b/src/components/Button/Bookmark/Subscribe.tsx @@ -1,12 +1,13 @@ import gql from 'graphql-tag' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Icon } from '~/components/Icon' import ICON_BOOKMARK_REGULAR_INACTIVE from '~/static/icons/bookmark-regular-inactive.svg?sprite' import ICON_BOOKMARK_SM_INACTIVE from '~/static/icons/bookmark-small-inactive.svg?sprite' import { BookmarkArticle } from './__generated__/BookmarkArticle' +import { SubscribeArticle } from './__generated__/SubscribeArticle' const SUBSCRIBE_ARTICLE = gql` mutation SubscribeArticle($id: ID!) { @@ -25,41 +26,40 @@ const Subscribe = ({ article: BookmarkArticle size: 'xsmall' | 'small' | 'default' disabled?: boolean -}) => ( - { + const [subscribe] = useMutation(SUBSCRIBE_ARTICLE, { + variables: { id: article.id }, + optimisticResponse: { subscribeArticle: { id: article.id, subscribed: true, __typename: 'Article' } - }} - > - {(subscribe: any, { data }: any) => ( - - )} - -) + } + }) + + return ( + + ) +} export default Subscribe diff --git a/src/components/Button/Bookmark/Unsubscribe.tsx b/src/components/Button/Bookmark/Unsubscribe.tsx index cbe2258eeb..e6eab9dc57 100644 --- a/src/components/Button/Bookmark/Unsubscribe.tsx +++ b/src/components/Button/Bookmark/Unsubscribe.tsx @@ -1,12 +1,13 @@ import gql from 'graphql-tag' import { Icon } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import ICON_BOOKMARK_REGULAR_ACTIVE from '~/static/icons/bookmark-regular-active.svg?sprite' import ICON_BOOKMARK_SM_ACTIVE from '~/static/icons/bookmark-small-active.svg?sprite' import { BookmarkArticle } from './__generated__/BookmarkArticle' +import { UnsubscribeArticle } from './__generated__/UnsubscribeArticle' const UNSUBSCRIBE_ARTICLE = gql` mutation UnsubscribeArticle($id: ID!) { @@ -25,41 +26,40 @@ const Unsubscribe = ({ article: BookmarkArticle size: 'xsmall' | 'small' | 'default' disabled?: boolean -}) => ( - { + const [unsubscribe] = useMutation(UNSUBSCRIBE_ARTICLE, { + variables: { id: article.id }, + optimisticResponse: { unsubscribeArticle: { id: article.id, subscribed: false, __typename: 'Article' } - }} - > - {(unsubscribe: any, { data }: any) => ( - - )} - -) + } + }) + + return ( + + ) +} export default Unsubscribe diff --git a/src/components/Button/Follow/Follow.tsx b/src/components/Button/Follow/Follow.tsx index dccb6d34fb..b280553b9c 100644 --- a/src/components/Button/Follow/Follow.tsx +++ b/src/components/Button/Follow/Follow.tsx @@ -2,7 +2,7 @@ import gql from 'graphql-tag' import _get from 'lodash/get' import { Button, Icon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import updateUserFollowerCount from '~/components/GQL/updates/userFollowerCount' import updateViewerFolloweeCount from '~/components/GQL/updates/viewerFolloweeCount' @@ -11,6 +11,7 @@ import { analytics } from '~/common/utils' import ICON_ADD from '~/static/icons/add.svg?sprite' import { FollowButtonUser } from './__generated__/FollowButtonUser' +import { FollowUser } from './__generated__/FollowUser' const FOLLOW_USER = gql` mutation FollowUser($id: ID!) { @@ -28,53 +29,49 @@ const Follow = ({ }: { user: FollowButtonUser size?: 'small' | 'default' -}) => ( - { + const [follow] = useMutation(FOLLOW_USER, { + variables: { id: user.id }, + optimisticResponse: { followUser: { id: user.id, isFollowee: true, isFollower: user.isFollower, __typename: 'User' } - }} - update={(cache: any) => { + }, + update: cache => { const userName = _get(user, 'userName', null) updateUserFollowerCount({ cache, type: 'increment', userName }) updateViewerFolloweeCount({ cache, type: 'increment' }) - }} - > - {(follow: any, { data }: any) => ( - - )} - -) + } + style={size === 'small' ? { width: '4rem' } : { width: '5.5rem' }} + onClick={() => { + follow() + analytics.trackEvent(ANALYTICS_EVENTS.FOLLOW_USER, { id: user.id }) + }} + bgColor="transparent" + outlineColor="green" + > + + + ) +} export default Follow diff --git a/src/components/Button/Follow/Unfollow.tsx b/src/components/Button/Follow/Unfollow.tsx index e4badef3a5..7a3172557b 100644 --- a/src/components/Button/Follow/Unfollow.tsx +++ b/src/components/Button/Follow/Unfollow.tsx @@ -3,7 +3,7 @@ import _get from 'lodash/get' import { useState } from 'react' import { Button, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import updateUserFollowerCount from '~/components/GQL/updates/userFollowerCount' import updateViewerFolloweeCount from '~/components/GQL/updates/viewerFolloweeCount' @@ -11,6 +11,7 @@ import { ANALYTICS_EVENTS, TEXT } from '~/common/enums' import { analytics } from '~/common/utils' import { FollowButtonUser } from './__generated__/FollowButtonUser' +import { UnfollowUser } from './__generated__/UnfollowUser' const UNFOLLOW_USER = gql` mutation UnfollowUser($id: ID!) { @@ -30,53 +31,49 @@ const Unfollow = ({ size?: 'small' | 'default' }) => { const [hover, setHover] = useState(false) + const [unfollow] = useMutation(UNFOLLOW_USER, { + variables: { id: user.id }, + optimisticResponse: { + unfollowUser: { + id: user.id, + isFollowee: false, + isFollower: user.isFollower, + __typename: 'User' + } + }, + update: cache => { + const userName = _get(user, 'userName', null) + updateUserFollowerCount({ cache, type: 'decrement', userName }) + updateViewerFolloweeCount({ cache, type: 'decrement' }) + } + }) return ( - { - const userName = _get(user, 'userName', null) - updateUserFollowerCount({ cache, type: 'decrement', userName }) - updateViewerFolloweeCount({ cache, type: 'decrement' }) + + {hover ? ( + + ) : ( + )} - + ) } diff --git a/src/components/ShareButton/Douban.tsx b/src/components/Button/Share/Douban.tsx similarity index 97% rename from src/components/ShareButton/Douban.tsx rename to src/components/Button/Share/Douban.tsx index 71be0583e2..f6df476aac 100644 --- a/src/components/ShareButton/Douban.tsx +++ b/src/components/Button/Share/Douban.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/Email.tsx b/src/components/Button/Share/Email.tsx similarity index 97% rename from src/components/ShareButton/Email.tsx rename to src/components/Button/Share/Email.tsx index 0b8a78bd9e..43ff24c24d 100644 --- a/src/components/ShareButton/Email.tsx +++ b/src/components/Button/Share/Email.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/Facebook.tsx b/src/components/Button/Share/Facebook.tsx similarity index 97% rename from src/components/ShareButton/Facebook.tsx rename to src/components/Button/Share/Facebook.tsx index 3aef327374..c41870858b 100644 --- a/src/components/ShareButton/Facebook.tsx +++ b/src/components/Button/Share/Facebook.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/LINE.tsx b/src/components/Button/Share/LINE.tsx similarity index 97% rename from src/components/ShareButton/LINE.tsx rename to src/components/Button/Share/LINE.tsx index 80a181ba4a..3c8915ae6e 100644 --- a/src/components/ShareButton/LINE.tsx +++ b/src/components/Button/Share/LINE.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/ShareModal.tsx b/src/components/Button/Share/ShareModal.tsx similarity index 98% rename from src/components/ShareButton/ShareModal.tsx rename to src/components/Button/Share/ShareModal.tsx index 41dd7d07bc..09ac350fde 100644 --- a/src/components/ShareButton/ShareModal.tsx +++ b/src/components/Button/Share/ShareModal.tsx @@ -1,5 +1,3 @@ -import _get from 'lodash/get' - import { Icon } from '~/components/Icon' import { Translate } from '~/components/Language' import { Modal } from '~/components/Modal' diff --git a/src/components/ShareButton/Telegram.tsx b/src/components/Button/Share/Telegram.tsx similarity index 97% rename from src/components/ShareButton/Telegram.tsx rename to src/components/Button/Share/Telegram.tsx index e83ff6ac81..043f060157 100644 --- a/src/components/ShareButton/Telegram.tsx +++ b/src/components/Button/Share/Telegram.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/Twitter.tsx b/src/components/Button/Share/Twitter.tsx similarity index 97% rename from src/components/ShareButton/Twitter.tsx rename to src/components/Button/Share/Twitter.tsx index b9728f90ad..a8dad8d6b1 100644 --- a/src/components/ShareButton/Twitter.tsx +++ b/src/components/Button/Share/Twitter.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/WeChat.tsx b/src/components/Button/Share/WeChat.tsx similarity index 94% rename from src/components/ShareButton/WeChat.tsx rename to src/components/Button/Share/WeChat.tsx index cbc75a7abc..10b903cc49 100644 --- a/src/components/ShareButton/WeChat.tsx +++ b/src/components/Button/Share/WeChat.tsx @@ -1,5 +1,3 @@ -import _get from 'lodash/get' - import { Icon } from '~/components/Icon' import { Translate } from '~/components/Language' import { TextIcon } from '~/components/TextIcon' diff --git a/src/components/ShareButton/Weibo.tsx b/src/components/Button/Share/Weibo.tsx similarity index 97% rename from src/components/ShareButton/Weibo.tsx rename to src/components/Button/Share/Weibo.tsx index a09ad900fa..985a3c65ea 100644 --- a/src/components/ShareButton/Weibo.tsx +++ b/src/components/Button/Share/Weibo.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/WhatsApp.tsx b/src/components/Button/Share/WhatsApp.tsx similarity index 97% rename from src/components/ShareButton/WhatsApp.tsx rename to src/components/Button/Share/WhatsApp.tsx index 7b3cce444d..250f280670 100644 --- a/src/components/ShareButton/WhatsApp.tsx +++ b/src/components/Button/Share/WhatsApp.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import queryString from 'query-string' import { Icon } from '~/components/Icon' diff --git a/src/components/ShareButton/index.tsx b/src/components/Button/Share/index.tsx similarity index 100% rename from src/components/ShareButton/index.tsx rename to src/components/Button/Share/index.tsx diff --git a/src/components/ShareButton/styles.css b/src/components/Button/Share/styles.css similarity index 100% rename from src/components/ShareButton/styles.css rename to src/components/Button/Share/styles.css diff --git a/src/components/Button/Shuffle/index.tsx b/src/components/Button/Shuffle/index.tsx index d2200cedad..b1c4d87869 100644 --- a/src/components/Button/Shuffle/index.tsx +++ b/src/components/Button/Shuffle/index.tsx @@ -18,6 +18,7 @@ export const ShuffleButton = ({ onClick }: { onClick: () => void }) => ( > + ) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 9bca09c68c..40e73bbf95 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -169,57 +169,53 @@ export const Button: React.FC = ({ // anchor if (is === 'anchor') { return ( - <> - - {icon} - {children && {children}} - + + {icon} + {children && {children}} + - + ) } // link if (is === 'link') { return ( - <> - - - {icon} - {children && {children}} - - - - + + + {icon} + {children && {children}} + + + + ) } // span if (is === 'span') { return ( - <> - - {icon} - {children && {children}} - + + {icon} + {children && {children}} + - + ) } // button return ( - <> - + ) } diff --git a/src/components/CollectionEditor/CollectForm.tsx b/src/components/CollectionEditor/CollectForm.tsx index 459026c357..c270a84ab6 100644 --- a/src/components/CollectionEditor/CollectForm.tsx +++ b/src/components/CollectionEditor/CollectForm.tsx @@ -1,10 +1,9 @@ import _debounce from 'lodash/debounce' -import _get from 'lodash/get' -import { FC, useContext, useRef, useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useContext, useRef, useState } from 'react' +import { useQuery } from 'react-apollo' +import { useDebounce } from 'use-debounce' import ArticleList from '~/components/Dropdown/ArticleList' -import { Query } from '~/components/GQL' import { SearchArticles, SearchArticles_search_edges_node_Article @@ -13,6 +12,7 @@ import SEARCH_ARTICLES from '~/components/GQL/queries/searchArticles' import { LanguageContext } from '~/components/Language' import { Dropdown, PopperInstance } from '~/components/Popper' +import { INPUT_DEBOUNCE } from '~/common/enums' import { translate } from '~/common/utils' import styles from './styles.css' @@ -21,16 +21,24 @@ interface Props { onAdd: (article: SearchArticles_search_edges_node_Article) => void } -const debouncedSetSearch = _debounce((value, setSearch) => { - setSearch(value) -}, 300) - -const CollectForm: FC = ({ onAdd }) => { +const CollectForm: React.FC = ({ onAdd }) => { const { lang } = useContext(LanguageContext) const [search, setSearch] = useState('') + const [debouncedSearch] = useDebounce(search, INPUT_DEBOUNCE) const [instance, setInstance] = useState(null) const inputNode: React.RefObject | null = useRef(null) + // query + const { loading, data } = useQuery(SEARCH_ARTICLES, { + variables: { search: debouncedSearch }, + skip: !debouncedSearch + }) + const articles = ((data && data.search.edges) || []) + .filter(({ node }) => node.__typename === 'Article') + .map(({ node }) => node) as SearchArticles_search_edges_node_Article[] + + // dropdown + const isShowDropdown = (articles && articles.length) || loading const hideDropdown = () => { if (instance) { instance.hide() @@ -38,74 +46,59 @@ const CollectForm: FC = ({ onAdd }) => { } const showDropdown = () => { if (instance) { - setTimeout(() => { - instance.show() - }, 100) // unknown bug, needs set a timeout + instance.show() } } - return ( - - {({ data, loading }: QueryResult & { data: SearchArticles }) => { - const articles = _get(data, 'search.edges', []).map( - ({ node }: { node: SearchArticles_search_edges_node_Article }) => node - ) - const isShowDropdown = (articles && articles.length) || loading - - if (isShowDropdown) { - showDropdown() - } else { - hideDropdown() - } + if (isShowDropdown) { + showDropdown() + } else { + hideDropdown() + } - return ( - <> - { - onAdd(article) - setSearch('') - hideDropdown() - if (inputNode && inputNode.current) { - inputNode.current.value = '' - } - }} - /> + return ( + <> + { + onAdd(article) + setSearch('') + hideDropdown() + if (inputNode && inputNode.current) { + inputNode.current.value = '' } - > - { - const value = event.target.value - debouncedSetSearch(value, setSearch) - }} - onFocus={() => { - if (isShowDropdown) { - showDropdown() - } - }} - /> - + }} + /> + } + > + { + const value = event.target.value + setSearch(value) + }} + onFocus={() => { + if (isShowDropdown) { + showDropdown() + } + }} + /> + - - - ) - }} - + + ) } diff --git a/src/components/CommentDigest/Content/index.tsx b/src/components/CommentDigest/Content/index.tsx index 74ae1db007..41d7a375ee 100644 --- a/src/components/CommentDigest/Content/index.tsx +++ b/src/components/CommentDigest/Content/index.tsx @@ -21,6 +21,7 @@ const CommentContent = ({ __html: content || '' }} /> + @@ -34,6 +35,7 @@ const CommentContent = ({ zh_hant="此評論因違反用戶協定而被隱藏" zh_hans="此评论因违反用户协定而被隐藏" /> +

) @@ -46,6 +48,7 @@ const CommentContent = ({ zh_hant={TEXT.zh_hant.commentDeleted} zh_hans={TEXT.zh_hans.commentDeleted} /> +

) diff --git a/src/components/CommentDigest/DropdownActions/DeleteButton.tsx b/src/components/CommentDigest/DropdownActions/DeleteButton.tsx index caed296e63..fda59e1d34 100644 --- a/src/components/CommentDigest/DropdownActions/DeleteButton.tsx +++ b/src/components/CommentDigest/DropdownActions/DeleteButton.tsx @@ -1,12 +1,12 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { Icon, TextIcon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { TEXT } from '~/common/enums' import ICON_REMOVE from '~/static/icons/remove.svg?sprite' +import { DeleteComment } from './__generated__/DeleteComment' import styles from './styles.css' const DELETE_COMMENT = gql` @@ -22,45 +22,43 @@ const DeleteButton: React.FC<{ commentId: string hideDropdown: () => void }> = ({ commentId, hideDropdown }) => { + const [deleteComment] = useMutation(DELETE_COMMENT, { + variables: { id: commentId }, + optimisticResponse: { + deleteComment: { + id: commentId, + state: 'archived' as any, + __typename: 'Comment' + } + } + }) + return ( - { + deleteComment() + hideDropdown() }} > - {(deleteComment: any) => ( - - )} - + + } + spacing="tight" + > + + + + + ) } diff --git a/src/components/CommentDigest/DropdownActions/EditButton.tsx b/src/components/CommentDigest/DropdownActions/EditButton.tsx index e97c0c5cb1..0350446554 100644 --- a/src/components/CommentDigest/DropdownActions/EditButton.tsx +++ b/src/components/CommentDigest/DropdownActions/EditButton.tsx @@ -32,6 +32,7 @@ const EditButton = ({ > + ) diff --git a/src/components/CommentDigest/DropdownActions/PinButton.tsx b/src/components/CommentDigest/DropdownActions/PinButton.tsx index bfa9e4a623..652874e889 100644 --- a/src/components/CommentDigest/DropdownActions/PinButton.tsx +++ b/src/components/CommentDigest/DropdownActions/PinButton.tsx @@ -1,14 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { Icon, TextIcon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { TEXT } from '~/common/enums' import ICON_PIN_TO_TOP from '~/static/icons/pin-to-top.svg?sprite' import ICON_UNPIN from '~/static/icons/unpin.svg?sprite' import { PinButtonComment } from './__generated__/PinButtonComment' +import { PinComment } from './__generated__/PinComment' +import { UnpinComment } from './__generated__/UnpinComment' import styles from './styles.css' const PIN_COMMENT = gql` @@ -86,62 +87,62 @@ const PinButton = ({ hideDropdown: () => void }) => { const canPin = comment.article.pinCommentLeft > 0 + const [unpinComment] = useMutation(UNPIN_COMMENT, { + variables: { id: comment.id }, + optimisticResponse: { + unpinComment: { + id: comment.id, + pinned: false, + article: { + ...comment.article + }, + __typename: 'Comment' + } + } + }) + const [pinComment] = useMutation(PIN_COMMENT, { + variables: { id: comment.id }, + optimisticResponse: { + pinComment: { + id: comment.id, + pinned: true, + article: { + ...comment.article + }, + __typename: 'Comment' + } + } + }) if (comment.pinned) { return ( - { + unpinComment() + hideDropdown() }} > - {(unpinComment: any) => ( - - )} - + + + + ) } return ( - { + pinComment() + hideDropdown() }} + disabled={!canPin} > - {(pinComment: any) => ( - - )} - + + + + ) } diff --git a/src/components/CommentDigest/DropdownActions/ReportButton.tsx b/src/components/CommentDigest/DropdownActions/ReportButton.tsx index 24f1fd865c..f07a3550e1 100644 --- a/src/components/CommentDigest/DropdownActions/ReportButton.tsx +++ b/src/components/CommentDigest/DropdownActions/ReportButton.tsx @@ -31,6 +31,7 @@ const EditButton = ({ zh_hans={TEXT.zh_hans.report} /> + ) diff --git a/src/components/CommentDigest/DropdownActions/index.tsx b/src/components/CommentDigest/DropdownActions/index.tsx index de0be77ce2..7a5b4a083a 100644 --- a/src/components/CommentDigest/DropdownActions/index.tsx +++ b/src/components/CommentDigest/DropdownActions/index.tsx @@ -2,6 +2,7 @@ import gql from 'graphql-tag' import { useContext, useState } from 'react' import { Dropdown, Icon, Menu, PopperInstance } from '~/components' +import BlockUserButton from '~/components/Button/BlockUser/Dropdown' import { ViewerContext } from '~/components/Viewer' import ICON_MORE_SMALL from '~/static/icons/more-small.svg?sprite' @@ -19,6 +20,7 @@ const fragments = { state author { id + ...BlockUser } parentComment { id @@ -33,50 +35,10 @@ const fragments = { ...PinButtonComment } ${PinButton.fragments.comment} + ${BlockUserButton.fragments.user} ` } -const DropdownContent: React.FC<{ - comment: DropdownActionsComment - hideDropdown: () => void - editComment?: () => void - isShowPinButton: boolean - isShowEditButton: boolean - isShowDeleteButton: boolean -}> = ({ - comment, - editComment, - hideDropdown, - isShowPinButton, - isShowEditButton, - isShowDeleteButton -}) => { - return ( - - {isShowPinButton && ( - - - - )} - {isShowEditButton && editComment && ( - - - - )} - {/* {!isCommentAuthor && isActive && ( - - - - )} */} - {isShowDeleteButton && ( - - - - )} - - ) -} - const DropdownActions = ({ comment, editComment @@ -84,6 +46,7 @@ const DropdownActions = ({ comment: DropdownActionsComment editComment?: () => void }) => { + const [shown, setShown] = useState(false) const [instance, setInstance] = useState(null) const hideDropdown = () => { if (!instance) { @@ -100,12 +63,17 @@ const DropdownActions = ({ const isCommentAuthor = viewer.id === comment.author.id const isActive = comment.state === 'active' const isDescendantComment = comment.parentComment + const isShowPinButton = isArticleAuthor && isActive && !isDescendantComment const isShowEditButton = isCommentAuthor && !!editComment && isActive const isShowDeleteButton = isCommentAuthor && isActive + const isShowBlockUserButton = !isCommentAuthor if ( - (!isShowPinButton && !isShowEditButton && !isShowDeleteButton) || + (!isShowPinButton && + !isShowEditButton && + !isShowDeleteButton && + !isShowBlockUserButton) || viewer.isInactive ) { return null @@ -114,17 +82,47 @@ const DropdownActions = ({ return ( + + {isShowPinButton && ( + + + + )} + {isShowEditButton && editComment && ( + + + + )} + {/* {!isCommentAuthor && isActive && ( + + + + )} */} + {isShowDeleteButton && ( + + + + )} + {isShowBlockUserButton && ( + + + + )} + } trigger="click" onCreate={setInstance} + onShown={() => setShown(true)} placement="bottom-end" zIndex={301} > diff --git a/src/components/CommentDigest/FeedDigest/index.tsx b/src/components/CommentDigest/FeedDigest/index.tsx index 52ce881496..6c46e76cd4 100644 --- a/src/components/CommentDigest/FeedDigest/index.tsx +++ b/src/components/CommentDigest/FeedDigest/index.tsx @@ -1,11 +1,11 @@ import classNames from 'classnames' -import _get from 'lodash/get' import { useState } from 'react' import CommentForm from '~/components/Form/CommentForm' import { FeedDigestComment, - FeedDigestComment_comments_edges_node + FeedDigestComment_comments_edges_node, + FeedDigestComment_comments_edges_node_replyTo_author } from '~/components/GQL/fragments/__generated__/FeedDigestComment' import commentFragments from '~/components/GQL/fragments/comment' import { Icon } from '~/components/Icon' @@ -28,11 +28,18 @@ const fragments = { comment: commentFragments.feed } -const ReplyTo = ({ user, inArticle }: { user: any; inArticle: boolean }) => ( +const ReplyTo = ({ + user, + inArticle +}: { + user: FeedDigestComment_comments_edges_node_replyTo_author + inArticle: boolean +}) => (
+ ( spacing="xxtight" hasUserName={inArticle} /> +
) @@ -52,6 +60,7 @@ const PinnedLabel = () => ( zh_hans={TEXT.zh_hant.authorRecommend} /> + ) @@ -59,6 +68,7 @@ const PinnedLabel = () => ( const CancelEditButton = ({ onClick }: { onClick: () => void }) => ( ) @@ -111,10 +121,11 @@ const DescendantComment = ({ setEdit(false)} extraButton={ setEdit(false)} />} + blocked={comment.article.author.isBlocking} defaultExpand={edit} + defaultContent={comment.content} /> )} {!edit && ( @@ -149,9 +160,10 @@ const FeedDigest = ({ } & FooterActionsControls) => { const [edit, setEdit] = useState(false) const { state, content, author, replyTo, parentComment, pinned } = comment - const descendantComments = _get(comment, 'comments.edges', []).filter( - ({ node }: { node: any }) => node.state === 'active' - ) + const descendantComments = ( + (comment.comments && comment.comments.edges) || + [] + ).filter(({ node }) => node.state === 'active') const restDescendantCommentCount = descendantComments.length - COLLAPSE_DESCENDANT_COUNT const [expand, setExpand] = useState( @@ -191,9 +203,10 @@ const FeedDigest = ({ setEdit(false)} extraButton={ setEdit(false)} />} + blocked={comment.article.author.isBlocking} + defaultContent={comment.content} defaultExpand={edit} /> )} @@ -211,7 +224,7 @@ const FeedDigest = ({
    {descendantComments .slice(0, expand ? undefined : COLLAPSE_DESCENDANT_COUNT) - .map(({ node, cursor }: { node: any; cursor: any }) => ( + .map(({ node, cursor }) => (
  • { + const viewer = useContext(ViewerContext) + const [unvote] = useMutation(UNVOTE_COMMENT, { + variables: { id: comment.id }, + optimisticResponse: { + unvoteComment: { + id: comment.id, + upvotes: comment.upvotes, + downvotes: comment.downvotes - 1, + myVote: null, + __typename: 'Comment' + } + } + }) + const [downvote] = useMutation(VOTE_COMMENT, { + variables: { id: comment.id, vote: 'down' }, + optimisticResponse: { + voteComment: { + id: comment.id, + upvotes: + comment.myVote === 'up' ? comment.upvotes - 1 : comment.upvotes, + downvotes: comment.downvotes + 1, + myVote: 'down' as any, + __typename: 'Comment' + } + } + }) + if (comment.myVote === 'down') { return ( - - {(unvote: any, { data }: any) => ( - )} - + ) } return ( - - {(downvote: any, { data }: any) => ( - )} - + ) } diff --git a/src/components/CommentDigest/FooterActions/UpvoteButton.tsx b/src/components/CommentDigest/FooterActions/UpvoteButton.tsx index 0f276e994c..b836b6d9f4 100644 --- a/src/components/CommentDigest/FooterActions/UpvoteButton.tsx +++ b/src/components/CommentDigest/FooterActions/UpvoteButton.tsx @@ -1,12 +1,16 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' +import { useContext } from 'react' import { Icon, TextIcon } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { UnvoteComment } from '~/components/GQL/mutations/__generated__/UnvoteComment' +import { VoteComment } from '~/components/GQL/mutations/__generated__/VoteComment' import { UNVOTE_COMMENT, VOTE_COMMENT } from '~/components/GQL/mutations/voteComment' +import { ModalSwitch } from '~/components/ModalManager' +import { ViewerContext } from '~/components/Viewer' import { numAbbr } from '~/common/utils' import ICON_LIKE_ACTIVE from '~/static/icons/like-active.svg?sprite' @@ -47,23 +51,48 @@ const UpvoteButton = ({ comment: UpvoteComment disabled?: boolean }) => { + const viewer = useContext(ViewerContext) + const [unvote] = useMutation(UNVOTE_COMMENT, { + variables: { id: comment.id }, + optimisticResponse: { + unvoteComment: { + id: comment.id, + upvotes: comment.upvotes - 1, + downvotes: comment.downvotes, + myVote: null, + __typename: 'Comment' + } + } + }) + const [upvote] = useMutation(VOTE_COMMENT, { + variables: { id: comment.id, vote: 'up' }, + optimisticResponse: { + voteComment: { + id: comment.id, + upvotes: comment.upvotes + 1, + downvotes: + comment.myVote === 'down' ? comment.downvotes - 1 : comment.downvotes, + myVote: 'up' as any, + __typename: 'Comment' + } + } + }) + if (comment.myVote === 'up') { return ( - - {(unvote: any, { data }: any) => ( - )} - + ) } return ( - - {(upvote: any, { data }: any) => ( - )} - + ) } diff --git a/src/components/CommentDigest/FooterActions/index.tsx b/src/components/CommentDigest/FooterActions/index.tsx index 6f9a13d860..3316b9e58a 100644 --- a/src/components/CommentDigest/FooterActions/index.tsx +++ b/src/components/CommentDigest/FooterActions/index.tsx @@ -1,11 +1,11 @@ import gql from 'graphql-tag' import jump from 'jump.js' -import _get from 'lodash/get' import { useRouter } from 'next/router' import { useContext, useState } from 'react' import { DateTime, Icon } from '~/components' import CommentForm from '~/components/Form/CommentForm' +import { ModalSwitch } from '~/components/ModalManager' import { ViewerContext } from '~/components/Viewer' import { PATHS } from '~/common/enums' @@ -38,7 +38,9 @@ const fragments = { slug mediaHash author { + id userName + isBlocking } } parentComment { @@ -73,6 +75,7 @@ const FooterActions: React.FC & { const { parentComment, id } = comment const { slug, mediaHash, author } = comment.article + const isBlockedByAuthor = author.isBlocking const fragment = parentComment && parentComment.id ? `${parentComment.id}-${id}` : id const commentPath = @@ -110,20 +113,31 @@ const FooterActions: React.FC & { {hasForm && ( <> - + + + {(open: any) => ( + + )} + )} @@ -152,9 +166,12 @@ const FooterActions: React.FC & { )} diff --git a/src/components/DateTime/index.tsx b/src/components/DateTime/index.tsx index 830a074382..3ba859a1c3 100644 --- a/src/components/DateTime/index.tsx +++ b/src/components/DateTime/index.tsx @@ -33,6 +33,7 @@ export const DateTime: React.FC = ({ {({ lang }) => ( )} diff --git a/src/components/DraftDigest/Components/DeleteButton.tsx b/src/components/DraftDigest/Components/DeleteButton.tsx index de094f329b..64b65a8415 100644 --- a/src/components/DraftDigest/Components/DeleteButton.tsx +++ b/src/components/DraftDigest/Components/DeleteButton.tsx @@ -1,10 +1,13 @@ import gql from 'graphql-tag' import { Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { TEXT } from '~/common/enums' +import { DeleteDraft } from './__generated__/DeleteDraft' +import { ViewerDrafts } from './__generated__/ViewerDrafts' + const DELETE_DRAFT = gql` mutation DeleteDraft($id: ID!) { deleteDraft(input: { id: $id }) @@ -27,53 +30,47 @@ const ME_DRADTS = gql` ` const DeleteButton = ({ id }: { id: string }) => { - return ( - { - try { - const data = cache.readQuery({ query: ME_DRADTS }) + const [deleteDraft] = useMutation(DELETE_DRAFT, { + variables: { id }, + update: cache => { + try { + const data = cache.readQuery({ query: ME_DRADTS }) - if ( - !data || - !data.viewer || - !data.viewer.drafts || - !data.viewer.drafts.edges - ) { - return - } + if ( + !data || + !data.viewer || + !data.viewer.drafts || + !data.viewer.drafts.edges + ) { + return + } - const edges = data.viewer.drafts.edges.filter( - ({ node }: { node: any }) => node.id !== id - ) + const edges = data.viewer.drafts.edges.filter( + ({ node }) => node.id !== id + ) - cache.writeQuery({ - query: ME_DRADTS, - data: { - viewer: { - ...data.viewer, - drafts: { - ...data.viewer.drafts, - edges - } + cache.writeQuery({ + query: ME_DRADTS, + data: { + viewer: { + ...data.viewer, + drafts: { + ...data.viewer.drafts, + edges } } - }) - } catch (e) { - console.error(e) - } - }} - > - {(deleteDraft: any) => ( - - )} - + } + }) + } catch (e) { + console.error(e) + } + } + }) + + return ( + ) } diff --git a/src/components/DraftDigest/Components/PendingState.tsx b/src/components/DraftDigest/Components/PendingState.tsx index 04bc6fc452..dbc61b3629 100644 --- a/src/components/DraftDigest/Components/PendingState.tsx +++ b/src/components/DraftDigest/Components/PendingState.tsx @@ -1,4 +1,4 @@ -import { Query, QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Icon, TextIcon, Translate } from '~/components' import { DraftPublishState } from '~/components/GQL/queries/__generated__/DraftPublishState' @@ -19,69 +19,63 @@ const PendingState = ({ draft }: { draft: FeedDigestDraft }) => { } = useCountdown({ timeLeft: Date.parse(scheduledAt) - Date.now() }) const isPublishing = !scheduledAt || !timeLeft || timeLeft <= 0 - return ( - - {({ data }: QueryResult & { data: DraftPublishState }) => { - if ( - data && - data.node && - data.node.publishState === 'published' && - process.browser - ) { - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'green', - content: ( - - ) - } - }) + const { data } = useQuery(DRAFT_PUBLISH_STATE, { + variables: { id: draft.id }, + pollInterval: 1000 * 5, + errorPolicy: 'none', + fetchPolicy: 'network-only', + skip: !process.browser || !isPublishing + }) + + if ( + data && + data.node && + data.node.__typename === 'Draft' && + data.node.publishState === 'published' && + process.browser + ) { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'green', + content: ( + ) } + }) + ) + } - return ( - - } - size="sm" - color="green" - weight="medium" - > - {isPublishing ? ( - - ) : ( - - )} - - ) - }} - + return ( + + } + size="sm" + color="green" + weight="medium" + > + {isPublishing ? ( + + ) : ( + + )} + ) } diff --git a/src/components/DraftDigest/Components/RecallButton.tsx b/src/components/DraftDigest/Components/RecallButton.tsx index dfd102719b..da7411ba09 100644 --- a/src/components/DraftDigest/Components/RecallButton.tsx +++ b/src/components/DraftDigest/Components/RecallButton.tsx @@ -1,10 +1,12 @@ import gql from 'graphql-tag' import { Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { TEXT } from '~/common/enums' +import { RecallPublish } from './__generated__/RecallPublish' + const RECALL_PUBLISH = gql` mutation RecallPublish($id: ID!) { recallPublish(input: { id: $id }) { @@ -16,30 +18,27 @@ const RECALL_PUBLISH = gql` ` const RecallButton = ({ id, text }: { id: string; text?: React.ReactNode }) => { + const [recall] = useMutation(RECALL_PUBLISH, { + variables: { id }, + optimisticResponse: { + recallPublish: { + id, + scheduledAt: null, + publishState: 'unpublished' as any, + __typename: 'Draft' + } + } + }) + return ( - - {(recall: any) => ( - + ) } diff --git a/src/components/DraftDigest/Components/RetryButton.tsx b/src/components/DraftDigest/Components/RetryButton.tsx index b947b30745..9512ec0f10 100644 --- a/src/components/DraftDigest/Components/RetryButton.tsx +++ b/src/components/DraftDigest/Components/RetryButton.tsx @@ -1,10 +1,12 @@ import gql from 'graphql-tag' import { Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { TEXT } from '~/common/enums' +import { RetryPublish } from './__generated__/RetryPublish' + const RETRY_PUBLISH = gql` mutation RetryPublish($id: ID!) { retryPublish: publishArticle(input: { id: $id }) { @@ -16,31 +18,22 @@ const RETRY_PUBLISH = gql` ` const RetryButton = ({ id }: { id: string }) => { + const [retry] = useMutation(RETRY_PUBLISH, { + variables: { id }, + optimisticResponse: { + retryPublish: { + id, + scheduledAt: new Date(Date.now() + 1000).toISOString(), + publishState: 'pending' as any, + __typename: 'Draft' + } + } + }) + return ( - - {(retry: any) => ( - - )} - + ) } diff --git a/src/components/Drawer/index.tsx b/src/components/Drawer/index.tsx deleted file mode 100644 index a62edc018e..0000000000 --- a/src/components/Drawer/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock' -import React, { useState } from 'react' - -import { dom } from '~/common/utils' - -import styles from './styles.css' - -export const DrawerContext = React.createContext({ - opened: false, - open: () => { - // Do nothing - }, - close: () => { - // Do nothing - } -}) - -export const DrawerConsumer = DrawerContext.Consumer - -export const DrawerProvider = ({ children }: { children: React.ReactNode }) => { - const [opened, setOpened] = useState(false) - - return ( - setOpened(true), - close: () => setOpened(false) - }} - > - {children} - - ) -} - -interface DrawerState { - width: null | undefined | number -} - -export class Drawer extends React.Component<{}, DrawerState> { - public state = { width: null } - - public componentDidMount() { - window.addEventListener('resize', this.handleResize) - this.handleResize() - } - - public componentWillUnmount() { - window.removeEventListener('resize', this.handleResize) - } - - public handleResize = () => { - this.setState({ width: this.calcWidth() }) - } - - public calcWidth = () => { - if (!process.browser) { - return null - } - - try { - return dom.getWindowWidth() - dom.offset(dom.$('#drawer-calc-hook')).left - } catch (e) { - return null - } - } - - public render() { - const { width } = this.state - - return ( - - {({ opened }) => { - return ( - - ) - }} - - ) - } -} diff --git a/src/components/Drawer/styles.css b/src/components/Drawer/styles.css deleted file mode 100644 index 817615bab3..0000000000 --- a/src/components/Drawer/styles.css +++ /dev/null @@ -1,42 +0,0 @@ -aside { - @mixin all-transition; - - position: fixed; - top: 0; - bottom: 0; - right: 0; - - padding: var(--spacing-loose) var(--spacing-tight); - - box-shadow: -8px 0 24px 0 rgba(0, 0, 0, 0.08); - background-color: var(--color-white); - transform: translateX(100%); - opacity: 0; - overflow: auto; - -webkit-overflow-scrolling: touch; - z-index: 110; - - @media (--lg-down) { - width: 30rem !important; - } - - @media (--sm-down) { - width: 100% !important; - } - - @media (--sm-up) { - margin-top: var(--global-header-height); /* fallback */ - margin-top: calc(var(--global-header-height) + 1px); - padding: var(--spacing-loose) var(--spacing-default); - z-index: 80; - } - - @supports (position: sticky) { - padding-top: 0; - } - - &.opened { - transform: translateX(0%); - opacity: 1; - } -} diff --git a/src/components/Dropdown/ArticleList/index.tsx b/src/components/Dropdown/ArticleList/index.tsx index 40de116def..bb38dfd4e1 100644 --- a/src/components/Dropdown/ArticleList/index.tsx +++ b/src/components/Dropdown/ArticleList/index.tsx @@ -25,28 +25,27 @@ const DropdownArticleList = ({ } return ( - <> - - {articles.map(article => ( - + {articles.map(article => ( + + - - ))} - - - + + + + + + ))} + ) } diff --git a/src/components/Dropdown/UserList/index.tsx b/src/components/Dropdown/UserList/index.tsx index 34571de7ae..1df8d7d9b0 100644 --- a/src/components/Dropdown/UserList/index.tsx +++ b/src/components/Dropdown/UserList/index.tsx @@ -45,6 +45,7 @@ const DropdownUserList = ({ ))} + ) diff --git a/src/components/Editor/CommentEditor/index.tsx b/src/components/Editor/CommentEditor/index.tsx index 72e48bb32f..185aedbc27 100644 --- a/src/components/Editor/CommentEditor/index.tsx +++ b/src/components/Editor/CommentEditor/index.tsx @@ -1,21 +1,20 @@ +import { QueryLazyOptions } from '@apollo/react-hooks' import classNames from 'classnames' -import _debounce from 'lodash/debounce' -import _get from 'lodash/get' -import React from 'react' -import { QueryResult } from 'react-apollo' +import React, { useContext } from 'react' +import { QueryResult, useLazyQuery } from 'react-apollo' import ReactQuill, { Quill } from 'react-quill' +import { useDebouncedCallback } from 'use-debounce/lib' import UserList from '~/components/Dropdown/UserList' -import { Query } from '~/components/GQL' import { SearchUsers, SearchUsers_search_edges_node_User } from '~/components/GQL/queries/__generated__/SearchUsers' import SEARCH_USERS from '~/components/GQL/queries/searchUsers' -import { LanguageConsumer } from '~/components/Language' +import { LanguageContext } from '~/components/Language' import { Spinner } from '~/components/Spinner' -import { TEXT } from '~/common/enums' +import { INPUT_DEBOUNCE, TEXT } from '~/common/enums' import contentStyles from '~/common/styles/utils/content.comment.css' import bubbleStyles from '~/common/styles/vendors/quill.bubble.css' import { translate } from '~/common/utils' @@ -29,6 +28,10 @@ interface Props { handleChange: (props: any) => any handleBlur?: (props: any) => any lang: Language + searchUsers: { + query: (options?: QueryLazyOptions> | undefined) => void + result: QueryResult> + } } interface State { @@ -93,7 +96,17 @@ class CommentEditor extends React.Component { } onMentionChange = (search: string) => { - this.setState({ search }) + const { searchUsers } = this.props + const prevSearch = this.state.search + + this.setState({ search }, () => { + // toggle search users for mention + if (prevSearch !== search) { + searchUsers.query({ + variables: { search } + }) + } + }) } onMentionModuleInit = (instance: any) => { @@ -101,8 +114,8 @@ class CommentEditor extends React.Component { } render() { - const { focus, search, mentionInstance } = this.state - const { content, expand, handleChange, lang } = this.props + const { content, expand, handleChange, lang, searchUsers } = this.props + const { focus, mentionInstance } = this.state const containerClasses = classNames({ container: true, focus @@ -113,6 +126,11 @@ class CommentEditor extends React.Component { lang }) + const { data, loading } = searchUsers.result + const users = ((data && data.search.edges) || []).map( + ({ node }) => node + ) as SearchUsers_search_edges_node_User[] + if (!expand) { return ( <> @@ -121,81 +139,82 @@ class CommentEditor extends React.Component { placeholder={placeholder} aria-label={placeholder} /> + ) } return ( - - {({ data, loading }: QueryResult & { data: SearchUsers }) => { - const users = _get(data, 'search.edges', []).map( - ({ node }: { node: SearchUsers_search_edges_node_User }) => node - ) - - return ( - <> -
    - this.setState({ focus: true })} - onBlur={() => this.setState({ focus: false })} - bounds="#comment-editor" - /> - - -
    - - - - - - ) - }} -
    +
    + this.setState({ focus: true })} + onBlur={() => this.setState({ focus: false })} + bounds="#comment-editor" + /> + + + + + + +
    ) } } -export default (props: Omit) => ( - - {({ lang }) => } - -) +const CommentEditorWrap = (props: Omit) => { + const { lang } = useContext(LanguageContext) + const [search, result] = useLazyQuery(SEARCH_USERS) + const [debouncedSearch] = useDebouncedCallback(search, INPUT_DEBOUNCE) + + return ( + + ) +} + +export default CommentEditorWrap diff --git a/src/components/Editor/SideToolbar/UploadAudioButton.tsx b/src/components/Editor/SideToolbar/UploadAudioButton.tsx index 79537d397a..dde0b21cd7 100644 --- a/src/components/Editor/SideToolbar/UploadAudioButton.tsx +++ b/src/components/Editor/SideToolbar/UploadAudioButton.tsx @@ -129,12 +129,14 @@ const UploadAudioButton = ({ aria-label="新增音頻" onChange={(event: any) => handleUploadChange(event)} /> + + diff --git a/src/components/Editor/SideToolbar/UploadImageButton.tsx b/src/components/Editor/SideToolbar/UploadImageButton.tsx index d782acc994..c8afe14997 100644 --- a/src/components/Editor/SideToolbar/UploadImageButton.tsx +++ b/src/components/Editor/SideToolbar/UploadImageButton.tsx @@ -114,12 +114,14 @@ const UploadImageButton = ({ aria-label="新增圖片" onChange={(event: any) => handleUploadChange(event)} /> + + ) diff --git a/src/components/Editor/SideToolbar/index.tsx b/src/components/Editor/SideToolbar/index.tsx index bf9f8c8f17..7f23a2751d 100644 --- a/src/components/Editor/SideToolbar/index.tsx +++ b/src/components/Editor/SideToolbar/index.tsx @@ -69,6 +69,7 @@ const SideToolbar = ({ upload={upload} /> + ) diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index db77f77bb9..d931d290aa 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -1,21 +1,22 @@ +import { QueryLazyOptions } from '@apollo/react-hooks' import classNames from 'classnames' import _debounce from 'lodash/debounce' -import _get from 'lodash/get' import _includes from 'lodash/includes' -import React from 'react' -import { QueryResult } from 'react-apollo' +import React, { useContext } from 'react' +import { QueryResult, useLazyQuery } from 'react-apollo' import ReactQuill, { Quill } from 'react-quill' +import { useDebouncedCallback } from 'use-debounce/lib' import UserList from '~/components/Dropdown/UserList' -import { Query } from '~/components/GQL' import { SearchUsers, SearchUsers_search_edges_node_User } from '~/components/GQL/queries/__generated__/SearchUsers' import SEARCH_USERS from '~/components/GQL/queries/searchUsers' -import { LanguageConsumer } from '~/components/Language' +import { LanguageContext } from '~/components/Language' import { Spinner } from '~/components/Spinner' +import { INPUT_DEBOUNCE } from '~/common/enums' import contentStyles from '~/common/styles/utils/content.article.css' import bubbleStyles from '~/common/styles/vendors/quill.bubble.css' import { initAudioPlayers, translate, trimLineBreaks } from '~/common/utils' @@ -35,6 +36,10 @@ interface Props { draft: EditorDraft lang: Language upload: DraftAssetUpload + searchUsers: { + query: (options?: QueryLazyOptions> | undefined) => void + result: QueryResult> + } } interface State { @@ -65,7 +70,7 @@ class Editor extends React.Component { mentionInstance: null } - this.saveDraft = _debounce(this.saveDraft.bind(this), 300) + this.saveDraft = _debounce(this.saveDraft.bind(this), INPUT_DEBOUNCE) } componentDidMount() { @@ -74,7 +79,7 @@ class Editor extends React.Component { initAudioPlayers() } - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: Props, prevState: State) { this.attachQuillRefs() this.resetLinkInputPlaceholder() initAudioPlayers() @@ -179,7 +184,17 @@ class Editor extends React.Component { } onMentionChange = (search: string) => { - this.setState({ search }) + const { searchUsers } = this.props + const prevSearch = this.state.search + + this.setState({ search }, () => { + // toggle search users for mention + if (prevSearch !== search) { + searchUsers.query({ + variables: { search } + }) + } + }) } onMentionModuleInit = (instance: any) => { @@ -194,8 +209,8 @@ class Editor extends React.Component { } render() { - const { draft, onSave, lang, upload } = this.props - const { search, mentionInstance } = this.state + const { draft, onSave, lang, upload, searchUsers } = this.props + const { mentionInstance } = this.state const isPending = draft.publishState === 'pending' const isPublished = draft.publishState === 'published' const containerClasses = classNames({ @@ -203,94 +218,99 @@ class Editor extends React.Component { 'u-area-disable': isPending || isPublished }) + const { data, loading } = searchUsers.result + const users = ((data && data.search.edges) || []).map( + ({ node }) => node + ) as SearchUsers_search_edges_node_User[] + if (this.quill) { this.quill.clipboard.addMatcher('IMG', createImageMatcher(upload)) } return ( - - {({ data, loading }: QueryResult & { data: SearchUsers }) => { - const users = _get(data, 'search.edges', []).map( - ({ node }: { node: SearchUsers_search_edges_node_User }) => node - ) - - return ( - <> -
    - - - - -
    - - - - - - ) - }} -
    +
    + + + + + + + + +
    ) } } -export default (props: Omit) => ( - - {({ lang }) => } - -) +const EditorWrap = (props: Omit) => { + const { lang } = useContext(LanguageContext) + const [search, result] = useLazyQuery(SEARCH_USERS) + const [debouncedSearch] = useDebouncedCallback(search, INPUT_DEBOUNCE) + + return ( + + ) +} + +export default EditorWrap diff --git a/src/components/Empty/EmptyFollowee.tsx b/src/components/Empty/EmptyFollowee.tsx deleted file mode 100644 index 14c66132a8..0000000000 --- a/src/components/Empty/EmptyFollowee.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Empty, Icon, Translate } from '~/components' - -import ICON_EMPTY_WARNING from '~/static/icons/empty-warning.svg?sprite' - -const EmptyFollowee = ({ description }: { description?: React.ReactNode }) => ( - - } - description={ - description || ( - - ) - } - /> -) - -export default EmptyFollowee diff --git a/src/components/Empty/EmptyFollower.tsx b/src/components/Empty/EmptyFollower.tsx deleted file mode 100644 index 52597e7e5c..0000000000 --- a/src/components/Empty/EmptyFollower.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Empty, Icon, Translate } from '~/components' - -import ICON_EMPTY_WARNING from '~/static/icons/empty-warning.svg?sprite' - -const EmptyFollower = ({ description }: { description?: React.ReactNode }) => ( - - } - description={ - description || - } - /> -) - -export default EmptyFollower diff --git a/src/components/Empty/EmptyWarning.tsx b/src/components/Empty/EmptyWarning.tsx new file mode 100644 index 0000000000..bddfcdbb6a --- /dev/null +++ b/src/components/Empty/EmptyWarning.tsx @@ -0,0 +1,18 @@ +import { Empty, Icon } from '~/components' + +import ICON_EMPTY_WARNING from '~/static/icons/empty-warning.svg?sprite' + +const EmptyWarning = ({ description }: { description: React.ReactNode }) => ( + + } + description={description} + /> +) + +export default EmptyWarning diff --git a/src/components/Empty/index.tsx b/src/components/Empty/index.tsx index cc6b8d26ee..85285da997 100644 --- a/src/components/Empty/index.tsx +++ b/src/components/Empty/index.tsx @@ -34,6 +34,7 @@ export const Empty: React.FC = ({ {icon &&
    {icon}
    } {description &&
    {description}
    } {children &&
    {children}
    } + ) diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx index ae3c245be7..d41130c09a 100644 --- a/src/components/Error/index.tsx +++ b/src/components/Error/index.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import getConfig from 'next/config' import { Translate } from '~/components' @@ -44,12 +45,19 @@ export const Error: React.FC = ({ error, type = 'network' }) => { + const errorCodeClass = classNames({ + 'error-code': true, + small: typeof statusCode === 'string' && statusCode.length > 3 + }) + return (
    - {statusCode &&

    {statusCode}

    } + + {statusCode &&

    {statusCode}

    } +

    {type === 'not_found' ? ( @@ -59,7 +67,9 @@ export const Error: React.FC = ({ )}

    + {children &&
    {children}
    } + {error && !isProd && (
     = ({
               }}
             />
           )}
    +
           
         
    ) diff --git a/src/components/Error/styles.css b/src/components/Error/styles.css index 61fc94a85b..1d517a08fb 100644 --- a/src/components/Error/styles.css +++ b/src/components/Error/styles.css @@ -14,6 +14,10 @@ font-size: 3rem; font-weight: var(--font-weight-normal); line-height: 1.16666667; + + &.small { + font-size: var(--font-size-xl); + } } .error-message { diff --git a/src/components/FileUploader/ProfileAvatar/index.tsx b/src/components/FileUploader/ProfileAvatar/index.tsx index 2175f92b9e..8b10f80a88 100644 --- a/src/components/FileUploader/ProfileAvatar/index.tsx +++ b/src/components/FileUploader/ProfileAvatar/index.tsx @@ -1,8 +1,9 @@ import gql from 'graphql-tag' -import { FC, useState } from 'react' +import { useState } from 'react' import { Avatar } from '~/components/Avatar' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { SingleFileUpload } from '~/components/GQL/mutations/__generated__/SingleFileUpload' import UPLOAD_FILE from '~/components/GQL/mutations/uploadFile' import { Icon } from '~/components/Icon' import { Translate } from '~/components/Language' @@ -13,6 +14,7 @@ import { } from '~/common/enums' import ICON_CAMERA from '~/static/icons/camera-white.svg?sprite' +import { UpdateUserInfoAvatar } from './__generated__/UpdateUserInfoAvatar' import styles from './styles.css' /** @@ -38,12 +40,13 @@ const UPDATE_USER_INFO = gql` } ` -export const ProfileAvatarUploader: FC = ({ user }) => { - const acceptTypes = ACCEPTED_UPLOAD_IMAGE_TYPES.join(',') - +export const ProfileAvatarUploader: React.FC = ({ user }) => { + const [update] = useMutation(UPDATE_USER_INFO) + const [upload] = useMutation(UPLOAD_FILE) const [error, setError] = useState<'size' | undefined>(undefined) + const acceptTypes = ACCEPTED_UPLOAD_IMAGE_TYPES.join(',') - const handleChange = (event: any, upload: any, update: any) => { + const handleChange = (event: React.ChangeEvent) => { event.stopPropagation() if (!upload || !event.target || !event.target.files) { @@ -63,70 +66,54 @@ export const ProfileAvatarUploader: FC = ({ user }) => { input: { file, type: 'avatar', entityType: 'user' } } }) - .then(({ data }: any) => { - const { - singleFileUpload: { id } - } = data + .then(({ data }) => { + const id = data && data.singleFileUpload.id if (update) { return update({ variables: { input: { avatar: id } } }) } }) - .then((result: any) => { + .then(() => { setError(undefined) }) - .catch((result: any) => { + .catch(() => { // TODO: Handler error }) } - const Uploader = ({ - upload, - update - }: { - upload: () => {} - update: () => {} - }) => ( - <> -
    - -
    -
    - - - - -
    - handleChange(event, upload, update)} - /> -
    - {error === 'size' && ( - - )} -
    + return ( +
    + + +
    +
    + + + +
    -
    - - - ) - return ( - - {(update: any) => ( - - {(upload: any) => } - - )} - + handleChange(event)} + /> + +
    + {error === 'size' && ( + + )} +
    +
    + + +
    ) } diff --git a/src/components/FileUploader/ProfileCover/index.tsx b/src/components/FileUploader/ProfileCover/index.tsx index c2f0d48213..26c17a04c5 100644 --- a/src/components/FileUploader/ProfileCover/index.tsx +++ b/src/components/FileUploader/ProfileCover/index.tsx @@ -1,7 +1,8 @@ import gql from 'graphql-tag' -import { FC, useState } from 'react' +import { useState } from 'react' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { SingleFileUpload } from '~/components/GQL/mutations/__generated__/SingleFileUpload' import UPLOAD_FILE from '~/components/GQL/mutations/uploadFile' import { Icon } from '~/components/Icon' import { Translate } from '~/components/Language' @@ -13,6 +14,7 @@ import { } from '~/common/enums' import ICON_CAMERA from '~/static/icons/camera-white.svg?sprite' +import { UpdateUserInfoCover } from './__generated__/UpdateUserInfoCover' import styles from './styles.css' /** @@ -40,12 +42,13 @@ const UPDATE_USER_INFO = gql` } ` -export const ProfileCoverUploader: FC = ({ user }) => { - const acceptTypes = ACCEPTED_UPLOAD_IMAGE_TYPES.join(',') - +export const ProfileCoverUploader: React.FC = ({ user }) => { + const [update] = useMutation(UPDATE_USER_INFO) + const [upload] = useMutation(UPLOAD_FILE) const [error, setError] = useState<'size' | undefined>(undefined) + const acceptTypes = ACCEPTED_UPLOAD_IMAGE_TYPES.join(',') - const handleChange = (event: any, upload: any, update: any) => { + const handleChange = (event: React.ChangeEvent) => { event.stopPropagation() if (!upload || !event.target || !event.target.files) { @@ -65,24 +68,24 @@ export const ProfileCoverUploader: FC = ({ user }) => { input: { file, type: 'profileCover', entityType: 'user' } } }) - .then(({ data }: any) => { - const { - singleFileUpload: { id } - } = data + .then(({ data }) => { + const id = data && data.singleFileUpload.id if (update) { return update({ variables: { input: { profileCover: id } } }) } }) - .then((result: any) => { + .then(result => { setError(undefined) }) - .catch((result: any) => { + .catch(result => { // TODO: error handler }) } - const removeCover = (event: any, update: any) => { + const removeCover = ( + event: React.MouseEvent + ) => { event.stopPropagation() if (!update || !user.info.profileCover) { @@ -98,66 +101,49 @@ export const ProfileCoverUploader: FC = ({ user }) => { }) } - const Uploader = ({ - upload, - update - }: { - upload: () => {} - update: () => {} - }) => ( - <> -
    - -
    -
    - + {user.info.profileCover && ( + - {user.info.profileCover && ( - + )} +
    + {error === 'size' && ( + )} -
    - {error === 'size' && ( - - )} -
    - - - ) - return ( - - {(update: any) => ( - - {(upload: any) => } - - )} - + + ) } diff --git a/src/components/FileUploader/SignUpAvatar/index.tsx b/src/components/FileUploader/SignUpAvatar/index.tsx index d53fe6530e..2ec5def006 100644 --- a/src/components/FileUploader/SignUpAvatar/index.tsx +++ b/src/components/FileUploader/SignUpAvatar/index.tsx @@ -1,7 +1,8 @@ -import { FC, useState } from 'react' +import { useState } from 'react' import { Avatar } from '~/components/Avatar' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { SingleFileUpload } from '~/components/GQL/mutations/__generated__/SingleFileUpload' import UPLOAD_FILE from '~/components/GQL/mutations/uploadFile' import { Icon } from '~/components/Icon' @@ -31,39 +32,36 @@ import styles from './styles.css' interface Props { field: string lang: Language - uploadCallback: (field: string, value: any, validate?: boolean) => {} + uploadCallback: (field: string, value: any) => void } -export const SignUpAvatarUploader: FC = ({ +export const SignUpAvatarUploader: React.FC = ({ field, lang, uploadCallback }) => { - const [avatar, setAvatar] = useState(undefined) - - const [error, setError] = useState<'size' | undefined>(undefined) + const [upload] = useMutation(UPLOAD_FILE) + const [avatar, setAvatar] = useState() + const [error, setError] = useState<'size'>() const avatarText = translate({ zh_hant: '選擇圖片', zh_hans: '选择图片', lang }) - const avatarHint = translate({ zh_hant: '上傳圖片作為大頭照 (5 MB 內)', zh_hans: '上传图片作为头像 (5 MB 內)', lang }) - const sizeError = translate({ zh_hant: '上傳檔案超過 5 MB', zh_hans: '上传文件超过 5 MB', lang }) - const acceptTypes = ACCEPTED_UPLOAD_IMAGE_TYPES.join(',') - const handleChange = (event: any, upload: any) => { + const handleChange = (event: React.ChangeEvent) => { event.stopPropagation() if (!upload || !event.target || !event.target.files) { @@ -81,15 +79,15 @@ export const SignUpAvatarUploader: FC = ({ upload({ variables: { input: { file, type: 'avatar', entityType: 'user' } } }) - .then(({ data }: any) => { - const { - singleFileUpload: { id, path } - } = data + .then(({ data }) => { + const id = data && data.singleFileUpload.id + const path = data && data.singleFileUpload.path + setAvatar(path) setError(undefined) if (uploadCallback) { - uploadCallback(field, id, false) + uploadCallback(field, id) } }) .catch((result: any) => { @@ -97,40 +95,33 @@ export const SignUpAvatarUploader: FC = ({ }) } - const Uploader = ({ upload }: any) => ( - <> -
    -
    - -
    -
    -
    - - {avatarText} - handleChange(event, upload)} - /> -
    -
    {avatarHint}
    -
    {error === 'size' && sizeError}
    + return ( +
    +
    + +
    +
    +
    + + {avatarText} + handleChange(event)} + />
    -
    - - - ) +
    {avatarHint}
    +
    {error === 'size' && sizeError}
    +
    - return ( - - {(upload: any) => } - + +
    ) } diff --git a/src/components/Fingerprint/index.tsx b/src/components/Fingerprint/index.tsx index c42caf94b1..6ff4373153 100644 --- a/src/components/Fingerprint/index.tsx +++ b/src/components/Fingerprint/index.tsx @@ -1,9 +1,7 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Query } from '~/components/GQL' import { Icon } from '~/components/Icon' import { Translate } from '~/components/Language' import { Popover } from '~/components/Popper' @@ -20,6 +18,7 @@ import ICON_HELP from '~/static/icons/help.svg?sprite' import ICON_SHARE_LINK from '~/static/icons/share-link.svg?sprite' import { FingerprintArticle } from './__generated__/FingerprintArticle' +import { Gateways } from './__generated__/Gateways' import styles from './styles.css' const GATEWAYS = gql` @@ -30,16 +29,13 @@ const GATEWAYS = gql` } ` -const FingerprintContent = ({ - shown, - dataHash -}: { - shown: boolean - dataHash: string -}) => { +const FingerprintContent = ({ dataHash }: { dataHash: string }) => { const [gatewaysExpand, setGatewaysExpand] = useState(false) const [helpExpand, setHelpExpand] = useState(false) + const { loading, data } = useQuery(GATEWAYS) + const gateways = (data && data.official.gatewayUrls) || [] + return (
    @@ -129,44 +125,32 @@ const FingerprintContent = ({ - - {({ data, loading, error }: QueryResult & { data: any }) => { - if (loading) { - return - } - - const gateways: string[] = _get(data, 'official.gatewayUrls', []) + {loading && } +
      + {gateways.slice(0, gatewaysExpand ? undefined : 2).map((url, i) => { + const gatewayUrl = `${url}${dataHash}` return ( -
        - {gateways - .slice(0, gatewaysExpand ? undefined : 2) - .map((url, i) => { - const gatewayUrl = `${url}${dataHash}` - return ( -
      • - - - {gatewayUrl} - - - - -
      • - ) - })} -
      +
    • + + + {gatewayUrl} + + + + +
    • ) - }} - + })} +
    {/* help */} @@ -197,6 +181,7 @@ const FingerprintContent = ({

    )}
    +
    ) @@ -211,16 +196,11 @@ const Fingerprint = ({ color?: 'grey' | 'green' size?: 'xs' | 'sm' }) => { - const [shown, setShown] = useState(false) - return ( setShown(true)} - content={ - - } + content={} > diff --git a/src/components/Follow/AuthorPicker/index.tsx b/src/components/Follow/AuthorPicker/index.tsx index 68ce7bd47f..f84e7939ef 100644 --- a/src/components/Follow/AuthorPicker/index.tsx +++ b/src/components/Follow/AuthorPicker/index.tsx @@ -1,10 +1,9 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { PageHeader, ShuffleButton, Spinner, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import FullDesc from '~/components/UserDigest/FullDesc' import { numFormat } from '~/common/utils' @@ -32,7 +31,7 @@ const AUTHOR_PICKER = gql` ${FullDesc.fragments.user} ` -export const AuthorPicker = ({ +const AuthorPicker = ({ viewer, title, titleIs, @@ -47,58 +46,45 @@ export const AuthorPicker = ({ 'small-size-header': titleIs === 'span' }) + const { loading, data, error, refetch } = useQuery( + AUTHOR_PICKER, + { + notifyOnNetworkStatusChange: true + } + ) + const edges = + (data && data.viewer && data.viewer.recommendation.authors.edges) || [] + const followeeCount = viewer.followees.totalCount || 0 + return ( - - {({ - data, - loading, - error, - refetch - }: QueryResult & { data: AuthorPickerType }) => { - const edges = _get(data, 'viewer.recommendation.authors.edges', []) - const followeeCount = _get(viewer, 'followees.totalCount', 0) +
    + +
    + refetch()} /> + + + {numFormat(followeeCount)} + + +
    +
    - if (!edges || edges.length <= 0) { - return null - } + {loading && } - return ( - <> -
    - -
    - refetch()} /> - - - - {numFormat(followeeCount)} - - - -
    -
    + {error && } - {loading && } + {!loading && ( +
      + {edges.map(({ node, cursor }) => ( +
    • + +
    • + ))} +
    + )} - {!loading && ( -
      - {edges.map(({ node, cursor }: { node: any; cursor: any }) => ( -
    • - -
    • - ))} -
    - )} -
    - - - ) - }} - + +
    ) } @@ -111,3 +97,5 @@ AuthorPicker.fragments = { } ` } + +export default AuthorPicker diff --git a/src/components/Follow/index.tsx b/src/components/Follow/index.tsx deleted file mode 100644 index 88ac24be70..0000000000 --- a/src/components/Follow/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './AuthorPicker' diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index 754a3b5c86..9b7202ead7 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -21,6 +21,7 @@ const BaseLink = ({ {text} + ) @@ -31,11 +32,6 @@ export const Footer = () => { return (
    - {/* */} { lang })} /> + { lang })} /> + { lang })} /> + { lang })} /> - {/* */} +

    © {year} Matters

    +
    ) diff --git a/src/components/Form/Button/SendCode/index.tsx b/src/components/Form/Button/SendCode/index.tsx index 0f3ddcc84a..cf35a03abb 100644 --- a/src/components/Form/Button/SendCode/index.tsx +++ b/src/components/Form/Button/SendCode/index.tsx @@ -1,14 +1,15 @@ import gql from 'graphql-tag' -import { FC, useState } from 'react' +import { useState } from 'react' import { Button } from '~/components/Button' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' import { useCountdown } from '~/components/Hook' import { Translate } from '~/components/Language' import { ADD_TOAST, TEXT } from '~/common/enums' import { translate } from '~/common/utils' +import { SendVerificationCode } from './__generated__/SendVerificationCode' import styles from './styles.css' /** @@ -41,28 +42,28 @@ export const SEND_CODE = gql` } ` -const SendCodeButton: FC = ({ email, lang, type }) => { +const SendCodeButton: React.FC = ({ email, lang, type }) => { + const [send] = useMutation(SEND_CODE) const [sent, setSent] = useState(false) const { countdown, setCountdown, formattedTimeLeft } = useCountdown({ timeLeft: 0 }) - const sendCode = (params: any) => { - const { event, send } = params + const sendCode = (event: any) => { event.stopPropagation() - if (!send || !params.email || countdown.timeLeft !== 0) { + if (!send || !email || countdown.timeLeft !== 0) { return } send({ - variables: { input: { email: params.email, type } } + variables: { input: { email, type } } }) - .then((result: any) => { + .then(result => { setCountdown({ timeLeft: 1000 * 60 }) setSent(true) }) - .catch((error: any) => { + .catch(error => { const errorCode = getErrorCodes(error)[0] const errorMessage = ( = ({ email, lang, type }) => { } return ( - <> - - {(send: any) => ( - - )} - + ) } diff --git a/src/components/Form/CheckBox/index.tsx b/src/components/Form/CheckBox/index.tsx index c7631b1659..1604b4be36 100644 --- a/src/components/Form/CheckBox/index.tsx +++ b/src/components/Form/CheckBox/index.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames' -import { FC } from 'react' import { Icon } from '~/components/Icon' @@ -17,13 +16,13 @@ interface Props { values: any errors: any handleBlur?: () => {} - handleChange: () => {} - setFieldValue: (field: string, value: any, validate?: boolean) => {} + handleChange(e: React.ChangeEvent): void + setFieldValue(field: string, value: any): void [key: string]: any } -const CheckBox: FC = ({ +const CheckBox: React.FC = ({ children, className = [], field, @@ -63,6 +62,7 @@ const CheckBox: FC = ({
    {children}
    + ) diff --git a/src/components/Form/CommentForm/index.tsx b/src/components/Form/CommentForm/index.tsx index d229d46111..0b316bc748 100644 --- a/src/components/Form/CommentForm/index.tsx +++ b/src/components/Form/CommentForm/index.tsx @@ -1,14 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import dynamic from 'next/dynamic' import { useContext, useState } from 'react' +import { useQuery } from 'react-apollo' import { Button } from '~/components/Button' -import { Mutation, Query } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import COMMENT_COMMENTS from '~/components/GQL/queries/commentComments' import { Icon } from '~/components/Icon' import IconSpinner from '~/components/Icon/Spinner' import { Translate } from '~/components/Language' +import { ModalSwitch } from '~/components/ModalManager' import { Spinner } from '~/components/Spinner' import { ViewerContext } from '~/components/Viewer' @@ -16,6 +17,8 @@ import { ADD_TOAST } from '~/common/enums' import { dom, trimLineBreaks } from '~/common/utils' import ICON_POST from '~/static/icons/post.svg?sprite' +import { CommentDraft } from './__generated__/CommentDraft' +import { PutComment } from './__generated__/PutComment' import styles from './styles.css' const CommentEditor = dynamic( @@ -45,7 +48,6 @@ const COMMENT_DRAFT = gql` ` interface CommentFormProps { - defaultContent?: string | null articleId: string commentId?: string replyToId?: string @@ -53,20 +55,23 @@ interface CommentFormProps { submitCallback?: () => void refetch?: boolean extraButton?: React.ReactNode + blocked?: boolean defaultExpand?: boolean + defaultContent?: string | null } // TODO: remove refetchQueries, use refetch in submitCallback instead const CommentForm = ({ - defaultContent, commentId, parentId, replyToId, articleId, submitCallback, refetch, + blocked, extraButton, - defaultExpand + defaultExpand, + defaultContent }: CommentFormProps) => { const commentDraftId = `${articleId}:${commentId || '0'}:${parentId || '0'}:${replyToId || '0'}` @@ -81,136 +86,147 @@ const CommentForm = ({ ] : [] - return ( - - {({ data: commentDraftData, client }: any) => { - const draftContent = _get(commentDraftData, 'commentDraft.content', '') - - return ( - - {(putComment: any) => { - const [isSubmitting, setSubmitting] = useState(false) - const [expand, setExpand] = useState(defaultExpand || false) - const [content, setContent] = useState( - draftContent || defaultContent || '' - ) - const viewer = useContext(ViewerContext) - const isValid = !!trimLineBreaks(content) - - const handleSubmit = ( - event: React.FormEvent - ) => { - const mentions = dom.getAttributes('data-id', content) - const input = { - id: commentId, - comment: { - content: trimLineBreaks(content), - replyTo: replyToId, - articleId, - parentId, - mentions - } - } - - event.preventDefault() - setSubmitting(true) - - putComment({ variables: { input } }) - .then(({ data }: any) => { - if (submitCallback) { - submitCallback() - } - setContent('') - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'green', - content: ( - - ) - } - }) - ) - }) - .catch((result: any) => { - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'red', - content: ( - - ) - } - }) - ) - }) - .finally(() => { - setSubmitting(false) - }) - } - - return ( -
    setExpand(true)} - onBlur={() => { - client.writeData({ - id: `CommentDraft:${commentDraftId}`, - data: { - content - } - }) - }} - > - setContent(value)} - /> -
    - {extraButton && extraButton} - -
    - - - + const { data, client } = useQuery(COMMENT_DRAFT, { + variables: { + id: commentDraftId + } + }) + const [putComment] = useMutation(PUT_COMMENT, { + refetchQueries + }) + const draftContent = (data && data.commentDraft.content) || '' + + const [isSubmitting, setSubmitting] = useState(false) + const [expand, setExpand] = useState(defaultExpand || false) + const [content, setContent] = useState(draftContent || defaultContent || '') + const viewer = useContext(ViewerContext) + const isValid = !!trimLineBreaks(content) + const handleSubmit = (event: React.FormEvent) => { + const mentions = dom.getAttributes('data-id', content) + const input = { + id: commentId, + comment: { + content: trimLineBreaks(content), + replyTo: replyToId, + articleId, + parentId, + mentions + } + } + + event.preventDefault() + setSubmitting(true) + + putComment({ variables: { input } }) + .then(() => { + if (submitCallback) { + submitCallback() + } + setContent('') + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'green', + content: + } + }) + ) + }) + .catch((result: any) => { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: ( + ) - }} -
    + } + }) ) + }) + .finally(() => { + setSubmitting(false) + }) + } + + return ( +
    setExpand(true)} + onBlur={() => { + client.writeData({ + id: `CommentDraft:${commentDraftId}`, + data: { + content + } + }) }} - + > + setContent(value)} + /> +
    + {extraButton && extraButton} + +
    + + + ) } -export default CommentForm +const CommentFormWrap = (props: CommentFormProps) => { + const viewer = useContext(ViewerContext) + + if (viewer.shouldSetupLikerID) { + return ( + + {(open: any) => ( + + )} + + ) + } + + if (props.blocked) { + return ( +
    + + + +
    + ) + } + + return +} + +export default CommentFormWrap diff --git a/src/components/Form/CommentForm/styles.css b/src/components/Form/CommentForm/styles.css index 49901e871b..47ccb77285 100644 --- a/src/components/Form/CommentForm/styles.css +++ b/src/components/Form/CommentForm/styles.css @@ -13,3 +13,14 @@ display: block; } } + +.blocked { + display: block; + width: 100%; + padding: var(--spacing-x-tight); + + color: var(--color-grey-dark); + font-size: var(--font-size-sm); + text-align: center; + background: var(--color-grey-lighter); +} diff --git a/src/components/Form/EmailChangeForm/Confirm/index.tsx b/src/components/Form/EmailChangeForm/Confirm/index.tsx index 09e67434aa..b53ac3ad36 100644 --- a/src/components/Form/EmailChangeForm/Confirm/index.tsx +++ b/src/components/Form/EmailChangeForm/Confirm/index.tsx @@ -1,12 +1,12 @@ -import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' import SendCodeButton from '~/components/Form/Button/SendCode' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' +import { ConfirmVerificationCode } from '~/components/GQL/mutations/__generated__/ConfirmVerificationCode' import { CONFIRM_CODE } from '~/components/GQL/mutations/verificationCode' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -14,14 +14,19 @@ import { Modal } from '~/components/Modal' import { TEXT } from '~/common/enums' import { isValidEmail, translate } from '~/common/utils' +import { ChangeEmail } from './__generated__/ChangeEmail' import styles from './styles.css' -interface Props { +interface FormProps { oldData: { email: string; codeId: string } - extraClass?: string[] submitCallback: () => void } +interface FormValues { + email: string + code: string +} + const CHANGE_EMAIL = gql` mutation ChangeEmail($input: ChangeEmailInput!) { changeEmail(input: $input) { @@ -33,15 +38,85 @@ const CHANGE_EMAIL = gql` } ` -export const EmailChangeConfirmForm: FC = ({ - oldData, - extraClass = [], - submitCallback -}) => { +const InnerForm = ({ + values, + errors, + touched, + isSubmitting, + handleBlur, + handleChange, + handleSubmit, + setFieldError +}: FormikProps) => { + const { lang } = useContext(LanguageContext) + const emailPlaceholder = translate({ + zh_hant: TEXT.zh_hant.enterNewEmail, + zh_hans: TEXT.zh_hans.enterNewEmail, + lang + }) + const codePlaceholder = translate({ + zh_hant: TEXT.zh_hant.enterVerificationCode, + zh_hans: TEXT.zh_hans.enterVerificationCode, + lang + }) + + return ( +
    + + + + } + values={values} + errors={errors} + touched={touched} + handleBlur={handleBlur} + handleChange={handleChange} + /> + +
    + + + +
    + + +
    + ) +} + +export const EmailChangeConfirmForm: React.FC = formProps => { + const [confirmCode] = useMutation(CONFIRM_CODE) + const [changeEmail] = useMutation(CHANGE_EMAIL) const { lang } = useContext(LanguageContext) + const { oldData, submitCallback } = formProps + + const validateEmail = (value: string) => { + let result - const validateEmail = (value: string, language: string) => { - let result: any if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -53,111 +128,36 @@ export const EmailChangeConfirmForm: FC = ({ zh_hans: TEXT.zh_hans.invalidEmail } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateCode = (value: string, language: string) => { - let result: any + const validateCode = (value: string) => { + let result + if (!value) { result = { zh_hant: TEXT.zh_hant.required, zh_hans: TEXT.zh_hans.required } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ - values, - errors, - touched, - isSubmitting, - handleBlur, - handleChange, - handleSubmit, - setFieldError - }: { - [key: string]: any - }) => { - const formClass = classNames('form', ...extraClass) - - const emailPlaceholder = translate({ - zh_hant: TEXT.zh_hant.enterNewEmail, - zh_hans: TEXT.zh_hans.enterNewEmail, - lang - }) - - const codePlaceholder = translate({ - zh_hant: TEXT.zh_hant.enterVerificationCode, - zh_hans: TEXT.zh_hans.enterVerificationCode, - lang - }) - - return ( - <> -
    - - - - } - values={values} - errors={errors} - touched={touched} - handleBlur={handleBlur} - handleChange={handleChange} - /> - -
    - - - -
    -
    - - - ) - } - - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ email: '', code: '' }), validate: ({ email, code }) => { - const isInvalidEmail = validateEmail(email, lang) - const isInvalidCode = validateCode(code, lang) + const isInvalidEmail = validateEmail(email) + const isInvalidCode = validateCode(code) const errors = { ...(isInvalidEmail ? { email: isInvalidEmail } : {}), ...(isInvalidCode ? { code: isInvalidCode } : {}) @@ -165,62 +165,43 @@ export const EmailChangeConfirmForm: FC = ({ return errors }, - handleSubmit: (values, { props, setFieldError, setSubmitting }: any) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { email, code } = values - const { preSubmitAction, submitAction } = props - if (!preSubmitAction || !submitAction) { - return - } - preSubmitAction({ - variables: { input: { email, type: 'email_reset_confirm', code } } - }) - .then(({ data }: any) => { - const { confirmVerificationCode } = data - const params = { - variables: { - input: { - oldEmail: oldData.email, - oldEmailCodeId: oldData.codeId, - newEmail: email, - newEmailCodeId: confirmVerificationCode - } - } - } - return submitAction(params) + try { + const { data } = await confirmCode({ + variables: { input: { email, type: 'email_reset_confirm', code } } }) - .then((result: any) => { - if (submitCallback) { - submitCallback() + const confirmVerificationCode = data && data.confirmVerificationCode + const params = { + variables: { + input: { + oldEmail: oldData.email, + oldEmailCodeId: oldData.codeId, + newEmail: email, + newEmailCodeId: confirmVerificationCode + } } + } + + await changeEmail(params) + + if (submitCallback) { + submitCallback() + } + } catch (error) { + const errorCode = getErrorCodes(error)[0] + const errorMessage = translate({ + zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, + zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, + lang }) - .catch((error: any) => { - const errorCode = getErrorCodes(error)[0] - const errorMessage = translate({ - zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, - zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, - lang - }) - setFieldError('code', errorMessage) - }) - .finally(() => { - setSubmitting(false) - }) + setFieldError('code', errorMessage) + } + + setSubmitting(false) } - })(BaseForm) + })(InnerForm) - return ( - <> - - {(confirm: any) => ( - - {(update: any) => ( - - )} - - )} - - - - ) + return } diff --git a/src/components/Form/EmailChangeForm/Confirm/styles.css b/src/components/Form/EmailChangeForm/Confirm/styles.css index 9b1e84b5e2..b8ea4f7c96 100644 --- a/src/components/Form/EmailChangeForm/Confirm/styles.css +++ b/src/components/Form/EmailChangeForm/Confirm/styles.css @@ -1,3 +1,3 @@ -.form { +form { width: 100%; } diff --git a/src/components/Form/EmailChangeForm/Request/index.tsx b/src/components/Form/EmailChangeForm/Request/index.tsx index c8c13ac5e6..33ea49932d 100644 --- a/src/components/Form/EmailChangeForm/Request/index.tsx +++ b/src/components/Form/EmailChangeForm/Request/index.tsx @@ -1,11 +1,11 @@ -import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' import SendCodeButton from '~/components/Form/Button/SendCode' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' +import { ConfirmVerificationCode } from '~/components/GQL/mutations/__generated__/ConfirmVerificationCode' import { CONFIRM_CODE } from '~/components/GQL/mutations/verificationCode' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -15,21 +15,92 @@ import { isValidEmail, translate } from '~/common/utils' import styles from './styles.css' -interface Props { +interface FormProps { defaultEmail: string - extraClass?: string[] submitCallback?: (params: any) => void } -export const EmailChangeRequestForm: FC = ({ - defaultEmail = '', - extraClass = [], - submitCallback -}) => { +interface FormValues { + email: string + code: string +} + +const InnerForm = ({ + values, + errors, + touched, + isSubmitting, + handleBlur, + handleChange, + handleSubmit +}: FormikProps) => { + const { lang } = useContext(LanguageContext) + const codePlaceholder = translate({ + zh_hant: TEXT.zh_hant.enterVerificationCode, + zh_hans: TEXT.zh_hans.enterVerificationCode, + lang + }) + + return ( +
    + + + + } + values={values} + errors={errors} + touched={touched} + handleBlur={handleBlur} + handleChange={handleChange} + /> + + +
    + + + +
    + + +
    + ) +} + +export const EmailChangeRequestForm: React.FC = formProps => { + const [confirmCode] = useMutation(CONFIRM_CODE) const { lang } = useContext(LanguageContext) + const { defaultEmail = '', submitCallback } = formProps + + const validateEmail = (value: string) => { + let result - const validateEmail = (value: string, language: string) => { - let result: any if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -41,105 +112,35 @@ export const EmailChangeRequestForm: FC = ({ zh_hans: TEXT.zh_hans.invalidEmail } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } + const validateCode = (value: string) => { + let result - const validateCode = (value: string, language: string) => { - let result: any if (!value) { result = { zh_hant: TEXT.zh_hant.required, zh_hans: TEXT.zh_hans.required } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ - values, - errors, - touched, - isSubmitting, - handleBlur, - handleChange, - handleSubmit - }: { - [key: string]: any - }) => { - const formClass = classNames('form', ...extraClass) - - const codePlaceholder = translate({ - zh_hant: TEXT.zh_hant.enterVerificationCode, - zh_hans: TEXT.zh_hans.enterVerificationCode, - lang - }) - - return ( - <> -
    - - - - } - values={values} - errors={errors} - touched={touched} - handleBlur={handleBlur} - handleChange={handleChange} - /> - - -
    - - - -
    -
    - - - ) - } - - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ email: defaultEmail, code: '' }), validate: ({ email, code }) => { - const isInvalidEmail = validateEmail(email, lang) - const isInvalidCode = validateCode(code, lang) + const isInvalidEmail = validateEmail(email) + const isInvalidCode = validateCode(code) const errors = { ...(isInvalidEmail ? { email: isInvalidEmail } : {}), ...(isInvalidCode ? { code: isInvalidCode } : {}) @@ -147,43 +148,31 @@ export const EmailChangeRequestForm: FC = ({ return errors }, - handleSubmit: (values, { props, setFieldError, setSubmitting }: any) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { email, code } = values - const { submitAction } = props - if (!submitAction) { - return - } - submitAction({ - variables: { input: { email, type: 'email_reset', code } } - }) - .then(({ data }: any) => { - const { confirmVerificationCode } = data - if (submitCallback && confirmVerificationCode) { - submitCallback({ codeId: confirmVerificationCode }) - } + try { + const { data } = await confirmCode({ + variables: { input: { email, type: 'email_reset', code } } }) - .catch((error: any) => { - const errorCode = getErrorCodes(error)[0] - const errorMessage = translate({ - zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, - zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, - lang - }) - setFieldError('code', errorMessage) - }) - .finally(() => { - setSubmitting(false) + const confirmVerificationCode = data && data.confirmVerificationCode + + if (submitCallback && confirmVerificationCode) { + submitCallback({ codeId: confirmVerificationCode }) + } + } catch (error) { + const errorCode = getErrorCodes(error)[0] + const errorMessage = translate({ + zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, + zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, + lang }) + setFieldError('code', errorMessage) + } + + setSubmitting(false) } - })(BaseForm) + })(InnerForm) - return ( - <> - - {(confirmCode: any) => } - - - - ) + return } diff --git a/src/components/Form/EmailChangeForm/Request/styles.css b/src/components/Form/EmailChangeForm/Request/styles.css index 9b1e84b5e2..b8ea4f7c96 100644 --- a/src/components/Form/EmailChangeForm/Request/styles.css +++ b/src/components/Form/EmailChangeForm/Request/styles.css @@ -1,3 +1,3 @@ -.form { +form { width: 100%; } diff --git a/src/components/Form/Input/index.tsx b/src/components/Form/Input/index.tsx index 8a8c4c5adf..4f78c4f63b 100644 --- a/src/components/Form/Input/index.tsx +++ b/src/components/Form/Input/index.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames' -import { FC } from 'react' import styles from './styles.css' @@ -45,7 +44,7 @@ interface Props { [key: string]: any } -const Input: FC = ({ +const Input: React.FC = ({ className = [], type, field, @@ -94,6 +93,7 @@ const Input: FC = ({ {(!error || !isTouched) && hint &&
    {hint}
    } + ) diff --git a/src/components/Form/LoginForm/index.tsx b/src/components/Form/LoginForm/index.tsx index feec63ea14..dadd74b451 100644 --- a/src/components/Form/LoginForm/index.tsx +++ b/src/components/Form/LoginForm/index.tsx @@ -1,12 +1,12 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Button } from '~/components/Button' import { Form } from '~/components/Form' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' import { ModalSwitch } from '~/components/ModalManager' @@ -27,6 +27,7 @@ import { translate } from '~/common/utils' +import { UserLogin } from './__generated__/UserLogin' import styles from './styles.css' /** @@ -43,13 +44,18 @@ import styles from './styles.css' * ``` * */ -interface Props { +interface FormProps { extraClass?: string[] purpose: 'modal' | 'page' submitCallback?: () => void scrollLock?: boolean } +interface FormValues { + email: string + password: '' +} + export const USER_LOGIN = gql` mutation UserLogin($input: UserLoginInput!) { userLogin(input: $input) { @@ -73,6 +79,7 @@ const PasswordResetRedirectButton = () => ( /> ? + ) @@ -117,18 +124,15 @@ const SignUpRedirection = () => ( ) -const LoginForm: FC = ({ - extraClass = [], - purpose, - submitCallback, - scrollLock -}) => { +const LoginForm: React.FC = formProps => { + const [login] = useMutation(USER_LOGIN) const { lang } = useContext(LanguageContext) + const { extraClass = [], purpose, submitCallback, scrollLock } = formProps const isInModal = purpose === 'modal' const isInPage = purpose === 'page' - const validateEmail = (value: string, language: string) => { - let result: any + const validateEmail = (value: string) => { + let result if (!value) { result = { @@ -142,12 +146,11 @@ const LoginForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - - const validatePassword = (value: string, language: string) => { - let result: any + const validatePassword = (value: string) => { + let result if (!value) { result = { @@ -155,12 +158,13 @@ const LoginForm: FC = ({ zh_hans: TEXT.zh_hans.required } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -168,85 +172,76 @@ const LoginForm: FC = ({ handleBlur, handleChange, handleSubmit - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) - const emailPlaceholder = translate({ zh_hant: TEXT.zh_hant.enterEmail, zh_hans: TEXT.zh_hans.enterEmail, lang }) - const passwordPlaceholder = translate({ zh_hant: TEXT.zh_hant.enterPassword, zh_hans: TEXT.zh_hans.enterPassword, lang }) - const loginText = translate({ - zh_hant: TEXT.zh_hant.login, - zh_hans: TEXT.zh_hans.login, - lang - }) - return ( - <> -
    - - - + + + + {isInModal && } + {isInPage && } + + +
    + {isInModal && } + {isInPage && } + + + + +
    - {isInModal && } - {isInPage && } -
    - -
    - {isInModal && } - {isInPage && } - - - {loginText} - -
    -
    - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ email: '', password: '' }), validate: ({ email, password }) => { - const isInvalidEmail = validateEmail(email, lang) - const isInvalidPassword = validatePassword(password, lang) + const isInvalidEmail = validateEmail(email) + const isInvalidPassword = validatePassword(password) const errors = { ...(isInvalidEmail ? { email: isInvalidEmail } : {}), ...(isInvalidPassword ? { password: isInvalidPassword } : {}) @@ -254,88 +249,77 @@ const LoginForm: FC = ({ return errors }, - handleSubmit: (values, { props, setErrors, setSubmitting }: any) => { + handleSubmit: async (values, { setErrors, setSubmitting }) => { const { email, password } = values - const { submitAction } = props - if (!submitAction) { - return - } - submitAction({ variables: { input: { email, password } } }) - .then(async (result: any) => { - if (submitCallback) { - submitCallback() - } - window.dispatchEvent( - new CustomEvent(ADD_TOAST, { - detail: { - color: 'green', - content: ( - - ) - } - }) - ) - analytics.identifyUser() - analytics.trackEvent(ANALYTICS_EVENTS.LOG_IN) - // await clearPersistCache() - redirectToTarget({ - fallback: !!isInPage ? 'homepage' : 'current' + try { + await login({ variables: { input: { email, password } } }) + + if (submitCallback) { + submitCallback() + } + + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'green', + content: ( + + ) + } }) + ) + analytics.identifyUser() + analytics.trackEvent(ANALYTICS_EVENTS.LOG_IN) + + // await clearPersistCache() + + redirectToTarget({ + fallback: !!isInPage ? 'homepage' : 'current' }) - .catch((error: any) => { - const errorCodes = getErrorCodes(error) - - if (errorCodes.indexOf(ERROR_CODES.USER_EMAIL_NOT_FOUND) >= 0) { - setErrors({ - email: translate({ - zh_hant: TEXT.zh_hant.error.USER_EMAIL_NOT_FOUND, - zh_hans: TEXT.zh_hans.error.USER_EMAIL_NOT_FOUND, - lang - }) + } catch (error) { + const errorCodes = getErrorCodes(error) + + if (errorCodes.indexOf(ERROR_CODES.USER_EMAIL_NOT_FOUND) >= 0) { + setErrors({ + email: translate({ + zh_hant: TEXT.zh_hant.error.USER_EMAIL_NOT_FOUND, + zh_hans: TEXT.zh_hans.error.USER_EMAIL_NOT_FOUND, + lang }) - } else if ( - errorCodes.indexOf(ERROR_CODES.USER_PASSWORD_INVALID) >= 0 - ) { - setErrors({ - password: translate({ - zh_hant: TEXT.zh_hant.error.USER_PASSWORD_INVALID, - zh_hans: TEXT.zh_hans.error.USER_PASSWORD_INVALID, - lang - }) + }) + } else if (errorCodes.indexOf(ERROR_CODES.USER_PASSWORD_INVALID) >= 0) { + setErrors({ + password: translate({ + zh_hant: TEXT.zh_hant.error.USER_PASSWORD_INVALID, + zh_hans: TEXT.zh_hans.error.USER_PASSWORD_INVALID, + lang }) - } else { - setErrors({ - email: translate({ - zh_hant: TEXT.zh_hant.error.UNKNOWN_ERROR, - zh_hans: TEXT.zh_hans.error.UNKNOWN_ERROR, - lang - }) + }) + } else { + setErrors({ + email: translate({ + zh_hant: TEXT.zh_hant.error.UNKNOWN_ERROR, + zh_hans: TEXT.zh_hans.error.UNKNOWN_ERROR, + lang }) - } - - analytics.trackEvent(ANALYTICS_EVENTS.LOG_IN_FAILED, { - email, - error }) + } + + analytics.trackEvent(ANALYTICS_EVENTS.LOG_IN_FAILED, { + email, + error }) - .finally(() => { - setSubmitting(false) - }) + } + + setSubmitting(false) } - })(BaseForm) - - return ( - <> - - {(login: any) => } - - - - ) + })(InnerForm) + + return } export default LoginForm diff --git a/src/components/Form/PasswordChangeForm/Confirm/index.tsx b/src/components/Form/PasswordChangeForm/Confirm/index.tsx index 58d2690c67..063ba49fa9 100644 --- a/src/components/Form/PasswordChangeForm/Confirm/index.tsx +++ b/src/components/Form/PasswordChangeForm/Confirm/index.tsx @@ -1,20 +1,21 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' import { TEXT } from '~/common/enums' import { isValidPassword, translate } from '~/common/utils' +import { ResetPassword } from './__generated__/ResetPassword' import styles from './styles.css' -interface Props { +interface FormProps { codeId: string extraClass?: string[] container: 'modal' | 'page' @@ -23,24 +24,30 @@ interface Props { scrollLock?: boolean } +interface FormValues { + password: string + comparedPassword: string +} + export const RESET_PASSWORD = gql` mutation ResetPassword($input: ResetPasswordInput!) { resetPassword(input: $input) } ` -export const PasswordChangeConfirmForm: FC = ({ - codeId, - extraClass = [], - container, - backPreviousStep, - submitCallback, - scrollLock -}) => { +export const PasswordChangeConfirmForm: React.FC = formProps => { + const [reset] = useMutation(RESET_PASSWORD) const { lang } = useContext(LanguageContext) + const { + codeId, + extraClass = [], + backPreviousStep, + submitCallback, + scrollLock + } = formProps - const validatePassword = (value: string, language: string) => { - let result: any + const validatePassword = (value: string) => { + let result if (!value) { result = { @@ -53,17 +60,14 @@ export const PasswordChangeConfirmForm: FC = ({ zh_hans: TEXT.zh_hans.passwordHint } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateComparedPassword = ( - value: string, - comparedValue: string, - language: string - ) => { - let result: any + const validateComparedPassword = (value: string, comparedValue: string) => { + let result if (!comparedValue) { result = { @@ -76,12 +80,13 @@ export const PasswordChangeConfirmForm: FC = ({ zh_hans: TEXT.zh_hans.passwordNotMatch } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -89,23 +94,18 @@ export const PasswordChangeConfirmForm: FC = ({ handleBlur, handleChange, handleSubmit - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) - const passwordPlaceholder = translate({ zh_hant: TEXT.zh_hant.enterPassword, zh_hans: TEXT.zh_hans.enterPassword, lang }) - const passwordHint = translate({ zh_hant: TEXT.zh_hant.passwordHint, zh_hans: TEXT.zh_hans.passwordHint, lang }) - const comparedPlaceholder = translate({ zh_hant: TEXT.zh_hant.enterPasswordAgain, zh_hans: TEXT.zh_hans.enterPasswordAgain, @@ -113,68 +113,66 @@ export const PasswordChangeConfirmForm: FC = ({ }) return ( - <> -
    - - + + + + + +
    + + - + + - - -
    - - - - - - -
    - +
    +
    + - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ password: '', comparedPassword: '' }), validate: ({ password, comparedPassword }) => { - const isInvalidPassword = validatePassword(password, lang) + const isInvalidPassword = validatePassword(password) const isInvalidComparedPassword = validateComparedPassword( password, - comparedPassword, - lang + comparedPassword ) const errors = { ...(isInvalidPassword ? { password: isInvalidPassword } : {}), @@ -185,40 +183,31 @@ export const PasswordChangeConfirmForm: FC = ({ return errors }, - handleSubmit: (values, { props, setFieldError, setSubmitting }: any) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { password } = values - const { submitAction } = props - if (!submitAction) { - return - } - submitAction({ variables: { input: { password, codeId } } }) - .then(({ data }: any) => { - const { resetPassword } = data - if (submitCallback && resetPassword) { - submitCallback() - } - }) - .catch((error: any) => { - const errorCode = getErrorCodes(error)[0] - const errorMessage = translate({ - zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, - zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, - lang - }) - setFieldError('password', errorMessage) + + try { + const { data } = await reset({ + variables: { input: { password, codeId } } }) - .finally(() => { - setSubmitting(false) + const resetPassword = data && data.resetPassword + + if (submitCallback && resetPassword) { + submitCallback() + } + } catch (error) { + const errorCode = getErrorCodes(error)[0] + const errorMessage = translate({ + zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, + zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, + lang }) + setFieldError('password', errorMessage) + } + + setSubmitting(false) } - })(BaseForm) - - return ( - <> - - {(reset: any) => } - - - - ) + })(InnerForm) + + return } diff --git a/src/components/Form/PasswordChangeForm/Request/index.tsx b/src/components/Form/PasswordChangeForm/Request/index.tsx index 7a1e2e10d9..8854518e26 100644 --- a/src/components/Form/PasswordChangeForm/Request/index.tsx +++ b/src/components/Form/PasswordChangeForm/Request/index.tsx @@ -1,11 +1,12 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' import SendCodeButton from '~/components/Form/Button/SendCode' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' +import { ConfirmVerificationCode } from '~/components/GQL/mutations/__generated__/ConfirmVerificationCode' import { CONFIRM_CODE } from '~/components/GQL/mutations/verificationCode' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -15,7 +16,7 @@ import { isValidEmail, translate } from '~/common/utils' import styles from './styles.css' -interface Props { +interface FormProps { defaultEmail: string extraClass?: string[] container: 'modal' | 'page' @@ -24,18 +25,24 @@ interface Props { scrollLock?: boolean } -export const PasswordChangeRequestForm: FC = ({ - defaultEmail = '', - extraClass = [], - container, - purpose, - submitCallback, - scrollLock -}) => { - const { lang } = useContext(LanguageContext) +interface FormValues { + email: string + code: string +} - const validateEmail = (value: string, language: string) => { - let result: any +export const PasswordChangeRequestForm: React.FC = formProps => { + const [confirmCode] = useMutation(CONFIRM_CODE) + const { lang } = useContext(LanguageContext) + const { + defaultEmail = '', + extraClass = [], + purpose, + submitCallback, + scrollLock + } = formProps + + const validateEmail = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -48,12 +55,12 @@ export const PasswordChangeRequestForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateCode = (value: string, language: string) => { - let result: any + const validateCode = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -61,11 +68,11 @@ export const PasswordChangeRequestForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -74,9 +81,7 @@ export const PasswordChangeRequestForm: FC = ({ handleChange, handleSubmit, setFieldError - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) const emailPlaceholder = @@ -99,68 +104,67 @@ export const PasswordChangeRequestForm: FC = ({ }) return ( - <> -
    - - - - } - values={values} - errors={errors} - touched={touched} - handleBlur={handleBlur} - handleChange={handleChange} - /> - - -
    - - + + + - -
    -
    + } + values={values} + errors={errors} + touched={touched} + handleBlur={handleBlur} + handleChange={handleChange} + /> +
    + +
    + + + +
    + - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ email: defaultEmail, code: '' }), validate: ({ email, code }) => { - const isInvalidEmail = validateEmail(email, lang) - const isInvalidCode = validateCode(code, lang) + const isInvalidEmail = validateEmail(email) + const isInvalidCode = validateCode(code) const errors = { ...(isInvalidEmail ? { email: isInvalidEmail } : {}), ...(isInvalidCode ? { code: isInvalidCode } : {}) @@ -168,43 +172,31 @@ export const PasswordChangeRequestForm: FC = ({ return errors }, - handleSubmit: (values, { props, setFieldError, setSubmitting }: any) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { email, code } = values - const { submitAction } = props - if (!submitAction) { - return - } - submitAction({ - variables: { input: { email, type: 'password_reset', code } } - }) - .then(({ data }: any) => { - const { confirmVerificationCode } = data - if (submitCallback && confirmVerificationCode) { - submitCallback({ email, codeId: confirmVerificationCode }) - } - }) - .catch((error: any) => { - const errorCode = getErrorCodes(error)[0] - const errorMessage = translate({ - zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, - zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, - lang - }) - setFieldError('code', errorMessage) + try { + const { data } = await confirmCode({ + variables: { input: { email, type: 'password_reset', code } } }) - .finally(() => { - setSubmitting(false) + const confirmVerificationCode = data && data.confirmVerificationCode + + if (submitCallback && confirmVerificationCode) { + submitCallback({ email, codeId: confirmVerificationCode }) + } + } catch (error) { + const errorCode = getErrorCodes(error)[0] + const errorMessage = translate({ + zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, + zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, + lang }) + setFieldError('code', errorMessage) + } + + setSubmitting(false) } - })(BaseForm) - - return ( - <> - - {(confirmCode: any) => } - - - - ) + })(InnerForm) + + return } diff --git a/src/components/Form/SignUpComplete/index.tsx b/src/components/Form/SignUpComplete/index.tsx index a23c322f10..e2220198b4 100644 --- a/src/components/Form/SignUpComplete/index.tsx +++ b/src/components/Form/SignUpComplete/index.tsx @@ -65,6 +65,7 @@ const SignUpComplete = ({ + ) diff --git a/src/components/Form/SignUpForm/Follow/index.tsx b/src/components/Form/SignUpForm/Follow/index.tsx index db85430d5e..dfe3d60b53 100644 --- a/src/components/Form/SignUpForm/Follow/index.tsx +++ b/src/components/Form/SignUpForm/Follow/index.tsx @@ -1,17 +1,17 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' -import { FC, useContext } from 'react' -import { QueryResult } from 'react-apollo' +import { useContext } from 'react' +import { useQuery } from 'react-apollo' import { Button } from '~/components/Button' -import { AuthorPicker } from '~/components/Follow' -import { Query } from '~/components/GQL' +import AuthorPicker from '~/components/Follow/AuthorPicker' +import { QueryError } from '~/components/GQL' import { LanguageContext } from '~/components/Language' import { Spinner } from '~/components/Spinner' import { translate } from '~/common/utils' +import { SignUpMeFollow } from './__generated__/SignUpMeFollow' import styles from './styles.css' /** @@ -45,60 +45,64 @@ interface Props { submitCallback?: () => void } -export const SignUpFollowForm: FC = ({ +export const SignUpFollowForm: React.FC = ({ extraClass = [], purpose, submitCallback }) => { const { lang } = useContext(LanguageContext) + const { loading, data, error } = useQuery(ME_FOLLOW) const containerStyle = classNames( purpose === 'modal' ? 'modal-container' : 'page-container' ) - const titleText = translate({ zh_hant: '請至少選擇 5 位作者', zh_hans: '请至少选择 5 位作者', lang }) - const nextText = translate({ zh_hant: '下一步', zh_hans: '下一步', lang }) + if (loading) { + return + } + + if (error) { + return + } + + if (!data || !data.viewer) { + return null + } + + const followeeCount = data.viewer.followees.totalCount || 0 + return ( - - {({ data, loading, error }: QueryResult & { data: any }) => { - if (loading) { - return - } +
    + + +
    + +
    - const followeeCount = _get(data, 'viewer.followees.totalCount', 0) - return ( -
    - -
    - -
    - -
    - ) - }} - + +
    ) } diff --git a/src/components/Form/SignUpForm/Init/index.tsx b/src/components/Form/SignUpForm/Init/index.tsx index 7e07c46385..b07ada8ced 100644 --- a/src/components/Form/SignUpForm/Init/index.tsx +++ b/src/components/Form/SignUpForm/Init/index.tsx @@ -1,13 +1,14 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' import Link from 'next/link' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' import SendCodeButton from '~/components/Form/Button/SendCode' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' +import { ConfirmVerificationCode } from '~/components/GQL/mutations/__generated__/ConfirmVerificationCode' import { CONFIRM_CODE } from '~/components/GQL/mutations/verificationCode' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -23,6 +24,7 @@ import { translate } from '~/common/utils' +import { UserRegister } from './__generated__/UserRegister' import styles from './styles.css' /** @@ -40,7 +42,7 @@ import styles from './styles.css' * ``` * */ -interface Props { +interface FormProps { defaultEmail?: string extraClass?: string[] purpose: 'modal' | 'page' @@ -48,6 +50,14 @@ interface Props { scrollLock?: boolean } +interface FormValues { + email: string + code: string + userName: string + password: string + tos: boolean +} + const USER_REGISTER = gql` mutation UserRegister($input: UserRegisterInput!) { userRegister(input: $input) { @@ -76,19 +86,22 @@ const LoginRedirection = () => ( ) -export const SignUpInitForm: FC = ({ - defaultEmail = '', - extraClass = [], - purpose, - submitCallback, - scrollLock -}) => { +export const SignUpInitForm: React.FC = formProps => { + const [confirm] = useMutation(CONFIRM_CODE) + const [register] = useMutation(USER_REGISTER) const { lang } = useContext(LanguageContext) + const { + defaultEmail = '', + extraClass = [], + purpose, + submitCallback, + scrollLock + } = formProps const isInModal = purpose === 'modal' const isInPage = purpose === 'page' - const validateEmail = (value: string, language: string) => { - let result: any + const validateEmail = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -101,12 +114,11 @@ export const SignUpInitForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - - const validateCode = (value: string, language: string) => { - let result: any + const validateCode = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -114,12 +126,12 @@ export const SignUpInitForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } + const validateUserName = (value: string) => { + let result - const validateUserName = (value: string, language: string) => { - let result: any if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -131,13 +143,14 @@ export const SignUpInitForm: FC = ({ zh_hans: TEXT.zh_hans.userNameHint } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } + const validatePassword = (value: string) => { + let result - const validatePassword = (value: string, language: string) => { - let result: any if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -149,22 +162,24 @@ export const SignUpInitForm: FC = ({ zh_hans: TEXT.zh_hans.passwordHint } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } + const validateToS = (value: boolean) => { + let result - const validateToS = (value: boolean, language: string) => { - let result: any if (value === false) { result = { zh_hant: '請勾選', zh_hans: '请勾选' } } + if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -174,9 +189,7 @@ export const SignUpInitForm: FC = ({ handleSubmit, setFieldValue, setFieldError - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) const emailPlaceholder = translate({ @@ -216,112 +229,111 @@ export const SignUpInitForm: FC = ({ }) return ( - <> -
    - - - - } - values={values} - errors={errors} - touched={touched} - handleBlur={handleBlur} - handleChange={handleChange} - /> - - + + + + } + values={values} + errors={errors} + touched={touched} + handleBlur={handleBlur} + handleChange={handleChange} + /> + + +
    + -
    - - - {agreeText} - - - {' '} - {tosText} - - - - -
    - - -
    - {isInModal && } - {isInPage && } - - - - + + {agreeText} + + + {' '} + {tosText} + + + +
    - + + +
    + {isInModal && } + {isInPage && } + + + + +
    + - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ email: defaultEmail, code: '', @@ -331,11 +343,11 @@ export const SignUpInitForm: FC = ({ }), validate: ({ email, code, userName, password, tos }) => { - const isInvalidEmail = validateEmail(email, lang) - const isInvalidCodeId = validateCode(code, lang) - const isInvalidPassword = validatePassword(password, lang) - const isInvalidUserName = validateUserName(userName, lang) - const isInvalidToS = validateToS(tos, lang) + const isInvalidEmail = validateEmail(email) + const isInvalidCodeId = validateCode(code) + const isInvalidPassword = validatePassword(password) + const isInvalidUserName = validateUserName(userName) + const isInvalidToS = validateToS(tos) const errors: { [key: string]: any } = { ...(isInvalidEmail ? { email: isInvalidEmail } : {}), ...(isInvalidCodeId ? { code: isInvalidCodeId } : {}), @@ -346,24 +358,16 @@ export const SignUpInitForm: FC = ({ return errors }, - handleSubmit: async ( - values, - { props, setFieldError, setSubmitting }: any - ) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { email, code, userName, password } = values - const { preSubmitAction, submitAction } = props - if (!preSubmitAction || !submitAction) { - return - } try { - const { - data: { confirmVerificationCode: codeId } - } = await preSubmitAction({ + const { data } = await confirm({ variables: { input: { email, code, type: 'register' } } }) + const codeId = data && data.confirmVerificationCode - await submitAction({ + await register({ variables: { input: { email, codeId, userName, displayName: userName, password } } @@ -386,17 +390,7 @@ export const SignUpInitForm: FC = ({ analytics.identifyUser() analytics.trackEvent(ANALYTICS_EVENTS.SIGNUP_SUCCESS) } - })(BaseForm) - - return ( - - {(confirm: any) => ( - - {(register: any) => ( - - )} - - )} - - ) + })(InnerForm) + + return } diff --git a/src/components/Form/SignUpForm/Profile/index.tsx b/src/components/Form/SignUpForm/Profile/index.tsx index 1669fcf899..0535feb3ca 100644 --- a/src/components/Form/SignUpForm/Profile/index.tsx +++ b/src/components/Form/SignUpForm/Profile/index.tsx @@ -1,18 +1,19 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { SignUpAvatarUploader } from '~/components/FileUploader' import { Form } from '~/components/Form' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' import { TEXT } from '~/common/enums' import { isValidDisplayName, translate } from '~/common/utils' +import { UpdateUserInfoProfileInit } from './__generated__/UpdateUserInfoProfileInit' import styles from './styles.css' /** @@ -30,13 +31,19 @@ import styles from './styles.css' * */ -interface Props { +interface FormProps { extraClass?: string[] purpose: 'modal' | 'page' submitCallback?: () => void scrollLock?: boolean } +interface FormValues { + avatar: null | string + displayName: string + description: string +} + const UPDATE_USER_INFO = gql` mutation UpdateUserInfoProfileInit($input: UpdateUserInfoInput!) { updateUserInfo(input: $input) { @@ -53,24 +60,20 @@ const AvatarError = ({ field, errors, touched }: { [key: string]: any }) => { const error = errors[field] const isTouched = touched[field] return ( - <> -
    - {error && isTouched &&
    {error}
    } -
    +
    + {error && isTouched &&
    {error}
    } + - +
    ) } -export const SignUpProfileForm: FC = ({ - extraClass = [], - purpose, - submitCallback, - scrollLock -}) => { +export const SignUpProfileForm: React.FC = formProps => { + const [update] = useMutation(UPDATE_USER_INFO) const { lang } = useContext(LanguageContext) + const { extraClass = [], submitCallback, scrollLock } = formProps - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -80,9 +83,7 @@ export const SignUpProfileForm: FC = ({ handleSubmit, setFieldValue, setFieldError - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) const displayNamePlaceholder = translate({ @@ -90,7 +91,6 @@ export const SignUpProfileForm: FC = ({ zh_hans: '姓名', lang }) - const descriptionPlaceholder = translate({ zh_hant: '介紹你自己,獲得更多社區關注', zh_hans: '介绍你自己,获得更多社区关注', @@ -98,63 +98,62 @@ export const SignUpProfileForm: FC = ({ }) return ( - <> -
    - - - - - + + + + + + + +
    + + - + +
    -
    - - - -
    - - + ) } - const validateAvatar = (value: string | null, language: string) => { - let result: any + const validateAvatar = (value: string | null) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -162,12 +161,12 @@ export const SignUpProfileForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateDisplayName = (value: string, language: string) => { - let result: any + const validateDisplayName = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -180,12 +179,12 @@ export const SignUpProfileForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateDescription = (value: string, language: string) => { - let result: any + const validateDescription = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -193,11 +192,11 @@ export const SignUpProfileForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ avatar: null, displayName: '', @@ -205,9 +204,9 @@ export const SignUpProfileForm: FC = ({ }), validate: ({ avatar, displayName, description }) => { - const isValidAvatar = validateAvatar(avatar, lang) - const isInvalidDisplayName = validateDisplayName(displayName, lang) - const isValidDescription = validateDescription(description, lang) + const isValidAvatar = validateAvatar(avatar) + const isInvalidDisplayName = validateDisplayName(displayName) + const isValidDescription = validateDescription(description) const errors = { ...(isValidAvatar ? { avatar: isValidAvatar } : {}), ...(isInvalidDisplayName ? { displayName: isInvalidDisplayName } : {}), @@ -218,13 +217,9 @@ export const SignUpProfileForm: FC = ({ handleSubmit: async (values, { props, setSubmitting }: any) => { const { avatar, displayName, description } = values - const { submitAction } = props - if (!submitAction) { - return - } try { - await submitAction({ + await update({ variables: { input: { displayName, @@ -244,15 +239,7 @@ export const SignUpProfileForm: FC = ({ setSubmitting(false) } - })(BaseForm) - - return ( - <> - - {(update: any) => } - + })(InnerForm) - - - ) + return } diff --git a/src/components/Form/Textarea/index.tsx b/src/components/Form/Textarea/index.tsx index 4cdf110007..5cbb5b841b 100644 --- a/src/components/Form/Textarea/index.tsx +++ b/src/components/Form/Textarea/index.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames' -import { FC } from 'react' import styles from './styles.css' @@ -41,7 +40,7 @@ interface Props { [key: string]: any } -const Textarea: FC = ({ +const Textarea: React.FC = ({ className = [], field, placeholder, @@ -72,10 +71,12 @@ const Textarea: FC = ({ style={style} />
    +
    {error && isTouched &&
    {error}
    } {!error && hint &&
    {hint}
    }
    + ) diff --git a/src/components/Form/UserNameChangeForm/Confirm/index.tsx b/src/components/Form/UserNameChangeForm/Confirm/index.tsx index d84dbe3978..f2a226ef0f 100644 --- a/src/components/Form/UserNameChangeForm/Confirm/index.tsx +++ b/src/components/Form/UserNameChangeForm/Confirm/index.tsx @@ -1,24 +1,30 @@ import classNames from 'classnames' -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Form } from '~/components/Form' -import { getErrorCodes, Mutation } from '~/components/GQL' +import { getErrorCodes, useMutation } from '~/components/GQL' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' import { TEXT } from '~/common/enums' import { isValidUserName, translate } from '~/common/utils' +import { UpdateUserInfoUserName } from './__generated__/UpdateUserInfoUserName' import styles from './styles.css' -interface Props { +interface FormProps { extraClass?: string[] submitCallback: () => void } +interface FormValues { + userName: string + comparedUserName: string +} + const UPDATE_USER_INFO = gql` mutation UpdateUserInfoUserName($input: UpdateUserInfoInput!) { updateUserInfo(input: $input) { @@ -28,14 +34,13 @@ const UPDATE_USER_INFO = gql` } ` -export const UserNameChangeConfirmForm: FC = ({ - extraClass = [], - submitCallback -}) => { +export const UserNameChangeConfirmForm: React.FC = formProps => { + const [update] = useMutation(UPDATE_USER_INFO) const { lang } = useContext(LanguageContext) + const { extraClass = [], submitCallback } = formProps - const validateUserName = (value: string, language: string) => { - let result: any + const validateUserName = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -48,16 +53,11 @@ export const UserNameChangeConfirmForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - - const validateComparedUserName = ( - value: string, - comparedValue: string, - language: string - ) => { - let result: any + const validateComparedUserName = (value: string, comparedValue: string) => { + let result if (!comparedValue) { result = { zh_hant: TEXT.zh_hant.required, @@ -70,11 +70,11 @@ export const UserNameChangeConfirmForm: FC = ({ } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -82,23 +82,18 @@ export const UserNameChangeConfirmForm: FC = ({ handleBlur, handleChange, handleSubmit - }: { - [key: string]: any - }) => { + }: FormikProps) => { const formClass = classNames('form', ...extraClass) - const userNameHint = translate({ zh_hant: TEXT.zh_hant.userNameHint, zh_hans: TEXT.zh_hans.userNameHint, lang }) - const userNamePlaceholder = translate({ zh_hant: TEXT.zh_hant.enterUserName, zh_hans: TEXT.zh_hans.enterUserName, lang }) - const comparedUserNamePlaceholder = translate({ zh_hant: TEXT.zh_hant.enterUserNameAgign, zh_hans: TEXT.zh_hans.enterUserNameAgign, @@ -106,62 +101,60 @@ export const UserNameChangeConfirmForm: FC = ({ }) return ( - <> -
    - - - + + + + +
    + + - -
    - - - -
    - +
    +
    + - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ userName: '', comparedUserName: '' }), validate: ({ userName, comparedUserName }) => { - const isInvalidUserName = validateUserName(userName, lang) + const isInvalidUserName = validateUserName(userName) const isInvalidComparedUserName = validateComparedUserName( userName, - comparedUserName, - lang + comparedUserName ) const errors = { ...(isInvalidUserName ? { userName: isInvalidUserName } : {}), @@ -172,40 +165,28 @@ export const UserNameChangeConfirmForm: FC = ({ return errors }, - handleSubmit: (values, { props, setFieldError, setSubmitting }: any) => { + handleSubmit: async (values, { setFieldError, setSubmitting }) => { const { userName } = values - const { submitAction } = props - if (!submitAction) { - return - } - submitAction({ variables: { input: { userName } } }) - .then(({ data }: any) => { - if (submitCallback) { - submitCallback() - } - }) - .catch((error: any) => { - const errorCode = getErrorCodes(error)[0] - const errorMessage = translate({ - zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, - zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, - lang - }) - setFieldError('userName', errorMessage) - }) - .finally(() => { - setSubmitting(false) + try { + await update({ variables: { input: { userName } } }) + + if (submitCallback) { + submitCallback() + } + } catch (error) { + const errorCode = getErrorCodes(error)[0] + const errorMessage = translate({ + zh_hant: TEXT.zh_hant.error[errorCode] || errorCode, + zh_hans: TEXT.zh_hans.error[errorCode] || errorCode, + lang }) + setFieldError('userName', errorMessage) + } + + setSubmitting(false) } - })(BaseForm) - - return ( - <> - - {(update: any) => } - - - - ) + })(InnerForm) + + return } diff --git a/src/components/GQL/GraphqlErrorHandler.tsx b/src/components/GQL/GraphqlErrorHandler.tsx deleted file mode 100644 index 3ca9fffce0..0000000000 --- a/src/components/GQL/GraphqlErrorHandler.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import _get from 'lodash/get' -import React from 'react' -import { MutationResult, QueryResult } from 'react-apollo' - -import { checkError } from './error' - -export const QueryErrorHandler = ({ - result, - children -}: { - result: QueryResult - children: any -}) => { - if (result.error) { - checkError(result.error) - } - - return <>{children(result)} -} - -export const MutationErrorHandler = ({ - result, - mutateFn, - children -}: { - result: MutationResult - mutateFn: any - children: any -}) => { - const mutateWithCatch: any = async (options: any) => { - try { - const mutationResult = await mutateFn(options) - return mutationResult - } catch (err) { - checkError(err) - } - } - - return <>{children(mutateWithCatch, result)} -} diff --git a/src/components/GQL/Mutation.tsx b/src/components/GQL/Mutation.tsx deleted file mode 100644 index 4ee6bd13eb..0000000000 --- a/src/components/GQL/Mutation.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import { Mutation as ApolloMutation } from 'react-apollo' - -import { MutationErrorHandler } from './GraphqlErrorHandler' - -export const Mutation = ({ children, ...rest }: any) => ( - - {(mutateFn: any, result: any) => ( - - {children} - - )} - -) diff --git a/src/components/GQL/Query.tsx b/src/components/GQL/Query.tsx deleted file mode 100644 index c11e8b1c6f..0000000000 --- a/src/components/GQL/Query.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import { Query as ApolloQuery } from 'react-apollo' - -import ErrorBoundary from '~/components/ErrorBoundary' - -import { QueryErrorHandler } from './GraphqlErrorHandler' - -export const Query = ({ children, ...rest }: any) => ( - - - {(result: any) => ( - {children} - )} - - -) diff --git a/src/components/GQL/error.tsx b/src/components/GQL/error.tsx index dfa0bdd8b0..841d6af3ed 100644 --- a/src/components/GQL/error.tsx +++ b/src/components/GQL/error.tsx @@ -1,24 +1,22 @@ import * as Sentry from '@sentry/browser' import { ApolloError } from 'apollo-client' -import _get from 'lodash/get' + +import { Error } from '~/components/Error' import { ADD_TOAST, ERROR_CODES, TEXT } from '~/common/enums' import { Translate } from '../Language' import { ModalSwitch } from '../ModalManager' -export const checkFor = (code: string, errors: ApolloError['graphQLErrors']) => - errors && errors.find(e => _get(e, 'extensions.code') === code) - -export const getErrorCodes = (error: any) => { +export const getErrorCodes = (error: ApolloError) => { const errorCodes: string[] = [] if (!error || !error.graphQLErrors) { return errorCodes } - error.graphQLErrors.forEach((e: any) => { - const code = _get(e, 'extensions.code') + error.graphQLErrors.forEach(e => { + const code = e.extensions && e.extensions.code if (code) { errorCodes.push(code) } @@ -27,7 +25,10 @@ export const getErrorCodes = (error: any) => { return errorCodes } -export const checkError = (error: ApolloError) => { +/** + * Check mutation on error to throw a `` + */ +export const mutationOnError = (error: ApolloError) => { // Add info to Sentry Sentry.captureException(error) @@ -119,7 +120,7 @@ export const checkError = (error: ApolloError) => { ERROR_CODES.TAG_NOT_FOUND, ERROR_CODES.NOTICE_NOT_FOUND, ERROR_CODES.NOT_ENOUGH_MAT, - ERROR_CODES.USER_FOLLOW_FAILED + ERROR_CODES.ACTION_FAILED ] CATCH_CODES.forEach(code => { if (errorMap[code]) { @@ -148,3 +149,16 @@ export const checkError = (error: ApolloError) => { */ throw error } + +/** + * Pass an `useQuery` or `useLazyQuery` error and return `` + */ + +export const QueryError = ({ error }: { error: ApolloError }) => { + // Add info to Sentry + Sentry.captureException(error) + + const errorCodes = getErrorCodes(error) + + return +} diff --git a/src/components/GQL/fragments/comment.ts b/src/components/GQL/fragments/comment.ts index 66b36822ed..469377fabb 100644 --- a/src/components/GQL/fragments/comment.ts +++ b/src/components/GQL/fragments/comment.ts @@ -35,6 +35,10 @@ export default { feed: gql` fragment FeedDigestComment on Comment { ...BaseDigestComment + author { + id + isBlocking + } comments(input: { sort: oldest, first: null }) @include(if: $hasDescendantComments) { edges { diff --git a/src/components/GQL/fragments/user.ts b/src/components/GQL/fragments/user.ts new file mode 100644 index 0000000000..d00b6d5e44 --- /dev/null +++ b/src/components/GQL/fragments/user.ts @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export default { + block: gql` + fragment BlockUser on User { + id + userName + displayName + isBlocked + } + ` +} diff --git a/src/components/GQL/hooks.ts b/src/components/GQL/hooks.ts new file mode 100644 index 0000000000..e697ebb79e --- /dev/null +++ b/src/components/GQL/hooks.ts @@ -0,0 +1,21 @@ +import { OperationVariables } from '@apollo/react-common' +import { DocumentNode } from 'graphql' +import { + MutationHookOptions, + MutationTuple, + useMutation as baseUseMutation +} from 'react-apollo' + +import { mutationOnError } from './error' + +export const useMutation = ( + mutation: DocumentNode, + options?: MutationHookOptions +): MutationTuple => { + const [mutate, result] = baseUseMutation(mutation, { + onError: error => mutationOnError(error), + ...options + }) + + return [mutate, result] +} diff --git a/src/components/GQL/index.ts b/src/components/GQL/index.ts index 68db404eca..d60c312722 100644 --- a/src/components/GQL/index.ts +++ b/src/components/GQL/index.ts @@ -1,4 +1,2 @@ -export * from './GraphqlErrorHandler' -export * from './Mutation' -export * from './Query' export * from './error' +export * from './hooks' diff --git a/src/components/GQL/mutations/blockUser.ts b/src/components/GQL/mutations/blockUser.ts new file mode 100644 index 0000000000..ce44ca61b2 --- /dev/null +++ b/src/components/GQL/mutations/blockUser.ts @@ -0,0 +1,10 @@ +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/unblockUser.ts b/src/components/GQL/mutations/unblockUser.ts new file mode 100644 index 0000000000..f753ecb950 --- /dev/null +++ b/src/components/GQL/mutations/unblockUser.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag' + +export default gql` + mutation UnblockUser($id: ID!) { + unblockUser(input: { id: $id }) { + id + isBlocked + } + } +` diff --git a/src/components/GQL/updates/viewerUnreadFolloweeArticles.ts b/src/components/GQL/updates/viewerUnreadFolloweeArticles.ts index 28290e1662..fafbc46174 100644 --- a/src/components/GQL/updates/viewerUnreadFolloweeArticles.ts +++ b/src/components/GQL/updates/viewerUnreadFolloweeArticles.ts @@ -1,8 +1,11 @@ +import { DataProxy } from 'apollo-cache' + +import { UnreadFolloweeArticles } from '~/components/GQL/queries/__generated__/UnreadFolloweeArticles' import UNREAD_FOLLOWEE_ARTICLES from '~/components/GQL/queries/unreadFolloweeArticles' -const update = (cache: any) => { +const update = (cache: DataProxy) => { try { - const data = cache.readQuery({ + const data = cache.readQuery({ query: UNREAD_FOLLOWEE_ARTICLES }) diff --git a/src/components/GQL/updates/viewerUnreadNoticeCount.ts b/src/components/GQL/updates/viewerUnreadNoticeCount.ts index 145150b76c..519e036902 100644 --- a/src/components/GQL/updates/viewerUnreadNoticeCount.ts +++ b/src/components/GQL/updates/viewerUnreadNoticeCount.ts @@ -1,8 +1,11 @@ +import { DataProxy } from 'apollo-cache' + +import { UnreadNoticeCount } from '~/components/GQL/queries/__generated__/UnreadNoticeCount' import { UNREAD_NOTICE_COUNT } from '~/components/GQL/queries/notice' -const update = (cache: any) => { +const update = (cache: DataProxy) => { try { - const cacheData = cache.readQuery({ + const cacheData = cache.readQuery({ query: UNREAD_NOTICE_COUNT }) diff --git a/src/components/GlobalHeader/Context.tsx b/src/components/GlobalHeader/Context.tsx index 71d8d9c7d4..729c9cdf77 100644 --- a/src/components/GlobalHeader/Context.tsx +++ b/src/components/GlobalHeader/Context.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import { createContext, useState } from 'react' export interface DefaultHeader { diff --git a/src/components/GlobalHeader/Logo/index.tsx b/src/components/GlobalHeader/Logo/index.tsx index 833d00469e..ad66c96813 100644 --- a/src/components/GlobalHeader/Logo/index.tsx +++ b/src/components/GlobalHeader/Logo/index.tsx @@ -8,16 +8,15 @@ import ICON_LOGO from '~/static/icons/logo.svg?sprite' import styles from './styles.css' export default () => ( - <> - - - - - - - + + + + + + + ) diff --git a/src/components/GlobalHeader/MeDigest/DropdownMenu.tsx b/src/components/GlobalHeader/MeDigest/DropdownMenu.tsx index fc82c3e23b..5eaf353337 100644 --- a/src/components/GlobalHeader/MeDigest/DropdownMenu.tsx +++ b/src/components/GlobalHeader/MeDigest/DropdownMenu.tsx @@ -1,9 +1,9 @@ -import _get from 'lodash/get' import Link from 'next/link' import { useContext } from 'react' import { Icon, LanguageContext, Menu, TextIcon } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { UserLogout } from '~/components/GQL/mutations/__generated__/UserLogout' import USER_LOGOUT from '~/components/GQL/mutations/userLogout' import { Translate } from '~/components/Language' import { ViewerContext } from '~/components/Viewer' @@ -23,6 +23,7 @@ import ICON_READING_HISTORY from '~/static/icons/reading-history.svg?sprite' import ICON_SETTINGS from '~/static/icons/settings.svg?sprite' const DropdownMenu = ({ hideDropdown }: { hideDropdown: () => void }) => { + const [logout] = useMutation(USER_LOGOUT) const { lang } = useContext(LanguageContext) const viewer = useContext(ViewerContext) const userPath = toPath({ @@ -121,53 +122,49 @@ const DropdownMenu = ({ hideDropdown }: { hideDropdown: () => void }) => { - - {(logout: any) => ( - - )} - + } + text={translate({ + zh_hant: TEXT.zh_hant.logout, + zh_hans: TEXT.zh_hans.logout, + lang + })} + spacing="xtight" + /> + ) diff --git a/src/components/GlobalHeader/MeDigest/index.tsx b/src/components/GlobalHeader/MeDigest/index.tsx index d5ebdd3c36..c7aa860375 100644 --- a/src/components/GlobalHeader/MeDigest/index.tsx +++ b/src/components/GlobalHeader/MeDigest/index.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' import { useContext, useState } from 'react' import { Dropdown, PopperInstance } from '~/components' @@ -33,40 +32,40 @@ const MeDigest = ({ user }: { user: MeDigestUser }) => { }) return ( - <> - } - onCreate={setInstance} - > - - - - + } + onCreate={setInstance} + > + + ) } diff --git a/src/components/GlobalHeader/Nav/DesktopNav/index.tsx b/src/components/GlobalHeader/Nav/DesktopNav/index.tsx index 9436bf7efb..c63ebca630 100644 --- a/src/components/GlobalHeader/Nav/DesktopNav/index.tsx +++ b/src/components/GlobalHeader/Nav/DesktopNav/index.tsx @@ -33,6 +33,7 @@ const DesktopNav: React.FC<{ unread: boolean }> = ({ unread }) => { /> + @@ -43,6 +44,7 @@ const DesktopNav: React.FC<{ unread: boolean }> = ({ unread }) => { + ) diff --git a/src/components/GlobalHeader/Nav/MobileNav/index.tsx b/src/components/GlobalHeader/Nav/MobileNav/index.tsx index 2f112ef0df..88701b442c 100644 --- a/src/components/GlobalHeader/Nav/MobileNav/index.tsx +++ b/src/components/GlobalHeader/Nav/MobileNav/index.tsx @@ -8,7 +8,7 @@ import ICON_MENU from '~/static/icons/menu.svg?sprite' import DropdownContent from './DropdownContent' import styles from './styles.css' -export default ({ unread }: { unread: boolean }) => { +const MobileNav = ({ unread }: { unread: boolean }) => { const [instance, setInstance] = useState(null) const hideDropdown = () => { if (!instance) { @@ -34,8 +34,11 @@ export default ({ unread }: { unread: boolean }) => { viewBox={ICON_MENU.viewBox} style={{ width: 20, height: 16 }} /> + ) } + +export default MobileNav diff --git a/src/components/GlobalHeader/Nav/index.tsx b/src/components/GlobalHeader/Nav/index.tsx index cd26f7bb9f..b4a0858df1 100644 --- a/src/components/GlobalHeader/Nav/index.tsx +++ b/src/components/GlobalHeader/Nav/index.tsx @@ -1,6 +1,8 @@ import _get from 'lodash/get' -import { Query, QueryResult } from 'react-apollo' +import { useEffect } from 'react' +import { useQuery } from 'react-apollo' +import { UnreadFolloweeArticles } from '~/components/GQL/queries/__generated__/UnreadFolloweeArticles' import UNREAD_FOLLOWEE_ARTICLES from '~/components/GQL/queries/unreadFolloweeArticles' import { POLL_INTERVAL } from '~/common/enums' @@ -9,28 +11,35 @@ import DesktopNav from './DesktopNav' import MobileNav from './MobileNav' import styles from './styles.css' -export default () => ( - - {({ data }: QueryResult) => { - const unread = !!_get(data, 'viewer.status.unreadFolloweeArticles') - - return ( - - ) - }} - -) +const Nav = () => { + const { data, startPolling } = useQuery( + UNREAD_FOLLOWEE_ARTICLES, + { + errorPolicy: 'none', + fetchPolicy: 'network-only', + skip: !process.browser + } + ) + const unread = !!_get(data, 'viewer.status.unreadFolloweeArticles') + + // FIXME: https://github.com/apollographql/apollo-client/issues/3775 + useEffect(() => { + startPolling(POLL_INTERVAL) + }, []) + + return ( + + ) +} + +export default Nav diff --git a/src/components/GlobalHeader/NotificationButton/DropdownNotices/index.tsx b/src/components/GlobalHeader/NotificationButton/DropdownNotices/index.tsx index 62c48d230e..ab19cb4c7c 100644 --- a/src/components/GlobalHeader/NotificationButton/DropdownNotices/index.tsx +++ b/src/components/GlobalHeader/NotificationButton/DropdownNotices/index.tsx @@ -1,8 +1,8 @@ -import _get from 'lodash/get' import Link from 'next/link' import { Error, Icon, Spinner, TextIcon, Translate } from '~/components' import EmptyNotice from '~/components/Empty/EmptyNotice' +import { MeNotifications } from '~/components/GQL/queries/__generated__/MeNotifications' import NoticeDigest from '~/components/NoticeDigest' import { PATHS, TEXT } from '~/common/enums' @@ -13,7 +13,7 @@ import styles from './styles.css' interface DropdownNoticesProps { hideDropdown: () => void - data: any + data?: MeNotifications loading: boolean error: any } @@ -42,6 +42,7 @@ const Header = () => ( + ) @@ -66,6 +67,7 @@ const Footer = () => ( + ) @@ -108,7 +110,7 @@ const DropdownNotices = ({ ) } - const edges = _get(data, 'viewer.notices.edges') + const edges = data && data.viewer && data.viewer.notices.edges return (
    @@ -117,7 +119,7 @@ const DropdownNotices = ({
      {edges && edges.length > 0 ? ( - edges.map(({ node, cursor }: { node: any; cursor: any }) => ( + edges.map(({ node, cursor }) => (
    • diff --git a/src/components/GlobalHeader/NotificationButton/index.tsx b/src/components/GlobalHeader/NotificationButton/index.tsx index 526ed79584..b5f563e50d 100644 --- a/src/components/GlobalHeader/NotificationButton/index.tsx +++ b/src/components/GlobalHeader/NotificationButton/index.tsx @@ -1,12 +1,14 @@ import classNames from 'classnames' -import _get from 'lodash/get' -import { useContext, useState } from 'react' -import { Query, QueryResult } from 'react-apollo' +import { useContext, useEffect, useState } from 'react' +import { useQuery } from 'react-apollo' import { Dropdown, Icon, PopperInstance } from '~/components' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { MarkAllNoticesAsRead } from '~/components/GQL/mutations/__generated__/MarkAllNoticesAsRead' import MARK_ALL_NOTICES_AS_READ from '~/components/GQL/mutations/markAllNoticesAsRead' +import { MeNotifications } from '~/components/GQL/queries/__generated__/MeNotifications' +import { UnreadNoticeCount } from '~/components/GQL/queries/__generated__/UnreadNoticeCount' import { ME_NOTIFICATIONS, UNREAD_NOTICE_COUNT @@ -27,7 +29,7 @@ const NoticeButton = ({ refetch, markAllNoticesAsRead }: { - data: any + data?: MeNotifications loading: boolean error: any hasUnreadNotices: any @@ -73,48 +75,58 @@ const NoticeButton = ({ > ) } -export default () => ( - - {({ data: unreadCountData }: QueryResult) => ( - - {({ data, loading, error, refetch }: QueryResult) => ( - - {(markAllNoticesAsRead: any) => ( - = - 1 - } - markAllNoticesAsRead={markAllNoticesAsRead} - /> - )} - - )} - - )} - -) +const NotificationButton = () => { + const { data: unreadCountData, startPolling } = useQuery( + UNREAD_NOTICE_COUNT, + { + errorPolicy: 'ignore', + fetchPolicy: 'network-only', + skip: !process.browser + } + ) + const { data, loading, error, refetch } = useQuery( + ME_NOTIFICATIONS, + { + variables: { first: 5 }, + errorPolicy: 'ignore', + notifyOnNetworkStatusChange: true + } + ) + const [markAllNoticesAsRead] = useMutation( + MARK_ALL_NOTICES_AS_READ, + { + update: updateViewerUnreadNoticeCount + } + ) + + // FIXME: https://github.com/apollographql/apollo-client/issues/3775 + useEffect(() => { + startPolling(POLL_INTERVAL) + }, []) + + return ( + = 1 + } + markAllNoticesAsRead={markAllNoticesAsRead} + /> + ) +} + +export default NotificationButton diff --git a/src/components/GlobalHeader/SearchButton/index.tsx b/src/components/GlobalHeader/SearchButton/index.tsx index 8b7042ba1e..79e5ef6049 100644 --- a/src/components/GlobalHeader/SearchButton/index.tsx +++ b/src/components/GlobalHeader/SearchButton/index.tsx @@ -9,18 +9,19 @@ import ICON_SEARCH from '~/static/icons/search.svg?sprite' import styles from './styles.css' -export default () => { +const SearchButton = () => { const { headerState } = useContext(HeaderContext) const isDraft = headerState.type === 'draft' return ( - <> - - - - - + + + ) } + +export default SearchButton diff --git a/src/components/GlobalHeader/WriteButton/index.tsx b/src/components/GlobalHeader/WriteButton/index.tsx index de9fc51601..17dd8c6655 100644 --- a/src/components/GlobalHeader/WriteButton/index.tsx +++ b/src/components/GlobalHeader/WriteButton/index.tsx @@ -1,9 +1,9 @@ import gql from 'graphql-tag' import Router from 'next/router' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Button, Icon, LanguageContext, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { ModalSwitch } from '~/components/ModalManager' import { TEXT } from '~/common/enums' @@ -15,7 +15,9 @@ import { CreateDraft } from './__generated__/CreateDraft' interface Props { allowed: boolean - CustomButton?: FC<{ onClick: (event: React.MouseEvent) => void }> + CustomButton?: React.FC<{ + onClick: (event: React.MouseEvent) => void + }> } export const CREATE_DRAFT = gql` @@ -27,13 +29,28 @@ export const CREATE_DRAFT = gql` } ` +const WriteIcon = ({ loading }: { loading: boolean }) => { + const icon = loading ? ICON_SPINNER : ICON_WRITE + + return ( + + ) +} + const WriteButton = ({ allowed, CustomButton }: Props) => { const { lang } = useContext(LanguageContext) - - const placeholder = translate({ - zh_hans: TEXT.zh_hans.untitle, - zh_hant: TEXT.zh_hant.untitle, - lang + const [putDraft, { loading }] = useMutation(CREATE_DRAFT, { + variables: { + title: translate({ + zh_hans: TEXT.zh_hans.untitle, + zh_hant: TEXT.zh_hant.untitle, + lang + }) + } }) if (!allowed) { @@ -57,55 +74,43 @@ const WriteButton = ({ allowed, CustomButton }: Props) => { ) } - return ( - - {(putDraft: any, { loading }: any) => { - const WriteIcon = () => { - const icon = loading ? ICON_SPINNER : ICON_WRITE - return ( - - ) - } - const onClick = () => { - putDraft().then((result: any) => { - const { data } = result as { data: CreateDraft } - const { slug, id } = data.putDraft - const path = toPath({ page: 'draftDetail', slug, id }) - Router.push(path.as) - }) - } + const onClick = () => { + putDraft().then(({ data }) => { + const { slug, id } = (data && data.putDraft) || {} - return CustomButton ? ( - - ) : ( - <> - + if (slug && id) { + const path = toPath({ page: 'draftDetail', slug, id }) + Router.push(path.as) + } + }) + } - + + )} + ) diff --git a/src/components/Menu/Header/index.tsx b/src/components/Menu/Header/index.tsx index 62697a42b3..5b0baf762c 100644 --- a/src/components/Menu/Header/index.tsx +++ b/src/components/Menu/Header/index.tsx @@ -13,6 +13,7 @@ const Item: React.FC = ({ title, children }) => { {title} {children} + ) diff --git a/src/components/Menu/Item/index.tsx b/src/components/Menu/Item/index.tsx index 66e637b1cb..90585dc230 100644 --- a/src/components/Menu/Item/index.tsx +++ b/src/components/Menu/Item/index.tsx @@ -30,6 +30,7 @@ const Item: React.FC = ({ return (
    • {children} +
    • ) diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 0a6661a2cd..288d233e62 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -36,12 +36,11 @@ export class Menu extends React.PureComponent { }) return ( - <> -
        - {children} -
      +
        + {children} + - +
      ) } } diff --git a/src/components/Modal/Anchor/index.tsx b/src/components/Modal/Anchor/index.tsx index 6791a7ce80..43b5a5e2f3 100644 --- a/src/components/Modal/Anchor/index.tsx +++ b/src/components/Modal/Anchor/index.tsx @@ -53,7 +53,7 @@ const Anchor = () => { viewer.isAuthed && !isLikeCoinClosed && isLikeCoinAllowPaths && - (viewer.isOnboarding || !viewer.likerId) + viewer.shouldSetupLikerID const closeLikeCoinModal = () => { setIsLikeCoinClosed(true) } @@ -101,7 +101,9 @@ const Anchor = () => { {viewer.isAuthed && disagreedToS && } + {shouldShowLikeCoinModal && } + ) diff --git a/src/components/Modal/BlockUserModal/index.tsx b/src/components/Modal/BlockUserModal/index.tsx new file mode 100644 index 0000000000..8fb25adc89 --- /dev/null +++ b/src/components/Modal/BlockUserModal/index.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react' +import { useMutation } from 'react-apollo' + +import { BlockUser } from '~/components/GQL/fragments/__generated__/BlockUser' +import { BlockUser as BlockUserMutate } from '~/components/GQL/mutations/__generated__/BlockUser' +import BLOCK_USER from '~/components/GQL/mutations/blockUser' +import { Translate } from '~/components/Language' +import { Modal } from '~/components/Modal' +import ModalComplete from '~/components/Modal/Complete' + +import { TEXT } from '~/common/enums' + +/** + * This component is a modal for changing user name. + * + * Usage: + * + * ```jsx + * + * ``` + * + */ + +interface Props { + user: BlockUser +} + +type Step = 'confirm' | 'complete' + +const BlocKUserModal: React.FC = ({ + close, + user +}) => { + const [step, setStep] = useState('confirm') + const [blockUser] = useMutation(BLOCK_USER, { + variables: { id: user.id }, + optimisticResponse: { + blockUser: { + id: user.id, + isBlocked: true, + __typename: 'User' + } + } + }) + + const confirmCallback = () => setStep('complete') + + return ( + <> + {step === 'confirm' && ( + <> + + +
      + +
      +
      + + + + { + event.stopPropagation() + await blockUser() + confirmCallback() + }} + > + + +
      + + )} + {step === 'complete' && ( + + } + /> + )} + + ) +} + +export default BlocKUserModal diff --git a/src/components/Modal/Complete/index.tsx b/src/components/Modal/Complete/index.tsx index 699d0a52db..9732021644 100644 --- a/src/components/Modal/Complete/index.tsx +++ b/src/components/Modal/Complete/index.tsx @@ -15,7 +15,9 @@ const ModalComplete = ({ {message} +

      {hint}

      + diff --git a/src/components/Modal/Container/index.tsx b/src/components/Modal/Container/index.tsx index 32fcbdba26..7e79b696e7 100644 --- a/src/components/Modal/Container/index.tsx +++ b/src/components/Modal/Container/index.tsx @@ -1,6 +1,6 @@ // External modules import classNames from 'classnames' -import { FC, useContext, useRef, useState } from 'react' +import { useContext, useRef, useState } from 'react' import { LanguageContext } from '~/components' import { useNativeEventListener, useOutsideClick } from '~/components/Hook' @@ -20,7 +20,7 @@ export interface ContainerProps { layout?: 'default' | 'small' } -const Container: FC = ({ +const Container: React.FC = ({ children, close, defaultCloseable = true, @@ -100,6 +100,7 @@ const Container: FC = ({ + ) diff --git a/src/components/Modal/EmailModal/index.tsx b/src/components/Modal/EmailModal/index.tsx index 4e31dac617..6ef4c20df0 100644 --- a/src/components/Modal/EmailModal/index.tsx +++ b/src/components/Modal/EmailModal/index.tsx @@ -1,4 +1,4 @@ -import { FC, useContext, useState } from 'react' +import { useContext, useState } from 'react' import { EmailChangeConfirmForm, @@ -23,7 +23,7 @@ import { TEXT } from '~/common/enums' type Step = 'request' | 'confirm' | 'complete' -const EmailModal: FC = ({ close }) => { +const EmailModal: React.FC = ({ close }) => { const viewer = useContext(ViewerContext) const [step, setStep] = useState('request') const [data, setData] = useState<{ [key: string]: any }>({ diff --git a/src/components/Modal/FooterButton/index.tsx b/src/components/Modal/FooterButton/index.tsx index 0ba1cc964d..32d9f0596e 100644 --- a/src/components/Modal/FooterButton/index.tsx +++ b/src/components/Modal/FooterButton/index.tsx @@ -56,6 +56,7 @@ const FooterButton: React.FC = ({ {children} + ) @@ -68,6 +69,7 @@ const FooterButton: React.FC = ({ {loading && } {!loading && children} + ) diff --git a/src/components/Modal/Header/index.tsx b/src/components/Modal/Header/index.tsx index 934b47a12d..c4f333f9d9 100644 --- a/src/components/Modal/Header/index.tsx +++ b/src/components/Modal/Header/index.tsx @@ -39,6 +39,7 @@ const ModalHeader: React.FC = ({ /> )} + )} diff --git a/src/components/Modal/LikeCoinTermModal/index.tsx b/src/components/Modal/LikeCoinTermModal/index.tsx index 92ff3c2b65..06835fcbb5 100644 --- a/src/components/Modal/LikeCoinTermModal/index.tsx +++ b/src/components/Modal/LikeCoinTermModal/index.tsx @@ -1,4 +1,4 @@ -import { FC, useContext } from 'react' +import { useContext } from 'react' import { LanguageContext, Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -15,7 +15,7 @@ interface Props { submitCallback: () => void } -const LikeCoinTermModal: FC = ({ +const LikeCoinTermModal: React.FC = ({ submitCallback }) => { const { lang } = useContext(LanguageContext) diff --git a/src/components/Modal/PasswordModal/index.tsx b/src/components/Modal/PasswordModal/index.tsx index 11600e50c0..184b9cbc79 100644 --- a/src/components/Modal/PasswordModal/index.tsx +++ b/src/components/Modal/PasswordModal/index.tsx @@ -1,4 +1,4 @@ -import { FC, useContext, useState } from 'react' +import { useContext, useState } from 'react' import { PasswordChangeConfirmForm, @@ -23,7 +23,7 @@ import { translate } from '~/common/utils' * */ -const PasswordModal: FC< +const PasswordModal: React.FC< ModalInstanceProps & { purpose: 'forget' | 'change' } > = ({ purpose }) => { const { lang } = useContext(LanguageContext) @@ -96,7 +96,7 @@ const PasswordModal: FC< setStep('reset') } - const backPreviousStep = (event: any) => { + const backPreviousStep = () => { setStep('request') } diff --git a/src/components/Modal/PublishModal/PublishSlide.tsx b/src/components/Modal/PublishModal/PublishSlide.tsx index 801c13eb34..1c0c72aed2 100644 --- a/src/components/Modal/PublishModal/PublishSlide.tsx +++ b/src/components/Modal/PublishModal/PublishSlide.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import { useContext } from 'react' import { LanguageContext } from '~/components/Language' @@ -16,6 +15,7 @@ const Descriptions = ({ data }: any) => ( {desc} ))} + ) @@ -66,6 +66,7 @@ const PublishSlide = () => { style={{ backgroundImage: `url(${PUBLISH_IMAGE})` }} /> +
      {title} @@ -74,6 +75,7 @@ const PublishSlide = () => { </div> <Descriptions data={descriptions} /> </div> + <style jsx>{styles}</style> </> ) diff --git a/src/components/Modal/PublishModal/index.tsx b/src/components/Modal/PublishModal/index.tsx index 075e5a107a..cbe720f9fd 100644 --- a/src/components/Modal/PublishModal/index.tsx +++ b/src/components/Modal/PublishModal/index.tsx @@ -1,12 +1,11 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { FC } from 'react' import { DraftDetailQuery_node_Draft } from '~/views/Me/DraftDetail/__generated__/DraftDetailQuery' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Translate } from '~/components/Language' import { Modal } from '~/components/Modal' +import { PublishArticle } from './__generated__/PublishArticle' import PublishSlide from './PublishSlide' import styles from './styles.css' @@ -37,13 +36,24 @@ const PUBLISH_ARTICLE = gql` } ` -export const PublishModal: FC<Props> = ({ close, draft }) => { +export const PublishModal: React.FC<Props> = ({ close, draft }) => { const draftId = draft.id const hasContent = draft.content && draft.content.length > 0 const hasTitle = draft.title && draft.title.length > 0 const isUnpublished = draft.publishState === 'unpublished' const publishable = draftId && isUnpublished && hasContent && hasTitle + const [publish] = useMutation<PublishArticle>(PUBLISH_ARTICLE, { + optimisticResponse: { + publishArticle: { + id: draftId, + scheduledAt: new Date(Date.now() + 1000).toISOString(), + publishState: 'pending' as any, + __typename: 'Draft' + } + } + }) + return ( <section> <Modal.Content layout="full-width" spacing="none"> @@ -55,38 +65,22 @@ export const PublishModal: FC<Props> = ({ close, draft }) => { <Translate zh_hant="暫存作品" zh_hans="暫存作品" /> </Modal.FooterButton> - <Mutation - mutation={PUBLISH_ARTICLE} - optimisticResponse={{ - publishArticle: { - id: draftId, - scheduledAt: new Date(Date.now() + 1000).toISOString(), - publishState: 'pending', - __typename: 'Draft' + <Modal.FooterButton + disabled={!publishable} + onClick={async () => { + const { data } = await publish({ variables: { draftId } }) + const state = + (data && data.publishArticle.publishState) || 'unpublished' + + if (state === 'pending') { + close() } }} > - {(publish: any, loading: any) => ( - <Modal.FooterButton - disabled={!publishable} - onClick={async () => { - const { data }: any = await publish({ variables: { draftId } }) - const state = _get( - data, - 'publishArticle.publishState', - 'unpublished' - ) - - if (state === 'pending') { - close() - } - }} - > - <Translate zh_hant="發佈作品" zh_hans="发布作品" /> - </Modal.FooterButton> - )} - </Mutation> + <Translate zh_hant="發佈作品" zh_hans="发布作品" /> + </Modal.FooterButton> </div> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/Modal/SignUpModal/index.tsx b/src/components/Modal/SignUpModal/index.tsx index 7230b468c5..457b5ea520 100644 --- a/src/components/Modal/SignUpModal/index.tsx +++ b/src/components/Modal/SignUpModal/index.tsx @@ -1,4 +1,4 @@ -import { FC, useContext, useState } from 'react' +import { useContext, useState } from 'react' import SignUpComplete from '~/components/Form/SignUpComplete' import { SignUpInitForm, SignUpProfileForm } from '~/components/Form/SignUpForm' @@ -22,7 +22,10 @@ import { translate } from '~/common/utils' type Step = 'signUp' | 'profile' | 'setupLikeCoin' | 'complete' -const SignUpModal: FC<ModalInstanceProps> = ({ closeable, setCloseable }) => { +const SignUpModal: React.FC<ModalInstanceProps> = ({ + closeable, + setCloseable +}) => { const { lang } = useContext(LanguageContext) const [step, setStep] = useState<Step>('signUp') diff --git a/src/components/Modal/TermModal/index.tsx b/src/components/Modal/TermModal/index.tsx index d966c3626d..c9b82eff61 100644 --- a/src/components/Modal/TermModal/index.tsx +++ b/src/components/Modal/TermModal/index.tsx @@ -1,9 +1,10 @@ -import { withFormik } from 'formik' +import { FormikProps, FormikValues, withFormik } from 'formik' import gql from 'graphql-tag' import Router from 'next/router' -import { FC, useContext } from 'react' +import { useContext } from 'react' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { UserLogout } from '~/components/GQL/mutations/__generated__/UserLogout' import USER_LOGOUT from '~/components/GQL/mutations/userLogout' import IconSpinner from '~/components/Icon/Spinner' import { LanguageContext } from '~/components/Language' @@ -13,6 +14,7 @@ import { TEXT } from '~/common/enums' import { translate } from '~/common/utils' import { Modal } from '..' +import { UpdateUserInfoAgreeOn } from './__generated__/UpdateUserInfoAgreeOn' import styles from './styles.css' /** @@ -25,6 +27,8 @@ import styles from './styles.css' * ``` */ +type FormProps = ModalInstanceProps + const UPDATE_AGREE_ON = gql` mutation UpdateUserInfoAgreeOn($input: UpdateUserInfoInput!) { updateUserInfo(input: $input) { @@ -36,98 +40,85 @@ const UPDATE_AGREE_ON = gql` } ` -const TermModal: FC<ModalInstanceProps> = ({ close }) => { +const TermModal: React.FC<FormProps> = formProps => { + const [logout] = useMutation<UserLogout>(USER_LOGOUT) + const [update] = useMutation<UpdateUserInfoAgreeOn>(UPDATE_AGREE_ON) const { lang } = useContext(LanguageContext) - const disagree = (action: any) => { - if (action) { - action() - .then(() => { - close() - Router.replace('/') - }) - .catch(() => { - // TODO: Handle error - }) - } - } + const InnerForm = ({ + isSubmitting, + handleSubmit + }: FormikProps<FormikValues>) => ( + <form onSubmit={handleSubmit}> + <div className="term"> + <span className="hint"> + {translate({ + zh_hant: + '我們的用戶協議和隱私政策發生了更改,請閱讀並同意後繼續使用', + zh_hans: + '我们的用户协议和隐私政策发生了更改,请阅读并同意后继续使用', + lang + })} + 。 + </span> + <div className="context"> + <Term /> + </div> + </div> + <div className="buttons"> + <Modal.FooterButton + onClick={() => { + logout() + .then(() => { + formProps.close() + Router.replace('/') + }) + .catch(() => { + // TODO: Handle error + }) + }} + bgColor="white" + > + {translate({ + zh_hant: TEXT.zh_hant.disagree, + zh_hans: TEXT.zh_hans.disagree, + lang + })} + </Modal.FooterButton> - const BaseForm = (props: any) => ( - <> - <form className="form" onSubmit={props.handleSubmit}> - <div className="term"> - <span className="hint"> - {translate({ - zh_hant: - '我們的用戶協議和隱私政策發生了更改,請閱讀並同意後繼續使用', - zh_hans: - '我们的用户协议和隐私政策发生了更改,请阅读并同意后继续使用', + <Modal.FooterButton + htmlType="submit" + disabled={isSubmitting} + loading={isSubmitting} + > + {isSubmitting && <IconSpinner />} + {!isSubmitting && + translate({ + zh_hant: TEXT.zh_hant.agreeAndContinue, + zh_hans: TEXT.zh_hans.agreeAndContinue, lang })} - 。 - </span> - <div className="context"> - <Term /> - </div> - </div> - <div className="buttons"> - <Mutation mutation={USER_LOGOUT}> - {(logout: any) => ( - <Modal.FooterButton - onClick={() => disagree(logout)} - bgColor="white" - > - {translate({ - zh_hant: TEXT.zh_hant.disagree, - zh_hans: TEXT.zh_hans.disagree, - lang - })} - </Modal.FooterButton> - )} - </Mutation> - <Modal.FooterButton - htmlType="submit" - disabled={props.isSubmitting} - loading={props.isSubmitting} - > - {props.isSubmitting && <IconSpinner />} - {!props.isSubmitting && - translate({ - zh_hant: TEXT.zh_hant.agreeAndContinue, - zh_hans: TEXT.zh_hans.agreeAndContinue, - lang - })} - </Modal.FooterButton> - </div> - </form> + </Modal.FooterButton> + </div> + <style jsx>{styles}</style> - </> + </form> ) - const TermForm: any = withFormik({ - handleSubmit: (values, { props, setSubmitting }: any) => { - const { submitAction } = props - if (!submitAction) { - return + const MainForm = withFormik<FormProps, {}>({ + handleSubmit: async (values, { props, setSubmitting }) => { + try { + await update({ variables: { input: { agreeOn: true } } }) + props.close() + } catch (error) { + // TODO: Handle error } - submitAction({ variables: { input: { agreeOn: true } } }) - .then((result: any) => { - close() - }) - .catch((result: any) => { - // TODO: Handle error - }) - .finally(() => { - setSubmitting(false) - }) + + setSubmitting(false) } - })(BaseForm) + })(InnerForm) - return ( - <Mutation mutation={UPDATE_AGREE_ON}> - {(update: any) => <TermForm submitAction={update} />} - </Mutation> - ) + return <MainForm {...formProps} /> } export default TermModal diff --git a/src/components/Modal/TermModal/styles.css b/src/components/Modal/TermModal/styles.css index 7ee3b1a07d..078aab0e66 100644 --- a/src/components/Modal/TermModal/styles.css +++ b/src/components/Modal/TermModal/styles.css @@ -1,4 +1,4 @@ -.form { +form { & > .term { padding: var(--spacing-tight) var(--spacing-tight) var(--spacing-default); diff --git a/src/components/Modal/UserNameModal/index.tsx b/src/components/Modal/UserNameModal/index.tsx index 9b8027c13b..b92ba6df80 100644 --- a/src/components/Modal/UserNameModal/index.tsx +++ b/src/components/Modal/UserNameModal/index.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react' +import { useState } from 'react' import { UserNameChangeConfirmForm } from '~/components/Form/UserNameChangeForm' import { Translate } from '~/components/Language' @@ -20,14 +20,9 @@ import { TEXT } from '~/common/enums' type Step = 'ask' | 'confirm' | 'complete' -const UserNameModal: FC<ModalInstanceProps> = ({ close }) => { +const UserNameModal: React.FC<ModalInstanceProps> = ({ close }) => { const [step, setStep] = useState<Step>('ask') - const askCallback = (event: any) => { - event.stopPropagation() - setStep('confirm') - } - const confirmCallback = () => setStep('complete') return ( @@ -47,7 +42,12 @@ const UserNameModal: FC<ModalInstanceProps> = ({ close }) => { zh_hans={TEXT.zh_hans.cancel} /> </Modal.FooterButton> - <Modal.FooterButton onClick={askCallback}> + <Modal.FooterButton + onClick={(event: any) => { + event.stopPropagation() + setStep('confirm') + }} + > <Translate zh_hant={TEXT.zh_hant.confirm} zh_hans={TEXT.zh_hans.confirm} diff --git a/src/components/NoticeDigest/ArticleMentionedYouNotice.tsx b/src/components/NoticeDigest/ArticleMentionedYouNotice.tsx index 5d8d0b51e7..62824d9c57 100644 --- a/src/components/NoticeDigest/ArticleMentionedYouNotice.tsx +++ b/src/components/NoticeDigest/ArticleMentionedYouNotice.tsx @@ -26,6 +26,7 @@ const ArticleMentionedYouNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticleNewAppreciationNotice.tsx b/src/components/NoticeDigest/ArticleNewAppreciationNotice.tsx index f13ed480e2..2788885ec8 100644 --- a/src/components/NoticeDigest/ArticleNewAppreciationNotice.tsx +++ b/src/components/NoticeDigest/ArticleNewAppreciationNotice.tsx @@ -61,6 +61,7 @@ const ArticleNewAppreciationNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticleNewCollectedNotice.tsx b/src/components/NoticeDigest/ArticleNewCollectedNotice.tsx index 9325af9255..3419839a1e 100644 --- a/src/components/NoticeDigest/ArticleNewCollectedNotice.tsx +++ b/src/components/NoticeDigest/ArticleNewCollectedNotice.tsx @@ -38,6 +38,7 @@ const ArticleNewCollectedNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticleNewCommentNotice.tsx b/src/components/NoticeDigest/ArticleNewCommentNotice.tsx index 3f4a90715d..110ad87739 100644 --- a/src/components/NoticeDigest/ArticleNewCommentNotice.tsx +++ b/src/components/NoticeDigest/ArticleNewCommentNotice.tsx @@ -60,6 +60,7 @@ const ArticleNewCommentNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticleNewDownstreamNotice.tsx b/src/components/NoticeDigest/ArticleNewDownstreamNotice.tsx index 41dc046797..d97e0a86e6 100644 --- a/src/components/NoticeDigest/ArticleNewDownstreamNotice.tsx +++ b/src/components/NoticeDigest/ArticleNewDownstreamNotice.tsx @@ -59,6 +59,7 @@ const ArticleNewDownstreamNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticleNewSubscriberNotice.tsx b/src/components/NoticeDigest/ArticleNewSubscriberNotice.tsx index 84aaf7e156..e02c6fe8bf 100644 --- a/src/components/NoticeDigest/ArticleNewSubscriberNotice.tsx +++ b/src/components/NoticeDigest/ArticleNewSubscriberNotice.tsx @@ -58,6 +58,7 @@ const ArticleNewSubscriberNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/ArticlePublishedNotice.tsx b/src/components/NoticeDigest/ArticlePublishedNotice.tsx index e9d9ee37ae..6517892dc3 100644 --- a/src/components/NoticeDigest/ArticlePublishedNotice.tsx +++ b/src/components/NoticeDigest/ArticlePublishedNotice.tsx @@ -29,6 +29,7 @@ const ArticlePublishedNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/CommentMentionedYouNotice.tsx b/src/components/NoticeDigest/CommentMentionedYouNotice.tsx index 35593d0a08..f2dc558bfb 100644 --- a/src/components/NoticeDigest/CommentMentionedYouNotice.tsx +++ b/src/components/NoticeDigest/CommentMentionedYouNotice.tsx @@ -26,6 +26,7 @@ const CommentMentionedYouNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/CommentNewReplyNotice.tsx b/src/components/NoticeDigest/CommentNewReplyNotice.tsx index 4cc7b55344..8431ae6df8 100644 --- a/src/components/NoticeDigest/CommentNewReplyNotice.tsx +++ b/src/components/NoticeDigest/CommentNewReplyNotice.tsx @@ -58,6 +58,7 @@ const CommentNewReplyNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/CommentNewUpvoteNotice.tsx b/src/components/NoticeDigest/CommentNewUpvoteNotice.tsx index 9f56735f63..3563508dc2 100644 --- a/src/components/NoticeDigest/CommentNewUpvoteNotice.tsx +++ b/src/components/NoticeDigest/CommentNewUpvoteNotice.tsx @@ -58,6 +58,7 @@ const CommentNewUpvoteNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/CommentPinnedNotice.tsx b/src/components/NoticeDigest/CommentPinnedNotice.tsx index e67a898a77..99ca878c70 100644 --- a/src/components/NoticeDigest/CommentPinnedNotice.tsx +++ b/src/components/NoticeDigest/CommentPinnedNotice.tsx @@ -26,6 +26,7 @@ const CommentPinnedNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/DownstreamArticleArchivedNotice.tsx b/src/components/NoticeDigest/DownstreamArticleArchivedNotice.tsx index 076df57359..5f35f77516 100644 --- a/src/components/NoticeDigest/DownstreamArticleArchivedNotice.tsx +++ b/src/components/NoticeDigest/DownstreamArticleArchivedNotice.tsx @@ -35,6 +35,7 @@ const DownstreamArticleArchivedNotice = ({ <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/NoticeComment.tsx b/src/components/NoticeDigest/NoticeComment.tsx index e14ee75eed..853dc953da 100644 --- a/src/components/NoticeDigest/NoticeComment.tsx +++ b/src/components/NoticeDigest/NoticeComment.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import CommentContent from '~/components/CommentDigest/Content' @@ -12,7 +11,7 @@ const NoticeComment = ({ comment }: { comment: NoticeCommentType | null }) => { return null } - const parentId = _get(comment, 'parentComment.id') + const parentId = comment && comment.parentComment && comment.parentComment.id const path = toPath({ page: 'articleDetail', userName: comment.article.author.userName || '', diff --git a/src/components/NoticeDigest/OfficialAnnouncementNotice.tsx b/src/components/NoticeDigest/OfficialAnnouncementNotice.tsx index b6684996d1..9fd1851fb3 100644 --- a/src/components/NoticeDigest/OfficialAnnouncementNotice.tsx +++ b/src/components/NoticeDigest/OfficialAnnouncementNotice.tsx @@ -28,6 +28,7 @@ const OfficialAnnouncementNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/SubscribedArticleNewCommentNotice.tsx b/src/components/NoticeDigest/SubscribedArticleNewCommentNotice.tsx index 1d9747dada..93912d0653 100644 --- a/src/components/NoticeDigest/SubscribedArticleNewCommentNotice.tsx +++ b/src/components/NoticeDigest/SubscribedArticleNewCommentNotice.tsx @@ -67,6 +67,7 @@ const SubscribedArticleNewCommentNotice = ({ <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/NoticeDigest/UserNewFollowerNotice.tsx b/src/components/NoticeDigest/UserNewFollowerNotice.tsx index c156a8e5f2..b8f2bc2d94 100644 --- a/src/components/NoticeDigest/UserNewFollowerNotice.tsx +++ b/src/components/NoticeDigest/UserNewFollowerNotice.tsx @@ -59,6 +59,7 @@ const UserNewFollowerNotice = ({ notice }: { notice: NoticeType }) => { <NoticeDate notice={notice} /> </section> + <style jsx>{styles}</style> </section> ) diff --git a/src/components/PageHeader/index.tsx b/src/components/PageHeader/index.tsx index 0f9a850518..54b8073eb5 100644 --- a/src/components/PageHeader/index.tsx +++ b/src/components/PageHeader/index.tsx @@ -19,6 +19,7 @@ export const PageHeader: React.FC<PageHeaderProps> = ({ {children} + ) diff --git a/src/components/Placeholder/ArticleDetail.tsx b/src/components/Placeholder/ArticleDetail.tsx index fa882c6946..1fc2001efa 100644 --- a/src/components/Placeholder/ArticleDetail.tsx +++ b/src/components/Placeholder/ArticleDetail.tsx @@ -1,7 +1,7 @@ import React from 'react' import ContentLoader from 'react-content-loader' -import { Responsive } from '~/components' +import { useResponsive } from '~/components/Hook' import { LoaderProps } from './utils' @@ -23,28 +23,30 @@ const LG = () => ( ) -const ArticleDetail = () => ( - <> - - - +const ArticleDetail = () => { + const isXSmall = useResponsive({ type: 'xsmall' })() + const isSmall = useResponsive({ type: 'small' })() + const isMedium = useResponsive({ type: 'medium' })() + const isLarge = useResponsive({ type: 'large' })() + const isXLarge = useResponsive({ type: 'xlarge' })() - - - + if (isXSmall) { + return + } - - - + if (isSmall) { + return + } - - - + if (isMedium) { + return + } - - - - -) + if (isLarge || isXLarge) { + return + } + + return null +} export default ArticleDetail diff --git a/src/components/Placeholder/ArticleDigestList.tsx b/src/components/Placeholder/ArticleDigestList.tsx index 64946cf21d..68b1c64fae 100644 --- a/src/components/Placeholder/ArticleDigestList.tsx +++ b/src/components/Placeholder/ArticleDigestList.tsx @@ -1,7 +1,7 @@ import React from 'react' import ContentLoader from 'react-content-loader' -import { Responsive } from '~/components' +import { useResponsive } from '~/components/Hook' import { LoaderProps } from './utils' @@ -23,28 +23,30 @@ const LG: React.FC = props => ( ) -const ArticleDigestList = () => ( - <> - - - +const ArticleDigestList = () => { + const isXSmall = useResponsive({ type: 'xsmall' })() + const isSmall = useResponsive({ type: 'small' })() + const isMedium = useResponsive({ type: 'medium' })() + const isLarge = useResponsive({ type: 'large' })() + const isXLarge = useResponsive({ type: 'xlarge' })() - - - + if (isXSmall) { + return + } - - - + if (isSmall) { + return + } - - - + if (isMedium) { + return + } - - - - -) + if (isLarge || isXLarge) { + return + } + + return null +} export default ArticleDigestList diff --git a/src/components/Placeholder/MattersToday.tsx b/src/components/Placeholder/MattersToday.tsx index 75a1a5f384..f70b24cf2a 100644 --- a/src/components/Placeholder/MattersToday.tsx +++ b/src/components/Placeholder/MattersToday.tsx @@ -1,7 +1,7 @@ import React from 'react' import ContentLoader from 'react-content-loader' -import { Responsive } from '~/components' +import { useResponsive } from '~/components/Hook' import { LoaderProps } from './utils' @@ -23,28 +23,30 @@ const LG = () => ( ) -const MattersToday = () => ( - <> - - - +const MattersToday = () => { + const isXSmall = useResponsive({ type: 'xsmall' })() + const isSmall = useResponsive({ type: 'small' })() + const isMedium = useResponsive({ type: 'medium' })() + const isLarge = useResponsive({ type: 'large' })() + const isXLarge = useResponsive({ type: 'xlarge' })() - - - + if (isXSmall) { + return + } - - - + if (isSmall) { + return + } - - - + if (isMedium) { + return + } - - - - -) + if (isLarge || isXLarge) { + return + } + + return null +} export default MattersToday diff --git a/src/components/Placeholder/UserProfile.tsx b/src/components/Placeholder/UserProfile.tsx index 1afd5e16fb..08146caedc 100644 --- a/src/components/Placeholder/UserProfile.tsx +++ b/src/components/Placeholder/UserProfile.tsx @@ -1,7 +1,7 @@ import React from 'react' import ContentLoader from 'react-content-loader' -import { Responsive } from '~/components' +import { useResponsive } from '~/components/Hook' import { LoaderProps } from './utils' @@ -35,24 +35,29 @@ const LG = () => ( ) -const UserProfile = () => ( - <> - - - +const UserProfile = () => { + const isXSmall = useResponsive({ type: 'xsmall' })() + const isSmall = useResponsive({ type: 'small' })() + const isMedium = useResponsive({ type: 'medium' })() + const isLargeUp = useResponsive({ type: 'large-up' })() - - - + if (isXSmall) { + return + } - - - + if (isSmall) { + return + } - - - - -) + if (isMedium) { + return + } + + if (isLargeUp) { + return + } + + return null +} export default UserProfile diff --git a/src/components/Protected/index.tsx b/src/components/Protected/index.tsx index 90ad18b23a..cdac28e6e3 100644 --- a/src/components/Protected/index.tsx +++ b/src/components/Protected/index.tsx @@ -8,6 +8,12 @@ import { redirectToLogin } from '~/common/utils' export const Protected: React.FC = ({ children }) => { const viewer = useContext(ViewerContext) + useEffect(() => { + if (!viewer.isAuthed && process.browser) { + redirectToLogin() + } + }, []) + if (viewer.isAuthed) { return <>{children} } @@ -16,9 +22,5 @@ export const Protected: React.FC = ({ children }) => { return } - useEffect(() => { - redirectToLogin() - }, []) - return } diff --git a/src/components/Responsive/index.tsx b/src/components/Responsive/index.tsx deleted file mode 100644 index 01ec8ac77d..0000000000 --- a/src/components/Responsive/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import MediaQuery from 'react-responsive' - -import { BREAKPOINTS } from '~/common/enums' - -const SmallDown: React.FC = props => ( - -) -const MediumDown: React.FC = props => ( - -) - -const SmallUp: React.FC = props => ( - -) -const MediumUp: React.FC = props => ( - -) -const LargeUp: React.FC = props => ( - -) - -const XSmall = SmallDown -const Small: React.FC = props => ( - -) -const Medium: React.FC = props => ( - -) -const Large: React.FC = props => ( - -) -const XLarge: React.FC = props => ( - -) - -export const Responsive = { - SmallDown, - MediumDown, - - SmallUp, - MediumUp, - LargeUp, - - XSmall, - Small, - Medium, - Large, - XLarge -} diff --git a/src/components/SearchBar/AutoComplete/ClearHistoryButton.tsx b/src/components/SearchBar/AutoComplete/ClearHistoryButton.tsx index 820fb30954..ad1beb8009 100644 --- a/src/components/SearchBar/AutoComplete/ClearHistoryButton.tsx +++ b/src/components/SearchBar/AutoComplete/ClearHistoryButton.tsx @@ -1,10 +1,12 @@ import gql from 'graphql-tag' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Translate } from '~/components/Language' import { ADD_TOAST } from '~/common/enums' +import { ClearHistory } from './__generated__/ClearHistory' +import { ViewerRecentSearches } from './__generated__/ViewerRecentSearches' import styles from './styles.css' const fragments = { @@ -43,12 +45,11 @@ const VIEWER_RECENT_SEARCHES = gql` ${fragments.user} ` -const ClearHistoryButton = () => ( - { +const ClearHistoryButton = () => { + const [clear] = useMutation(CLEAR_HISTORY, { + update: cache => { try { - const data = cache.readQuery({ + const data = cache.readQuery({ query: VIEWER_RECENT_SEARCHES }) @@ -79,35 +80,34 @@ const ClearHistoryButton = () => ( } catch (e) { console.error(e) } - }} - > - {(clear: any) => ( - - )} - -) + } + }) + + return ( + + ) +} ClearHistoryButton.fragments = fragments diff --git a/src/components/SearchBar/AutoComplete/index.tsx b/src/components/SearchBar/AutoComplete/index.tsx index 6b929ae02d..7eb6b89116 100644 --- a/src/components/SearchBar/AutoComplete/index.tsx +++ b/src/components/SearchBar/AutoComplete/index.tsx @@ -1,10 +1,10 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import Link from 'next/link' -import { QueryResult } from 'react-apollo' +import { useEffect } from 'react' +import { useLazyQuery } from 'react-apollo' -import { Empty, Icon, Menu, Spinner, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { Empty, Icon, Menu, Translate } from '~/components' +import { Spinner } from '~/components/Spinner' import { TEXT } from '~/common/enums' import { toPath } from '~/common/utils' @@ -14,6 +14,12 @@ import { SearchAutoComplete } from './__generated__/SearchAutoComplete' import ClearHistoryButton from './ClearHistoryButton' import styles from './styles.css' +interface Props { + hideDropdown: () => void + isShown: boolean + searchKey?: string +} + const SEARCH_AUTOCOMPLETE = gql` query SearchAutoComplete($searchKey: String) { frequentSearch(input: { first: 5, key: $searchKey }) @@ -30,108 +36,114 @@ const EmptyAutoComplete = () => ( icon={ } - description="暫無搜尋歷史" + description={} size="small" /> ) -const AutoComplete = ({ - hideDropdown, - searchKey = '' -}: { - hideDropdown: () => void - searchKey?: string -}) => ( +const AutoComplete = ({ hideDropdown, searchKey = '', isShown }: Props) => { + const [getAutoComplete, { data, loading }] = useLazyQuery( + SEARCH_AUTOCOMPLETE, + { + variables: { searchKey } + } + ) + + useEffect(() => { + if (isShown) { + getAutoComplete() + } + }, [searchKey, isShown]) + + const frequentSearch = (data && data.frequentSearch) || [] + const recentSearches = + (data && data.viewer && data.viewer.activity.recentSearches.edges) || [] + const showFrequentSearch = frequentSearch.length > 0 + const showSearchHistory = !searchKey + + if (loading) { + return + } + + if (!showFrequentSearch && !showSearchHistory) { + return null + } + + return ( + + {showFrequentSearch && ( + <> + } + /> + + {frequentSearch.map(key => ( + + + + {key} + + + + ))} + + )} + + {showSearchHistory && ( + <> + + + + } + > + {recentSearches.length > 0 && } + + + {recentSearches.map(({ node }) => { + const path = toPath({ + page: 'search', + q: node + }) + return ( + + + + {node} + + + + ) + })} + + {recentSearches.length <= 0 && } + + )} + + ) +} + +export default (props: Props) => (
      - - {({ - data, - loading, - error - }: QueryResult & { data: SearchAutoComplete }) => { - if (loading) { - return - } - - const recentSearches = data.viewer.activity.recentSearches.edges - - return ( - (!searchKey || data.frequentSearch.length > 0) && ( - - {data.frequentSearch.length > 0 && ( - <> - } - /> - {data.frequentSearch.map((key: any) => { - const path = toPath({ - page: 'search', - q: key - }) - return ( - - - - {key} - - - - ) - })} - - )} - - {!searchKey && ( - <> - - - - } - > - {recentSearches.length > 0 && } - - {recentSearches.map(({ node }: { node: any }) => { - const path = toPath({ - page: 'search', - q: node - }) - return ( - - - - {node} - - - - ) - })} - {recentSearches.length <= 0 && } - - )} - - ) - ) - }} - + +
      ) - -export default AutoComplete diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index e58108bfeb..e346a1f123 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -1,10 +1,11 @@ import { Formik } from 'formik' import Router, { useRouter } from 'next/router' import { useContext, useState } from 'react' +import { useDebounce } from 'use-debounce' import { Dropdown, Icon, LanguageContext, PopperInstance } from '~/components' -import { TEXT } from '~/common/enums' +import { INPUT_DEBOUNCE, TEXT } from '~/common/enums' import { getQuery, toPath, translate } from '~/common/utils' import ICON_SEARCH from '~/static/icons/search.svg?sprite' @@ -21,9 +22,10 @@ const BaseSearchBar: React.FC<{ autoComplete?: boolean }> = ({ autoComplete = true }) => { const router = useRouter() - - // translations + const q = getQuery({ router, key: 'q' }) || '' const { lang } = useContext(LanguageContext) + const [search, setSearch] = useState('') + const [debouncedSearch] = useDebounce(search, INPUT_DEBOUNCE) const textAriaLabel = translate({ zh_hant: TEXT.zh_hant.search, zh_hans: TEXT.zh_hans.search, @@ -37,6 +39,7 @@ const BaseSearchBar: React.FC<{ // dropdown const [instance, setInstance] = useState(null) + const [shown, setShown] = useState(false) const hideDropdown = () => { if (instance) { instance.hide() @@ -44,18 +47,13 @@ const BaseSearchBar: React.FC<{ } const showDropdown = () => { if (instance) { - setTimeout(() => { - instance.show() - }, 100) // unknown bug, needs set a timeout + instance.show() } } - // parse query - const routerQ = getQuery({ router, key: 'q' }) - return ( { const path = toPath({ @@ -63,6 +61,7 @@ const BaseSearchBar: React.FC<{ q: values.q }) Router.push(path.href, path.as) + hideDropdown() }} render={({ values, handleSubmit, handleChange }) => { if (!autoComplete) { @@ -78,7 +77,9 @@ const BaseSearchBar: React.FC<{ onChange={handleChange} value={values.q} /> + + ) @@ -87,10 +88,15 @@ const BaseSearchBar: React.FC<{ return ( + } trigger="manual" onCreate={setInstance} + onShown={() => setShown(true)} theme="dropdown shadow-light" >
      @@ -103,13 +109,16 @@ const BaseSearchBar: React.FC<{ value={values.q} onChange={e => { handleChange(e) + setSearch(e.target.value) showDropdown() }} - onFocus={() => !values.q && showDropdown()} - onClick={() => !values.q && showDropdown()} + onFocus={showDropdown} + onClick={showDropdown} onBlur={hideDropdown} /> + +
      diff --git a/src/components/SetupLikeCoin/Binding/index.tsx b/src/components/SetupLikeCoin/Binding/index.tsx index 82748d69bc..c4f80faa8a 100644 --- a/src/components/SetupLikeCoin/Binding/index.tsx +++ b/src/components/SetupLikeCoin/Binding/index.tsx @@ -1,7 +1,6 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useState } from 'react' -import { Query, QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Translate } from '~/components/Language' import { Modal } from '~/components/Modal' @@ -9,6 +8,7 @@ import { Spinner } from '~/components/Spinner' import { TEXT } from '~/common/enums' +import { ViewerLikerId } from './__generated__/ViewerLikerId' import styles from './styles.css' interface Props { @@ -34,74 +34,67 @@ const Binding: React.FC = ({ scrollLock }) => { const [polling, setPolling] = useState(true) + const { data, error } = useQuery(VIEWER_LIKER_ID, { + pollInterval: polling ? 1000 : undefined, + errorPolicy: 'none', + fetchPolicy: 'network-only', + skip: !process.browser + }) + const likerId = data && data.viewer && data.viewer.likerId - return ( - - {({ data, loading, error }: QueryResult) => { - const likerId = _get(data, 'viewer.likerId') - - if (likerId) { - nextStep() + if (likerId) { + nextStep() - if (windowRef) { - setTimeout(() => { - windowRef.close() - }, 5000) - } - - return null - } + if (windowRef) { + setTimeout(() => { + windowRef.close() + }, 5000) + } - if (error) { - setPolling(false) - } + return null + } - return ( - <> - -
      - {!error && ( - <> - -

      - -

      - - )} - {error && ( -

      - -

      - )} -
      -
      + if (error) { + setPolling(false) + } -
      - + return ( + <> + +
      + {!error && ( + <> + +

      - -

      +

      + + )} + {error && ( +

      + +

      + )} +
    + + +
    + + + +
    - - - ) - }} - + + ) } diff --git a/src/components/SetupLikeCoin/Generating/index.tsx b/src/components/SetupLikeCoin/Generating/index.tsx index bbe80a1769..4625886c2a 100644 --- a/src/components/SetupLikeCoin/Generating/index.tsx +++ b/src/components/SetupLikeCoin/Generating/index.tsx @@ -1,14 +1,14 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useEffect } from 'react' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Translate } from '~/components/Language' import { Modal } from '~/components/Modal' import { Spinner } from '~/components/Spinner' import { TEXT } from '~/common/enums' +import { GenerateLikerId } from './__generated__/GenerateLikerId' import styles from './styles.css' interface Props { @@ -30,64 +30,57 @@ const GENERATE_LIKER_ID = gql` ` const Generating: React.FC = ({ prevStep, nextStep, scrollLock }) => { - return ( - - {(generate: any, { error }: any) => { - useEffect(() => { - generate().then((result: any) => { - const likerId = _get(result, 'data.generateLikerId.likerId') + const [generate, { error }] = useMutation(GENERATE_LIKER_ID) - if (likerId) { - nextStep() - return null - } - }) - }, []) + useEffect(() => { + generate().then(result => { + const likerId = + result && result.data && result.data.generateLikerId.likerId - return ( - <> - -
    - {!error && ( - <> - -

    - -

    - - )} - {error && ( -

    - -

    - )} -
    -
    + if (likerId) { + nextStep() + return null + } + }) + }, []) -
    - + return ( + <> + +
    + {!error && ( + <> + +

    - -

    +

    + + )} + {error && ( +

    + +

    + )} +
    +
    + +
    + + + +
    - - - ) - }} -
    + + ) } diff --git a/src/components/SetupLikeCoin/Select/index.tsx b/src/components/SetupLikeCoin/Select/index.tsx index 4b6a1a2411..530abd8270 100644 --- a/src/components/SetupLikeCoin/Select/index.tsx +++ b/src/components/SetupLikeCoin/Select/index.tsx @@ -31,6 +31,7 @@ const Header = () => ( zh_hans="接下来我们会帮你生成 Liker ID,如果你已经有 Liker ID 也可以进行绑定。" />

    + ) diff --git a/src/components/Spinner/index.tsx b/src/components/Spinner/index.tsx index a0fc51f9f2..df15f3efff 100644 --- a/src/components/Spinner/index.tsx +++ b/src/components/Spinner/index.tsx @@ -7,6 +7,7 @@ import styles from './styles.css' export const Spinner = () => (
    +
    ) diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx index ad34b2dc38..31b2306f97 100644 --- a/src/components/Switch/index.tsx +++ b/src/components/Switch/index.tsx @@ -18,7 +18,9 @@ export const Switch = ({ return ( ) diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx index 60a675d209..cd24aae256 100644 --- a/src/components/Tabs/index.tsx +++ b/src/components/Tabs/index.tsx @@ -19,6 +19,7 @@ const Tab: React.FC = ({ return (
  • {children} +
  • ) @@ -34,6 +35,7 @@ export const Tabs: React.FC & { return ( ) diff --git a/src/components/Tag/index.tsx b/src/components/Tag/index.tsx index 45b5cf3993..2cf92ac5e1 100644 --- a/src/components/Tag/index.tsx +++ b/src/components/Tag/index.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' import Link from 'next/link' import { Icon, TextIcon } from '~/components' @@ -52,31 +51,30 @@ export const Tag = ({ size = 'default', type = 'default', tag }: TagProps) => { page: 'tagDetail', id: tag.id }) - const tagCount = numAbbr(_get(tag, 'articles.totalCount', 0)) + const tagCount = numAbbr(tag.articles.totalCount || 0) return ( - <> - - - - } - text={tag.content} - weight="medium" - size={isSmall ? 'sm' : 'md'} - spacing={isSmall ? 'xtight' : 'tight'} - /> + + + + } + text={tag.content} + weight="medium" + size={isSmall ? 'sm' : 'md'} + spacing={isSmall ? 'xtight' : 'tight'} + /> - {!!tagCount && {tagCount}} - - - - + {!!tagCount && {tagCount}} + + + + ) } diff --git a/src/components/TextIcon/index.tsx b/src/components/TextIcon/index.tsx index 280a38ee1c..a527da2506 100644 --- a/src/components/TextIcon/index.tsx +++ b/src/components/TextIcon/index.tsx @@ -51,7 +51,9 @@ export const TextIcon: React.FC = ({ return ( {text || children} + {icon} + ) @@ -60,7 +62,9 @@ export const TextIcon: React.FC = ({ return ( {icon} + {text === undefined ? children : text} + ) diff --git a/src/components/Title/index.tsx b/src/components/Title/index.tsx index 575a1a2d6b..1534523669 100644 --- a/src/components/Title/index.tsx +++ b/src/components/Title/index.tsx @@ -65,6 +65,7 @@ export const Title: React.FC = ({ {children} )} + ) diff --git a/src/components/ToastHolder/index.tsx b/src/components/ToastHolder/index.tsx index 84be25f296..20ba365aa3 100644 --- a/src/components/ToastHolder/index.tsx +++ b/src/components/ToastHolder/index.tsx @@ -1,5 +1,5 @@ import _filter from 'lodash/filter' -import { FC, useState } from 'react' +import { useState } from 'react' import { useEventListener } from '~/components' @@ -25,7 +25,7 @@ interface Props { layoutClasses?: string } -export const ToastHolder: FC = ({ +export const ToastHolder: React.FC = ({ layoutClasses = 'l-col-4 l-col-md-6 l-offset-md-1 l-col-lg-8 l-offset-lg-2' }) => { const [toasts, setToasts] = useState([]) @@ -49,17 +49,16 @@ export const ToastHolder: FC = ({ useEventListener('removeToast', remove) return ( - <> -
    -
    -
    - {toasts.map(toast => ( - - ))} -
    +
    +
    +
    + {toasts.map(toast => ( + + ))}
    + - +
    ) } diff --git a/src/components/TransactionDigest/AppreciationReceived/index.tsx b/src/components/TransactionDigest/AppreciationReceived/index.tsx index abb68713b7..b83debaa16 100644 --- a/src/components/TransactionDigest/AppreciationReceived/index.tsx +++ b/src/components/TransactionDigest/AppreciationReceived/index.tsx @@ -77,6 +77,7 @@ const AppreciationReceived = ({ + ) diff --git a/src/components/TransactionDigest/AppreciationSent/index.tsx b/src/components/TransactionDigest/AppreciationSent/index.tsx index 6aa11a5f4d..b4da72e3af 100644 --- a/src/components/TransactionDigest/AppreciationSent/index.tsx +++ b/src/components/TransactionDigest/AppreciationSent/index.tsx @@ -65,6 +65,7 @@ const AppreciationSent = ({ tx }: { tx: AppreciationSentTransaction }) => { + ) diff --git a/src/components/UserDigest/FullDesc/index.tsx b/src/components/UserDigest/FullDesc/index.tsx index 794182d2e5..d6fa8d9802 100644 --- a/src/components/UserDigest/FullDesc/index.tsx +++ b/src/components/UserDigest/FullDesc/index.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { Icon, TextIcon } from '~/components' import { Avatar } from '~/components/Avatar' +import UnblockButton from '~/components/Button/BlockUser/Unblock' import { FollowButton } from '~/components/Button/Follow' import { numAbbr, toPath } from '~/common/utils' @@ -49,12 +50,14 @@ const FullDesc = ({ user, nameSize = 'default', readonly, - appreciations + appreciations, + showUnblock }: { user: UserDigestFullDescUser nameSize?: 'default' | 'small' readonly?: boolean appreciations?: number + showUnblock?: boolean }) => { const showAppreciations = appreciations && appreciations > 0 const nameSizeClasses = classNames({ @@ -87,11 +90,12 @@ const FullDesc = ({ {showAppreciations && } - + {!showUnblock && }
    - + {showUnblock && } + {!showUnblock && }
    @@ -102,6 +106,7 @@ const FullDesc = ({ + ) @@ -119,10 +124,12 @@ FullDesc.fragments = { ...AvatarUser ...FollowStateUser ...FollowButtonUser + ...UnblockButtonUser } ${Avatar.fragments.user} ${FollowButton.State.fragments.user} ${FollowButton.fragments.user} + ${UnblockButton.fragments.user} ` } diff --git a/src/components/UserDigest/Mini/index.tsx b/src/components/UserDigest/Mini/index.tsx index a665401889..ef16f2b225 100644 --- a/src/components/UserDigest/Mini/index.tsx +++ b/src/components/UserDigest/Mini/index.tsx @@ -68,6 +68,7 @@ const Mini = ({ + ) diff --git a/src/components/UserProfile/Cover.tsx b/src/components/UserProfile/Cover.tsx index f84374a906..31215c851f 100644 --- a/src/components/UserProfile/Cover.tsx +++ b/src/components/UserProfile/Cover.tsx @@ -8,6 +8,7 @@ export const ProfileCover = ({ cover }: { cover: string | null }) => ( className="profile-cover" style={{ backgroundImage: `url(${cover || IMAGE_COVER})` }} /> + ) diff --git a/src/components/UserProfile/DropdownActions/index.tsx b/src/components/UserProfile/DropdownActions/index.tsx new file mode 100644 index 0000000000..4fd4965fb1 --- /dev/null +++ b/src/components/UserProfile/DropdownActions/index.tsx @@ -0,0 +1,63 @@ +import gql from 'graphql-tag' +import { useState } from 'react' + +import { Dropdown, Icon, Menu, PopperInstance } from '~/components' +import BlockUserButton from '~/components/Button/BlockUser/Dropdown' + +import ICON_MORE_SMALL from '~/static/icons/more-small.svg?sprite' + +import { DropdownActionsUser } from './__generated__/DropdownActionsUser' + +const fragments = { + user: gql` + fragment DropdownActionsUser on User { + id + ...BlockUser + } + ${BlockUserButton.fragments.user} + ` +} + +const DropdownActions = ({ user }: { user: DropdownActionsUser }) => { + const [shown, setShown] = useState(false) + const [instance, setInstance] = useState(null) + const hideDropdown = () => { + if (!instance) { + return + } + instance.hide() + } + + return ( + + + + + + } + trigger="click" + onCreate={setInstance} + onShown={() => setShown(true)} + placement="bottom-end" + zIndex={301} + > + + + ) +} + +DropdownActions.fragments = fragments + +export default DropdownActions diff --git a/src/components/UserProfile/EditProfileButton.tsx b/src/components/UserProfile/EditProfileButton.tsx index 2a15becaa8..e2e1ca4f56 100644 --- a/src/components/UserProfile/EditProfileButton.tsx +++ b/src/components/UserProfile/EditProfileButton.tsx @@ -1,5 +1,3 @@ -import _get from 'lodash/get' - import { Icon, TextIcon, Translate } from '~/components' import ICON_SETTINGS from '~/static/icons/settings.svg?sprite' diff --git a/src/components/UserProfile/index.tsx b/src/components/UserProfile/index.tsx index 751be3e7ff..20ed604932 100644 --- a/src/components/UserProfile/index.tsx +++ b/src/components/UserProfile/index.tsx @@ -1,17 +1,18 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import { get, some } from 'lodash' +import _get from 'lodash/get' +import _some from 'lodash/some' import Link from 'next/link' import { useRouter } from 'next/router' import { useContext, useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Avatar, Placeholder, Tooltip, Translate } from '~/components' import { FollowButton } from '~/components/Button/Follow' -import { Query } from '~/components/GQL' +import ShareButton from '~/components/Button/Share' +import ShareModal from '~/components/Button/Share/ShareModal' import { Icon } from '~/components/Icon' -import ShareButton from '~/components/ShareButton' -import ShareModal from '~/components/ShareButton/ShareModal' +import Throw404 from '~/components/Throw404' import { UserProfileEditor } from '~/components/UserProfileEditor' import { ViewerContext } from '~/components/Viewer' @@ -19,10 +20,11 @@ import { TEXT } from '~/common/enums' import { getQuery, numAbbr, toPath } from '~/common/utils' import ICON_SEED_BADGE from '~/static/icons/early-user-badge.svg?sprite' -import Throw404 from '../Throw404' +import { MeProfileUser } from './__generated__/MeProfileUser' import { UserProfileUser } from './__generated__/UserProfileUser' import Cover from './Cover' import Description from './Description' +import DropdownActions from './DropdownActions' import EditProfileButton from './EditProfileButton' import styles from './styles.css' @@ -50,9 +52,11 @@ const fragments = { } ...AvatarUser ...FollowButtonUser @skip(if: $isMe) + ...DropdownActionsUser } ${Avatar.fragments.user} ${FollowButton.fragments.user} + ${DropdownActions.fragments.user} ` } @@ -86,17 +90,14 @@ const SeedBadge = () => ( ) -const CoverContainer = ({ children }: any) => ( - <> -
    -
    {children}
    -
    +const CoverContainer: React.FC = ({ children }) => ( +
    +
    {children}
    + - +
    ) -type UserProfileResultType = QueryResult & { data: UserProfileUser } - const BaseUserProfile = () => { const router = useRouter() const viewer = useContext(ViewerContext) @@ -110,154 +111,183 @@ const BaseUserProfile = () => { inactive: isMe && viewer.isInactive }) + const { data, loading } = useQuery( + isMe ? ME_PROFILE : USER_PROFILE, + { + variables: isMe ? {} : { userName } + } + ) + const user = isMe ? _get(data, 'viewer') : _get(data, 'user') + + if (loading) { + return ( +
    + + + + + +
    + ) + } + + if (isMe && editing) { + return ( +
    + + + +
    + ) + } + + if (!user) { + return + } + + const userFollowersPath = toPath({ + page: 'userFollowers', + userName: user.userName + }) + const userFolloweesPath = toPath({ + page: 'userFollowees', + userName: user.userName + }) + const badges = user.info.badges || [] + const hasSeedBadge = _some(badges, { type: 'seed' }) + const profileCover = user.info.profileCover || '' + return (
    - - {({ data, loading, error }: UserProfileResultType) => { - if (loading) { - return ( - - - - ) - } - - if (isMe && editing) { - return ( - - ) - } - - const user = isMe ? data.viewer : data.user - - if (!user) { - return - } - - const userFollowersPath = toPath({ - page: 'userFollowers', - userName: user.userName - }) - const userFolloweesPath = toPath({ - page: 'userFollowees', - userName: user.userName - }) - const badges = get(user, 'info.badges', []) - const hasSeedBadge = some(badges, { type: 'seed' }) - const profileCover = get(user, 'info.profileCover', '') - - return ( - <> - - - -
    -
    -
    -
    - + + + +
    +
    +
    +
    + + + {!isMe && ( +
    + + + + {!isMe && } + + + + + + + +
    + )} +
    + +
    +
    +
    + {!viewer.isInactive && ( + <> + {user.displayName} + @{user.userName} + {hasSeedBadge && } + + {!isMe && } + + + )} + + {viewer.isArchived && ( + + + + )} + + {viewer.isFrozen && ( + + + + )} + + {viewer.isBanned && ( + + - {!isMe && ( -
    - - - - -
    - )} -
    - -
    -
    -
    - {!viewer.isInactive && ( - - {user.displayName} - {hasSeedBadge && } - {!isMe && } - - )} - {viewer.isArchived && ( - - - - )} - {viewer.isFrozen && ( - - - - )} - {viewer.isBanned && ( - - - - )} -
    -
    - {isMe && !viewer.isInactive && ( - - )} - - - -
    -
    - - {!viewer.isInactive && ( - - )} -
    - - - - {numAbbr(user.followers.totalCount)} - - - - - - - - {numAbbr(user.followees.totalCount)} - - - - -
    -
    -
    + + )}
    -
    - - ) - }} -
    - +
    + {isMe && !viewer.isInactive && ( + + )} + + + {!isMe && } + + +
    + + + {!viewer.isInactive && ( + + )} + +
    + + + + {numAbbr(user.followers.totalCount)} + + + + + + + + + {numAbbr(user.followees.totalCount)} + + + + +
    +
    + + +
    ) } -export const UserProfile = BaseUserProfile +export const UserProfile = () => ( + <> + + + +) diff --git a/src/components/UserProfile/styles.css b/src/components/UserProfile/styles.css index eb348034ef..799ee61aaf 100644 --- a/src/components/UserProfile/styles.css +++ b/src/components/UserProfile/styles.css @@ -75,31 +75,35 @@ @mixin flex-start-space-between; } -.name { - font-size: var(--font-size-lg); - font-weight: var(--font-weight-bold); +.basic { + @mixin flex-center-all; + margin-left: var(--spacing-x-tight); - @media (--sm-up) { - font-size: var(--font-size-xl); - line-height: 1.25; + & .name { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + + @media (--sm-up) { + font-size: var(--font-size-xl); + line-height: 1.25; + } } - & > span { - display: inline-flex; - align-items: center; + & .username { margin-left: var(--spacing-x-tight); - & :first-child { - margin-left: 0; - } + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + color: var(--color-grey); + } - & :global(> *) { - margin-left: var(--spacing-x-tight); - } + & :first-child { + margin-left: 0; + } - & :gloabl(> .btn) { - margin-top: 1px; - } + & :global(> * + *) { + font-size: 0; + margin-left: var(--spacing-x-tight); } } @@ -108,15 +112,27 @@ top: -12px; /* position hack in small device */ right: 0; + & :global(button) { + margin-left: var(--spacing-tight); + } + + & .follows { + & :global(button) { + margin: 0; + } + + & .follow-state { + display: inline-block; + margin-top: var(--spacing-tight); + line-height: 1; + } + } + @media (--sm-up) { position: initial; top: initial; right: initial; } - - & > :global(*) + :global(*) { - margin-left: var(--spacing-tight); - } } .description { diff --git a/src/components/UserProfileEditor/index.tsx b/src/components/UserProfileEditor/index.tsx index 1a88ed9881..e0a8ede1fd 100644 --- a/src/components/UserProfileEditor/index.tsx +++ b/src/components/UserProfileEditor/index.tsx @@ -1,7 +1,7 @@ -import { withFormik } from 'formik' +import { FormikProps, withFormik } from 'formik' import gql from 'graphql-tag' import _isEmpty from 'lodash/isEmpty' -import { FC, useContext } from 'react' +import { useContext } from 'react' import { Button } from '~/components/Button' import { @@ -9,7 +9,7 @@ import { ProfileCoverUploader } from '~/components/FileUploader' import { Form } from '~/components/Form' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Icon } from '~/components/Icon' import IconSpinner from '~/components/Icon/Spinner' import { LanguageContext, Translate } from '~/components/Language' @@ -19,13 +19,19 @@ import { TEXT } from '~/common/enums' import { isValidDisplayName, translate } from '~/common/utils' import ICON_SAVE from '~/static/icons/write.svg?sprite' +import { UpdateUserInfoProfile } from './__generated__/UpdateUserInfoProfile' import styles from './styles.css' -interface Props { +interface FormProps { user: { [key: string]: any } setEditing: (value: boolean) => void } +interface FormValues { + displayName: string + description: string +} + const UPDATE_USER_INFO = gql` mutation UpdateUserInfoProfile($input: UpdateUserInfoInput!) { updateUserInfo(input: $input) { @@ -38,12 +44,14 @@ const UPDATE_USER_INFO = gql` } ` -export const UserProfileEditor: FC = ({ user, setEditing }) => { +export const UserProfileEditor: React.FC = formProps => { + const [update] = useMutation(UPDATE_USER_INFO) const { lang } = useContext(LanguageContext) const viewer = useContext(ViewerContext) + const { user, setEditing } = formProps - const validateDisplayName = (value: string, language: string) => { - let result: any + const validateDisplayName = (value: string) => { + let result if (!value) { result = { zh_hant: TEXT.zh_hant.required, @@ -56,12 +64,12 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const validateDescription = (value: string, language: Language) => { - let result: any + const validateDescription = (value: string) => { + let result if (value && value.length > 200) { result = { zh_hant: `已超過 200 字,目前 ${value.length} 字`, @@ -69,11 +77,11 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { } } if (result) { - return translate({ ...result, lang: language }) + return translate({ ...result, lang }) } } - const BaseForm = ({ + const InnerForm = ({ values, errors, touched, @@ -81,9 +89,7 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { handleBlur, handleChange, handleSubmit - }: { - [key: string]: any - }) => { + }: FormikProps) => { const displayNamePlaceholder = translate({ zh_hant: '輸入姓名', zh_hans: '输入姓名', @@ -111,77 +117,76 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { }) return ( - <> -
    - - -
    - - -
    - +
    + + +
    + + +
    + - + ) } - const MainForm: any = withFormik({ + const MainForm = withFormik({ mapPropsToValues: () => ({ displayName: user.displayName, description: user.info.description }), validate: ({ displayName, description }) => { - const inInvalidDisplayName = validateDisplayName(displayName, lang) - const isInvalidDescription = validateDescription(description, lang) + const inInvalidDisplayName = validateDisplayName(displayName) + const isInvalidDescription = validateDescription(description) const errors = { ...(inInvalidDisplayName ? { displayName: inInvalidDisplayName } : {}), ...(isInvalidDescription ? { description: isInvalidDescription } : {}) @@ -189,27 +194,22 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { return errors }, - handleSubmit: (values, { props, setSubmitting }: any) => { + handleSubmit: async (values, { setSubmitting }) => { const { displayName, description } = values - const { submitAction } = props - if (!submitAction) { - return + + try { + await update({ variables: { input: { displayName, description } } }) + + if (setEditing) { + setEditing(false) + } + } catch (error) { + // TODO: Handle error } - submitAction({ variables: { input: { displayName, description } } }) - .then(({ data }: any) => { - if (setEditing) { - setEditing(false) - } - }) - .catch((result: any) => { - // TODO: Handle error - }) - .finally(() => { - setSubmitting(false) - }) + setSubmitting(false) } - })(BaseForm) + })(InnerForm) return ( <> @@ -218,18 +218,18 @@ export const UserProfileEditor: FC = ({ user, setEditing }) => { +
    - - {(update: any) => } - +
    + ) diff --git a/src/components/Viewer/index.tsx b/src/components/Viewer/index.tsx index afa5b789b8..9bb4c6aa58 100644 --- a/src/components/Viewer/index.tsx +++ b/src/components/Viewer/index.tsx @@ -1,6 +1,5 @@ import * as Sentry from '@sentry/browser' import gql from 'graphql-tag' -import _get from 'lodash/get' import React from 'react' import { ViewerUser } from './__generated__/ViewerUser' @@ -41,12 +40,13 @@ type Viewer = ViewerUser & { isOnboarding: boolean isInactive: boolean isAdmin: boolean + shouldSetupLikerID: boolean } export const processViewer = (viewer: ViewerUser): Viewer => { const isAuthed = !!viewer.id - const state = _get(viewer, 'status.state') - const role = _get(viewer, 'status.role') + const state = viewer && viewer.status && viewer.status.state + const role = viewer && viewer.status && viewer.status.role const isActive = state === 'active' const isFrozen = state === 'frozen' const isBanned = state === 'banned' @@ -54,13 +54,14 @@ export const processViewer = (viewer: ViewerUser): Viewer => { const isOnboarding = state === 'onboarding' const isInactive = isAuthed && (isFrozen || isBanned || isArchived) const isAdmin = role === 'admin' + const shouldSetupLikerID = isOnboarding || !viewer.likerId // Add user info for Sentry Sentry.configureScope((scope: any) => { scope.setUser({ id: viewer.id, role, - language: _get(viewer, 'settings.language') + language: viewer.settings.language }) scope.setTag('source', 'web') }) @@ -74,7 +75,8 @@ export const processViewer = (viewer: ViewerUser): Viewer => { isFrozen, isOnboarding, isInactive, - isAdmin + isAdmin, + shouldSetupLikerID } } diff --git a/src/components/index.tsx b/src/components/index.tsx index ddf98ea108..018e52e24b 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -2,7 +2,6 @@ export * from './Head' export * from './Layout' export * from './GlobalHeader' export * from './GlobalStyles' -export * from './Responsive' export * from './Placeholder' export * from './Spinner' export * from './DateTime' @@ -38,4 +37,3 @@ export * from './Tabs' export * from './UserProfile' export * from './DraftDigest' export * from './CommentDigest' -export * from './Drawer' diff --git a/src/pages/MeSettingsBlocked.tsx b/src/pages/MeSettingsBlocked.tsx new file mode 100644 index 0000000000..10e383a3a4 --- /dev/null +++ b/src/pages/MeSettingsBlocked.tsx @@ -0,0 +1,9 @@ +import MeSettingsBlocked from '~/views/Me/Settings/Blocked' + +import { Protected } from '~/components/Protected' + +export default () => ( + + + +) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 010d86848b..3803f3564f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,10 @@ import * as Sentry from '@sentry/browser' import { InMemoryCache } from 'apollo-cache-inmemory' import { ApolloClient } from 'apollo-client' -import gql from 'graphql-tag' import App from 'next/app' import getConfig from 'next/config' import React from 'react' -import { ApolloProvider, QueryResult } from 'react-apollo' +import { ApolloProvider } from 'react-apollo' import { AnalyticsProvider, @@ -14,22 +13,9 @@ import { ModalProvider } from '~/components' import ErrorBoundary from '~/components/ErrorBoundary' -import { Query } from '~/components/GQL' import withApollo from '~/common/utils/withApollo' -import { RootQuery } from './__generated__/RootQuery' - -const ROOT_QUERY = gql` - query RootQuery { - viewer { - id - ...LayoutUser - } - } - ${Layout.fragments.user} -` - // start Sentry const { publicRuntimeConfig: { SENTRY_DSN } @@ -47,21 +33,10 @@ class MattersApp extends App<{ apollo: ApolloClient }> { - - {({ - data, - loading, - error - }: QueryResult & { data: RootQuery }) => ( - - - - )} - + + + + diff --git a/src/views/ArticleDetail/Collection/CollectionList.tsx b/src/views/ArticleDetail/Collection/CollectionList.tsx index e45d7d25e8..0fd43a6e66 100644 --- a/src/views/ArticleDetail/Collection/CollectionList.tsx +++ b/src/views/ArticleDetail/Collection/CollectionList.tsx @@ -1,10 +1,10 @@ import gql from 'graphql-tag' import _get from 'lodash/get' import _uniq from 'lodash/uniq' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, Icon, Spinner, TextIcon, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import articleFragments from '~/components/GQL/fragments/article' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' @@ -41,115 +41,114 @@ const CollectionList = ({ article: ArticleDetail_article setEditing: (editing: boolean) => void canEdit?: boolean -}) => ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: CollectionListTypes }) => { - if (loading) { - return - } +}) => { + const { data, loading, error, fetchMore } = useQuery( + COLLECTION_LIST, + { + variables: { mediaHash: article.mediaHash, first: 10 } + } + ) + + const connectionPath = 'article.collection' + const { edges, pageInfo, totalCount } = + (data && data.article && data.article.collection) || {} + + if (loading) { + return + } - const path = 'article.collection' - const { edges, pageInfo, totalCount } = _get(data, path, {}) - const loadRest = () => - fetchMore({ - variables: { - mediaHash: article.mediaHash, - after: pageInfo.endCursor, - first: null - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path - }) + if (error) { + return + } + + if (!edges || !pageInfo) { + return null + } + + const loadRest = () => + fetchMore({ + variables: { + mediaHash: article.mediaHash, + after: pageInfo.endCursor, + first: null + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath }) + }) + + if ((totalCount || 0) <= 0 && canEdit) { + return ( + + ) + } - if (totalCount <= 0 && canEdit) { - return ( - - ) - } - - return ( - <> -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.COLLECTION, - location: i - }) - } - > - -
    • - ) - )} -
    - - {pageInfo.hasNextPage && ( -
    - -
    - )} + + )} - - - ) - }} -
    -) + + + ) +} export default CollectionList diff --git a/src/views/ArticleDetail/Collection/EditButton.tsx b/src/views/ArticleDetail/Collection/EditButton.tsx index 55935994c5..e33a5922b4 100644 --- a/src/views/ArticleDetail/Collection/EditButton.tsx +++ b/src/views/ArticleDetail/Collection/EditButton.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' import _uniq from 'lodash/uniq' import { useContext } from 'react' @@ -11,7 +10,7 @@ import { TextIcon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import articleFragments from '~/components/GQL/fragments/article' import IconSpinner from '~/components/Icon/Spinner' @@ -21,6 +20,7 @@ import ICON_EDIT from '~/static/icons/collection-edit.svg?sprite' import ICON_SAVE from '~/static/icons/pen.svg?sprite' import { ArticleDetail_article } from '../__generated__/ArticleDetail' +import { EditorSetCollection } from './__generated__/EditorSetCollection' import styles from './styles.css' /** @@ -62,6 +62,9 @@ const EditButton = ({ setEditing: any editingArticles: string[] }) => { + const [setCollection, { loading }] = useMutation( + EDITOR_SET_COLLECTION + ) const { lang } = useContext(LanguageContext) const editButtonClass = classNames({ 'edit-button': true @@ -75,88 +78,80 @@ const EditButton = ({ + ) } return ( - - {(setCollection: any, { loading }: any) => ( - - + + - + }) + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'green', + content: translate({ + zh_hant: '關聯已更新', + zh_hans: '关联已更新', + lang + }), + closeButton: true, + duration: 2000 + } + }) + ) + } catch (error) { + window.dispatchEvent( + new CustomEvent(ADD_TOAST, { + detail: { + color: 'red', + content: translate({ + zh_hant: '關聯失敗', + zh_hans: '关联失敗', + lang + }), + clostButton: true, + duration: 2000 + } + }) + ) + } + setEditing(false) + }} + outlineColor="green" + > + + - - - )} - + + ) } diff --git a/src/views/ArticleDetail/Collection/EditingList.tsx b/src/views/ArticleDetail/Collection/EditingList.tsx index d461929b62..61fec49746 100644 --- a/src/views/ArticleDetail/Collection/EditingList.tsx +++ b/src/views/ArticleDetail/Collection/EditingList.tsx @@ -1,16 +1,18 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import _uniqBy from 'lodash/uniqBy' import dynamic from 'next/dynamic' import { useEffect } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Spinner } from '~/components' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import articleFragments from '~/components/GQL/fragments/article' import { ArticleDetail_article } from '../__generated__/ArticleDetail' -import { EditorCollection } from './__generated__/EditorCollection' +import { + EditorCollection, + EditorCollection_article_collection_edges_node +} from './__generated__/EditorCollection' import styles from './styles.css' const EDITOR_COLLECTION = gql` @@ -36,41 +38,43 @@ const EditingList = ({ setEditingArticles }: { article: ArticleDetail_article - editingArticles: any[] - setEditingArticles: (articles: any[]) => void -}) => ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: EditorCollection }) => { - if (loading) { - return - } + 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, first: null } + } + ) + const edges = (data && data.article && data.article.collection.edges) || [] - const { edges } = _get(data, 'article.collection', {}) + // init `editingArticles` when network collection is received + const edgesKeys = edges.map(({ node }) => node.id).join(',') || '' + useEffect(() => { + setEditingArticles(edges.map(({ node }) => node)) + }, [edgesKeys]) - useEffect(() => { - setEditingArticles(edges.map(({ node }: { node: any }) => node)) - }, []) + if (loading) { + return + } - return ( -
    - setEditingArticles(_uniqBy(articles, 'id'))} - /> + 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 58454a6bdd..5494b16c34 100644 --- a/src/views/ArticleDetail/Collection/index.tsx +++ b/src/views/ArticleDetail/Collection/index.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import _uniq from 'lodash/uniq' import { useState } from 'react' @@ -27,6 +26,7 @@ const Collection: React.FC<{ }> = ({ article, collectionCount, canEdit }) => { const [editing, setEditing] = useState(false) const [editingArticles, setEditingArticles] = useState([]) + return (
    diff --git a/src/views/ArticleDetail/Content/index.tsx b/src/views/ArticleDetail/Content/index.tsx index cd6db3ff4d..3e81d25d62 100644 --- a/src/views/ArticleDetail/Content/index.tsx +++ b/src/views/ArticleDetail/Content/index.tsx @@ -2,13 +2,22 @@ import gql from 'graphql-tag' import { useEffect, useState } from 'react' import { Waypoint } from 'react-waypoint' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { ANALYTICS_EVENTS } from '~/common/enums' import styles from '~/common/styles/utils/content.article.css' import { analytics, initAudioPlayers } from '~/common/utils' import { ContentArticle } from './__generated__/ContentArticle' +import { ReadArticle } from './__generated__/ReadArticle' + +const READ_ARTICLE = gql` + mutation ReadArticle($id: ID!) { + readArticle(input: { id: $id }) { + id + } + } +` const fragments = { article: gql` @@ -20,6 +29,9 @@ const fragments = { } const Content = ({ article }: { article: ContentArticle }) => { + const [read] = useMutation(READ_ARTICLE) + const [trackedFinish, setTrackedFinish] = useState(false) + const [trackedRead, setTrackedRead] = useState(false) const { id } = article useEffect(() => { @@ -56,9 +68,6 @@ const Content = ({ article }: { article: ContentArticle }) => { initAudioPlayers() }) - const [trackedFinish, setTrackedFinish] = useState(false) - const [trackedRead, setTrackedRead] = useState(false) - const FireOnMount = ({ fn }: { fn: () => void }) => { useEffect(() => { fn() @@ -67,43 +76,34 @@ const Content = ({ article }: { article: ContentArticle }) => { } return ( - + { + if (!trackedRead) { + read({ variables: { id } }) + setTrackedRead(true) } - } - `} - > - {(read: any) => ( - <> - { - if (!trackedRead) { - read({ variables: { id } }) - setTrackedRead(true) - } - }} - /> -
    - { - if (!trackedFinish) { - analytics.trackEvent(ANALYTICS_EVENTS.FINISH_ARTICLE, { - entrance: id - }) - setTrackedFinish(true) - } - }} - /> - - - )} - + }} + /> + +
    + + { + if (!trackedFinish) { + analytics.trackEvent(ANALYTICS_EVENTS.FINISH_ARTICLE, { + entrance: id + }) + setTrackedFinish(true) + } + }} + /> + + + ) } diff --git a/src/views/ArticleDetail/RelatedArticles/index.tsx b/src/views/ArticleDetail/RelatedArticles/index.tsx index 39250ad69f..dda66d493c 100644 --- a/src/views/ArticleDetail/RelatedArticles/index.tsx +++ b/src/views/ArticleDetail/RelatedArticles/index.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { ArticleDigest } from '~/components' @@ -27,7 +26,8 @@ const fragments = { } const RelatedArticles = ({ article }: { article: RelatedArticlesType }) => { - const edges = _get(article, 'relatedArticles.edges') + const edges = article.relatedArticles.edges + if (!edges || edges.length <= 0) { return null } @@ -36,23 +36,22 @@ const RelatedArticles = ({ article }: { article: RelatedArticlesType }) => {
    - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.RELATED_ARTICLE, - location: i, - entrance: article.id - }) - } - > - -
    - ) - )} + {edges.map(({ node, cursor }, i) => ( +
    + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.RELATED_ARTICLE, + location: i, + entrance: article.id + }) + } + > + +
    + ))}
    +
    ) diff --git a/src/views/ArticleDetail/Responses/FeaturedComments.tsx b/src/views/ArticleDetail/Responses/FeaturedComments.tsx index 0a3c931326..1dec8b0c1a 100644 --- a/src/views/ArticleDetail/Responses/FeaturedComments.tsx +++ b/src/views/ArticleDetail/Responses/FeaturedComments.tsx @@ -1,18 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import _has from 'lodash/has' -import _merge from 'lodash/merge' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { LoadMore, Spinner, Translate } from '~/components' import { CommentDigest } from '~/components/CommentDigest' -import { Query } from '~/components/GQL' import commentFragments from '~/components/GQL/fragments/comment' import { TEXT } from '~/common/enums' import { filterComments, getQuery, mergeConnections } from '~/common/utils' +import { ArticleFeaturedComments } from './__generated__/ArticleFeaturedComments' import styles from './styles.css' const FEATURED_COMMENTS = gql` @@ -46,80 +43,66 @@ const FEATURED_COMMENTS = gql` const FeaturedComments = () => { const router = useRouter() const mediaHash = getQuery({ router, key: 'mediaHash' }) + const { data, loading, fetchMore } = useQuery( + FEATURED_COMMENTS, + { + variables: { mediaHash }, + notifyOnNetworkStatusChange: true + } + ) - if (!mediaHash) { - return null + const connectionPath = 'article.featuredComments' + const { edges, pageInfo } = + (data && data.article && data.article.featuredComments) || {} + const comments = filterComments((edges || []).map(({ node }) => node)) + + if (loading) { + return } - return ( - - {({ data, loading, fetchMore }: QueryResult) => { - if (!data || !data.article) { - return - } + if (!edges || edges.length <= 0 || !pageInfo || comments.length <= 0) { + return null + } - const connectionPath = 'article.featuredComments' - const { edges, pageInfo } = _get(data, connectionPath, { - edges: {}, - pageInfo: {} + const loadMore = () => { + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath }) + }) + } - const loadMore = () => { - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - - const comments = filterComments( - (edges || []).map(({ node }: { node: any }) => node) - ) - - if (!comments || comments.length <= 0) { - return null - } - - return ( - - ) - }} - + return ( + ) } diff --git a/src/views/ArticleDetail/Responses/LatestResponses.tsx b/src/views/ArticleDetail/Responses/LatestResponses.tsx index ef64555d91..8f0dcf8a78 100644 --- a/src/views/ArticleDetail/Responses/LatestResponses.tsx +++ b/src/views/ArticleDetail/Responses/LatestResponses.tsx @@ -5,13 +5,13 @@ import _has from 'lodash/has' import _merge from 'lodash/merge' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { LoadMore, Spinner, Translate } from '~/components' import { ArticleDigest } from '~/components/ArticleDigest' import { CommentDigest } from '~/components/CommentDigest' import EmptyResponse from '~/components/Empty/EmptyResponse' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { ArticleDetailResponses } from '~/components/GQL/fragments/response' import { ArticleResponses as ArticleResponsesType } from '~/components/GQL/queries/__generated__/ArticleResponses' import ARTICLE_RESPONSES from '~/components/GQL/queries/articleResponses' @@ -27,6 +27,7 @@ import { unshiftConnections } from '~/common/utils' +import { ArticleCommentAdded } from './__generated__/ArticleCommentAdded' import styles from './styles.css' const RESPONSES_COUNT = 15 @@ -60,11 +61,7 @@ const LatestResponses = () => { const router = useRouter() const mediaHash = getQuery({ router, key: 'mediaHash' }) const [articleOnlyMode, setArticleOnlyMode] = useState(false) - const [storedCursor, setStoredCursor] = useState(null) - - if (!mediaHash) { - return - } + const [storedCursor, setStoredCursor] = useState(null) /** * Fragment Patterns @@ -82,223 +79,207 @@ const LatestResponses = () => { descendantId = fragment.split('-')[1] } - const queryVariables = { - mediaHash, - first: RESPONSES_COUNT, - articleOnly: articleOnlyMode - } + const { + data, + loading, + error, + fetchMore, + subscribeToMore, + refetch + } = useQuery(ARTICLE_RESPONSES, { + variables: { + mediaHash, + first: RESPONSES_COUNT, + articleOnly: articleOnlyMode + }, + notifyOnNetworkStatusChange: true + }) + const connectionPath = 'article.responses' + const article = data && data.article + const { edges, pageInfo } = (article && article.responses) || {} + const articleId = article && article.id - return ( - - {({ - data, - loading, - fetchMore, - subscribeToMore, - refetch - }: QueryResult & { data: ArticleResponsesType }) => { - if (!data || !data.article) { - return - } + const loadMore = (params?: { before: string }) => { + const loadBefore = (params && params.before) || null + const noLimit = loadBefore && pageInfo && pageInfo.endCursor - const connectionPath = 'article.responses' - const { edges, pageInfo } = _get(data, connectionPath, { - edges: {}, - pageInfo: {} + return fetchMore({ + variables: { + after: pageInfo && pageInfo.endCursor, + before: loadBefore, + first: noLimit ? null : RESPONSES_COUNT, + includeBefore: !!loadBefore, + articleOnly: articleOnlyMode + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath }) + }) + } - const loadMore = (params?: { before: string }) => { - const loadBefore = (params && params.before) || null - const noLimit = loadBefore && pageInfo.endCursor - return fetchMore({ - variables: { - after: pageInfo.endCursor, - before: loadBefore, - first: noLimit ? null : RESPONSES_COUNT, - includeBefore: !!loadBefore, - articleOnly: articleOnlyMode - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - - const commentCallback = () => { - return fetchMore({ - variables: { - before: storedCursor, - includeBefore: false, - articleOnly: articleOnlyMode - }, - updateQuery: (previousResult, { fetchMoreResult }) => { - const newEdges = _get( - fetchMoreResult, - `${connectionPath}.edges`, - [] - ) - const newResponseCount = _get( - fetchMoreResult, - 'article.responseCount' - ) - const oldResponseCount = _get( - previousResult, - 'article.responseCount' - ) - // update if response count has changed - if (newEdges.length === 0) { - if (oldResponseCount !== newResponseCount) { - return { - ...previousResult, - article: { - ...previousResult.article, - responseCount: newResponseCount - } - } - } - return previousResult - } + const commentCallback = () => { + return fetchMore({ + variables: { + before: storedCursor, + includeBefore: false, + articleOnly: articleOnlyMode + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + const newEdges = _get(fetchMoreResult, `${connectionPath}.edges`, []) + const newResponseCount = _get(fetchMoreResult, 'article.responseCount') + const oldResponseCount = _get(previousResult, 'article.responseCount') - // update if there are new items in responses.edges - const newResult = unshiftConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - const newStartCursor = _get( - newResult, - `${connectionPath}.pageInfo.startCursor`, - null - ) - if (newStartCursor) { - setStoredCursor(newStartCursor) + // update if response count has changed + if (newEdges.length === 0) { + if (oldResponseCount !== newResponseCount) { + return { + ...previousResult, + article: { + ...previousResult.article, + responseCount: newResponseCount } - return newResult } - }) + } + return previousResult } - const responses = filterResponses( - (edges || []).map(({ node }: { node: any }) => node) + // update if there are new items in responses.edges + const newResult = unshiftConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + const newStartCursor = _get( + newResult, + `${connectionPath}.pageInfo.startCursor`, + null ) + if (newStartCursor) { + setStoredCursor(newStartCursor) + } + return newResult + } + }) + } - // real time update with websocket - useEffect(() => { - if (data.article.live) { - subscribeToMore({ - document: SUBSCRIBE_RESPONSES, - variables: { - id: data.article.id, - first: edges.length, - articleOnly: articleOnlyMode - }, - updateQuery: (prev, { subscriptionData }) => - _merge(prev, { - article: subscriptionData.data.nodeEdited - }) - }) - } - }) + const responses = filterResponses((edges || []).map(({ node }) => node)) - // scroll to comment - useEffect(() => { - if (!fragment) { - return - } + // real time update with websocket + useEffect(() => { + if (article && edges) { + subscribeToMore({ + document: SUBSCRIBE_RESPONSES, + variables: { + id: article.id, + first: edges.length, + articleOnly: articleOnlyMode + }, + updateQuery: (prev, { subscriptionData }) => + _merge(prev, { + article: subscriptionData.data.nodeEdited + }) + }) + } + }, [articleId]) - const jumpToFragment = () => { - jump(`#${fragment}`, { - offset: fragment === UrlFragments.COMMENTS ? -10 : -64 - }) - } - const element = dom.$(`#${fragment}`) + // scroll to comment + useEffect(() => { + if (!fragment || !articleId) { + return + } - if (!element) { - loadMore({ before: parentId }).then(jumpToFragment) - } else { - jumpToFragment() - } - }, []) + const jumpToFragment = () => { + jump(`#${fragment}`, { + offset: fragment === UrlFragments.COMMENTS ? -10 : -64 + }) + } + const element = dom.$(`#${fragment}`) - useEventListener(REFETCH_RESPONSES, refetch) + if (!element) { + loadMore({ before: parentId }).then(jumpToFragment) + } else { + jumpToFragment() + } + }, [articleId]) - useEffect(() => { - if (pageInfo.startCursor) { - setStoredCursor(pageInfo.startCursor) - } - }, [pageInfo.startCursor]) + useEventListener(REFETCH_RESPONSES, refetch) - return ( -
    -
    -

    - -

    + useEffect(() => { + if (pageInfo && pageInfo.startCursor) { + setStoredCursor(pageInfo.startCursor) + } + }, [pageInfo && pageInfo.startCursor]) + + if (loading && !data) { + return + } + + if (error) { + return + } -
    - setArticleOnlyMode(!articleOnlyMode)} - checked={articleOnlyMode} - extraClass="narrow" - /> - - - -
    -
    + return ( +
    +
    +

    + +

    - {!responses || - (responses.length <= 0 && ( - - ))} +
    + setArticleOnlyMode(!articleOnlyMode)} + checked={articleOnlyMode} + extraClass="narrow" + /> + + + +
    +
    -
      - {responses.map(response => ( -
    • - {_has(response, 'title') ? ( - - ) : ( - - )} -
    • - ))} -
    + {!responses || + (responses.length <= 0 && ( + + ))} - {pageInfo.hasNextPage && ( - +
      + {responses.map(response => ( +
    • + {_has(response, 'title') ? ( + + ) : ( + )} +
    • + ))} +
    - -
    - ) - }} - + {pageInfo && pageInfo.hasNextPage && ( + + )} + + +
    ) } diff --git a/src/views/ArticleDetail/Responses/index.tsx b/src/views/ArticleDetail/Responses/index.tsx index f245b0b127..f38df24541 100644 --- a/src/views/ArticleDetail/Responses/index.tsx +++ b/src/views/ArticleDetail/Responses/index.tsx @@ -1,39 +1,34 @@ -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import gql from 'graphql-tag' +import { useQuery } from 'react-apollo' import { Translate } from '~/components' import CommentForm from '~/components/Form/CommentForm' -import { Query } from '~/components/GQL' import { ArticleResponseCount } from '~/components/GQL/queries/__generated__/ArticleResponseCount' import ARTICLE_RESPONSE_COUNT from '~/components/GQL/queries/articleResponseCount' import { REFETCH_RESPONSES, TEXT } from '~/common/enums' +import { ResponsesArticle } from './__generated__/ResponsesArticle' import FeatureComments from './FeaturedComments' import LatestResponses from './LatestResponses' import styles from './styles.css' -const ResponseCount = ({ mediaHash }: { mediaHash: string }) => ( - - {({ data }: QueryResult & { data: ArticleResponseCount }) => { - const count = _get(data, 'article.responseCount', 0) - return ( - <> - {count} - - - ) - }} - -) +const ResponseCount = ({ mediaHash }: { mediaHash: string }) => { + const { data } = useQuery(ARTICLE_RESPONSE_COUNT, { + variables: { mediaHash } + }) + const count = (data && data.article && data.article.responseCount) || 0 -const Responses = ({ - articleId, - mediaHash -}: { - articleId: string - mediaHash: string -}) => { + return ( + + {count} + + + + ) +} + +const Responses = ({ article }: { article: ResponsesArticle }) => { const refetchResponses = () => { window.dispatchEvent(new CustomEvent(REFETCH_RESPONSES, {})) } @@ -46,13 +41,14 @@ const Responses = ({ zh_hant={TEXT.zh_hant.response} zh_hans={TEXT.zh_hans.response} /> - +
    @@ -65,4 +61,17 @@ const Responses = ({ ) } +Responses.fragments = { + article: gql` + fragment ResponsesArticle on Article { + id + mediaHash + author { + id + isBlocking + } + } + ` +} + export default Responses diff --git a/src/views/ArticleDetail/State/index.tsx b/src/views/ArticleDetail/State/index.tsx index 3f2131bddb..19d5c4a662 100644 --- a/src/views/ArticleDetail/State/index.tsx +++ b/src/views/ArticleDetail/State/index.tsx @@ -40,7 +40,9 @@ const State = ({ article }: { article: StateArticle }) => { return (
    {isBanned && } />} + {isArchived && } />} +
    ) diff --git a/src/views/ArticleDetail/TagList/index.tsx b/src/views/ArticleDetail/TagList/index.tsx index 3e569a5064..34f3e5a13a 100644 --- a/src/views/ArticleDetail/TagList/index.tsx +++ b/src/views/ArticleDetail/TagList/index.tsx @@ -1,5 +1,4 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { Tag } from '~/components' diff --git a/src/views/ArticleDetail/Toolbar/AppreciationButton/index.tsx b/src/views/ArticleDetail/Toolbar/AppreciationButton/index.tsx index b89874cad6..1a3205aea6 100644 --- a/src/views/ArticleDetail/Toolbar/AppreciationButton/index.tsx +++ b/src/views/ArticleDetail/Toolbar/AppreciationButton/index.tsx @@ -1,17 +1,19 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' -import { forwardRef, useContext, useRef, useState } from 'react' +import { forwardRef, useContext, useState } from 'react' +import { useDebouncedCallback } from 'use-debounce' import { Icon, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { ModalSwitch } from '~/components/ModalManager' import { Tooltip } from '~/components/Popper' import { ViewerContext } from '~/components/Viewer' +import { APPRECIATE_DEBOUNCE } from '~/common/enums' import { numAbbr } from '~/common/utils' import ICON_LIKE from '~/static/icons/like.svg?sprite' +import { AppreciateArticle } from './__generated__/AppreciateArticle' import { AppreciationArticleDetail } from './__generated__/AppreciationArticleDetail' import styles from './styles.css' @@ -41,42 +43,40 @@ const APPRECIATE_ARTICLE = gql` } ` +const IconAppreciate = () => ( + +) + const AppreciatedCount = ({ num, limit }: { num: number; limit: number }) => { const classes = classNames({ 'appreciated-count': true, 'appreciated-reach-limit': num === limit }) return ( - <> - {num} + + {num} + - + ) } -const OnboardingAppreciateButton = ({ - article -}: { - article: AppreciationArticleDetail -}) => { - const buttonClasses = classNames({ - 'appreciate-button': true - }) - +const OnboardingAppreciateButton = () => { return ( {(open: any) => ( )} @@ -91,39 +91,32 @@ const AppreciateButton = forwardRef< canAppreciate: boolean isAuthed: boolean appreciatedCount: number - appreciateLimit: number + limit: number } ->( - ( - { appreciate, canAppreciate, isAuthed, appreciatedCount, appreciateLimit }, - ref - ) => { - const buttonClasses = classNames({ - 'appreciate-button': true - }) +>(({ appreciate, canAppreciate, isAuthed, appreciatedCount, limit }, ref) => { + const buttonClasses = classNames({ + 'appreciate-button': true + }) - return ( - - ) - } -) + return ( + + ) +}) const AppreciationButtonContainer = ({ article @@ -133,16 +126,38 @@ const AppreciationButtonContainer = ({ const viewer = useContext(ViewerContext) // bundle appreciations - const [bundling, setBundling] = useState(false) - const [appreciationAmount, setAppreciationAmount] = useState(0) - const amountRef = useRef(appreciationAmount) - amountRef.current = appreciationAmount - - const { appreciateLimit } = article - const appreciateLeft = article.appreciateLeft - appreciationAmount - const appreciatedCount = appreciateLimit - appreciateLeft - const isReachLimit = appreciateLeft <= 0 + const [amount, setAmount] = useState(0) + const [sendAppreciation] = useMutation(APPRECIATE_ARTICLE) + const { + appreciateLimit, + appreciateLeft, + appreciationsReceivedTotal + } = article + const limit = appreciateLimit + const left = appreciateLeft - amount + const appreciatedCount = limit - left + const [debouncedSendAppreciation] = useDebouncedCallback(() => { + sendAppreciation({ + variables: { id: article.id, amount }, + optimisticResponse: { + appreciateArticle: { + id: article.id, + appreciationsReceivedTotal: appreciationsReceivedTotal + amount, + hasAppreciate: true, + appreciateLeft: left, + __typename: 'Article' + } + } + }) + setAmount(0) + }, APPRECIATE_DEBOUNCE) + const appreciate = () => { + setAmount(amount + 1) + debouncedSendAppreciation() + } + // UI + const isReachLimit = left <= 0 const isMe = article.author.id === viewer.id const canAppreciate = (!isReachLimit && !isMe && !viewer.isInactive) || !viewer.isAuthed @@ -152,103 +167,63 @@ const AppreciationButtonContainer = ({ inactive: !canAppreciate, unlogged: !viewer.isAuthed }) + const appreciateButtonProps = { + limit, + appreciatedCount, + canAppreciate, + appreciate, + isAuthed: viewer.isAuthed + } - if (viewer.isOnboarding) { + if (viewer.shouldSetupLikerID) { return (
    - + + {numAbbr(article.appreciationsReceivedTotal)} +
    ) } return ( - - {(sendAppreciation: any, { data }: any) => { - // bundle appreciations - const appreciate = () => { - const debounce = 1000 - setAppreciationAmount(appreciationAmount + 1) - - if (!bundling) { - setBundling(true) - setTimeout(() => { - if (amountRef.current) { - sendAppreciation({ - variables: { id: article.id, amount: amountRef.current } - }) - setAppreciationAmount(0) - setBundling(false) - } - }, debounce) +
    + {canAppreciate && } + + {!canAppreciate && ( + } - } + > + + + )} + + + {numAbbr(article.appreciationsReceivedTotal)} + - return ( -
    - {canAppreciate && ( - - )} - {!canAppreciate && ( - - } - > - - - )} - - {numAbbr(article.appreciationsReceivedTotal)} - - -
    - ) - }} - + +
    ) } diff --git a/src/views/ArticleDetail/Toolbar/Appreciators/AppreciatorsModal.tsx b/src/views/ArticleDetail/Toolbar/Appreciators/AppreciatorsModal.tsx index c5ce15a9ba..fdb3215a56 100644 --- a/src/views/ArticleDetail/Toolbar/Appreciators/AppreciatorsModal.tsx +++ b/src/views/ArticleDetail/Toolbar/Appreciators/AppreciatorsModal.tsx @@ -1,10 +1,9 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { InfiniteScroll, Spinner, Translate, UserDigest } from '~/components' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { Modal } from '~/components/Modal' import { ModalInstance } from '~/components/ModalManager' @@ -50,101 +49,94 @@ const ARTICLE_APPRECIATORS = gql` const AppreciatorsModal = () => { const router = useRouter() const mediaHash = getQuery({ router, key: 'mediaHash' }) + const { data, loading, error, fetchMore } = useQuery( + ARTICLE_APPRECIATORS, + { variables: { mediaHash } } + ) - if (!mediaHash) { - return null + const article = data && data.article + const connectionPath = 'article.appreciationsReceived' + const { edges, pageInfo } = + (data && data.article && data.article.appreciationsReceived) || {} + + if (loading) { + return } - return ( - - {(props: ModalInstanceProps) => ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: AllArticleAppreciators }) => { - if (loading) { - return - } + if (error) { + return + } - const connectionPath = 'article.appreciationsReceived' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.APPRECIATOR, - location: edges.length, - entrance: data.article.id - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - const totalCount = numFormat( - _get(data, 'article.appreciationsReceived.totalCount', 0) - ) + if (!edges || edges.length <= 0 || !pageInfo || !article) { + return null + } - return ( - <> - - } - /> - - { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.APPRECIATOR, + location: edges.length, + entrance: article.id + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + const totalCount = numFormat( + (data && data.article && data.article.appreciationsReceived.totalCount) || 0 + ) + + return ( + <> + + } + /> + + +
      + {edges.map( + ({ node, cursor }, i) => + node.sender && ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.APPRECIATOR, + location: i, + entrance: article.id + }) + } > -
        - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
      • - analytics.trackEvent( - ANALYTICS_EVENTS.CLICK_FEED, - { - type: FEED_TYPE.APPRECIATOR, - location: i, - entrance: data.article.id - } - ) - } - > - -
      • - ) - )} - -
      - - - - ) - }} - - )} - + +
    • + ) + )} + + +
    +
    +
    + ) } -export default AppreciatorsModal +export default () => ( + + {(props: ModalInstanceProps) => } + +) diff --git a/src/views/ArticleDetail/Toolbar/Appreciators/index.tsx b/src/views/ArticleDetail/Toolbar/Appreciators/index.tsx index 32bceec611..c32a6edd8f 100644 --- a/src/views/ArticleDetail/Toolbar/Appreciators/index.tsx +++ b/src/views/ArticleDetail/Toolbar/Appreciators/index.tsx @@ -36,7 +36,7 @@ const fragments = { } const Appreciators = ({ article }: { article: AppreciatorsArticle }) => { - const edges = _get(article, 'appreciationsReceived.edges') + const edges = article.appreciationsReceived.edges if (!edges || edges.length <= 0) { return null @@ -54,17 +54,21 @@ const Appreciators = ({ article }: { article: AppreciatorsArticle }) => {
    {edges .slice(0, 5) - .map(({ node, cursor }: { node: any; cursor: any }) => ( - - ))} + .map( + ({ node, cursor }) => + node.sender && ( + + ) + )}

    {edges .slice(0, 3) - .map(({ node }: { node: any }) => node.sender.displayName) + .map(({ node }) => node.sender && node.sender.displayName) .join('、')}

    +

    `等 ${count} 人贊賞了作品`} @@ -75,6 +79,7 @@ const Appreciators = ({ article }: { article: AppreciatorsArticle }) => { />

    + )} diff --git a/src/views/ArticleDetail/Toolbar/Appreciators/styles.css b/src/views/ArticleDetail/Toolbar/Appreciators/styles.css index a1f7793bde..f14e0b3987 100644 --- a/src/views/ArticleDetail/Toolbar/Appreciators/styles.css +++ b/src/views/ArticleDetail/Toolbar/Appreciators/styles.css @@ -48,4 +48,11 @@ .modal-appreciators-list { margin-top: var(--spacing-default); padding: 0 var(--spacing-tight); + + & li:last-child { + & :global(> *) { + margin-bottom: 0; + border-bottom: 0; + } + } } diff --git a/src/views/ArticleDetail/Toolbar/CommentButton/index.tsx b/src/views/ArticleDetail/Toolbar/CommentButton/index.tsx index fc4b194afd..20da093ffb 100644 --- a/src/views/ArticleDetail/Toolbar/CommentButton/index.tsx +++ b/src/views/ArticleDetail/Toolbar/CommentButton/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' import jump from 'jump.js' -import _get from 'lodash/get' import { MouseEventHandler } from 'react' import { Icon, TextIcon } from '~/components' @@ -71,7 +70,7 @@ const CommentButton = ({ type: 'article-detail' }) }} - text={numAbbr(_get(article, 'commentCount', 0))} + text={numAbbr(article.commentCount || 0)} textPlacement={textPlacement} /> ) diff --git a/src/views/ArticleDetail/Toolbar/ExtendButton/index.tsx b/src/views/ArticleDetail/Toolbar/ExtendButton/index.tsx index 345e64c30d..a1930a06a4 100644 --- a/src/views/ArticleDetail/Toolbar/ExtendButton/index.tsx +++ b/src/views/ArticleDetail/Toolbar/ExtendButton/index.tsx @@ -3,7 +3,7 @@ import Router from 'next/router' import { useContext } from 'react' import { Icon, LanguageContext, Tooltip, Translate } from '~/components' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { ViewerContext } from '~/components/Viewer' import { TEXT } from '~/common/enums' @@ -34,12 +34,16 @@ const fragments = { const ExtendButton = ({ article }: { article: ExtendButtonArticle }) => { const viewer = useContext(ViewerContext) const { lang } = useContext(LanguageContext) - const placeholder = translate({ - zh_hans: TEXT.zh_hans.untitle, - zh_hant: TEXT.zh_hant.untitle, - lang + const [extend] = useMutation(EXTEND_ARTICLE, { + variables: { + title: translate({ + zh_hans: TEXT.zh_hans.untitle, + zh_hant: TEXT.zh_hant.untitle, + lang + }), + collection: [article.id] + } }) - const canExtend = viewer.isActive if (!canExtend) { @@ -47,35 +51,30 @@ const ExtendButton = ({ article }: { article: ExtendButtonArticle }) => { } return ( - } + placement="top" > - {(extend: any) => ( - } - placement="top" - > - - - )} - + + ) } diff --git a/src/views/ArticleDetail/Toolbar/ResponseButton/index.tsx b/src/views/ArticleDetail/Toolbar/ResponseButton/index.tsx index 2267f3515a..52fec98642 100644 --- a/src/views/ArticleDetail/Toolbar/ResponseButton/index.tsx +++ b/src/views/ArticleDetail/Toolbar/ResponseButton/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' import jump from 'jump.js' -import _get from 'lodash/get' import { MouseEventHandler } from 'react' import { Icon, TextIcon } from '~/components' @@ -71,7 +70,7 @@ const ResponseButton = ({ type: 'article-detail' }) }} - text={numAbbr(_get(article, 'responseCount', 0))} + text={numAbbr(article.responseCount || 0)} textPlacement={textPlacement} /> ) diff --git a/src/views/ArticleDetail/Toolbar/index.tsx b/src/views/ArticleDetail/Toolbar/index.tsx index 5d36c17d40..577fd7d421 100644 --- a/src/views/ArticleDetail/Toolbar/index.tsx +++ b/src/views/ArticleDetail/Toolbar/index.tsx @@ -1,9 +1,8 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' import { BookmarkButton } from '~/components/Button/Bookmark' -import ShareButton from '~/components/ShareButton' +import ShareButton from '~/components/Button/Share' import { ToolbarArticle } from './__generated__/ToolbarArticle' import AppreciationButton from './AppreciationButton' @@ -67,6 +66,7 @@ const Toolbar = ({
    +
    @@ -74,6 +74,7 @@ const Toolbar = ({
    + ) diff --git a/src/views/ArticleDetail/Wall/index.tsx b/src/views/ArticleDetail/Wall/index.tsx index b6b68b2f97..b4e6a4d310 100644 --- a/src/views/ArticleDetail/Wall/index.tsx +++ b/src/views/ArticleDetail/Wall/index.tsx @@ -62,6 +62,7 @@ const Wall = ({ show, client }: any) => { + ) diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index cda40c1971..8c2bbb2994 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -1,27 +1,20 @@ import gql from 'graphql-tag' import jump from 'jump.js' -import _get from 'lodash/get' import _merge from 'lodash/merge' import { useRouter } from 'next/router' import { useContext, useEffect, useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Waypoint } from 'react-waypoint' -import { - DateTime, - Footer, - Head, - Placeholder, - Responsive, - Title -} from '~/components' +import { DateTime, Footer, Head, Placeholder, Title } from '~/components' import { BookmarkButton } from '~/components/Button/Bookmark' +import ShareModal from '~/components/Button/Share/ShareModal' import { Fingerprint } from '~/components/Fingerprint' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' +import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' import CLIENT_PREFERENCE from '~/components/GQL/queries/clientPreference' -import { useImmersiveMode } from '~/components/Hook' +import { useImmersiveMode, useResponsive } from '~/components/Hook' import IconLive from '~/components/Icon/Live' -import ShareModal from '~/components/ShareButton/ShareModal' import Throw404 from '~/components/Throw404' import { UserDigest } from '~/components/UserDigest' import { ViewerContext } from '~/components/Viewer' @@ -30,6 +23,7 @@ import { ANALYTICS_EVENTS } from '~/common/enums' import { analytics, getQuery } from '~/common/utils' import { ArticleDetail as ArticleDetailType } from './__generated__/ArticleDetail' +import { ArticleEdited } from './__generated__/ArticleEdited' import Collection from './Collection' import Content from './Content' import RelatedArticles from './RelatedArticles' @@ -60,6 +54,7 @@ const ARTICLE_DETAIL = gql` summary createdAt author { + isBlocking ...UserDigestFullDescUser } collection(input: { first: 0 }) @connection(key: "articleCollection") { @@ -72,6 +67,7 @@ const ARTICLE_DETAIL = gql` ...RelatedArticles ...StateArticle ...FingerprintArticle + ...ResponsesArticle } } ${UserDigest.FullDesc.fragments.user} @@ -82,6 +78,20 @@ const ARTICLE_DETAIL = gql` ${RelatedArticles.fragments.article} ${State.fragments.article} ${Fingerprint.fragments.article} + ${Responses.fragments.article} +` + +const ARTICLE_EDITED = gql` + subscription ArticleEdited($id: ID!) { + nodeEdited(input: { id: $id }) { + id + ... on Article { + id + ...ToolbarArticle + } + } + } + ${Toolbar.fragments.article} ` const Block = ({ @@ -110,219 +120,188 @@ const ArticleDetail = ({ const [fixedToolbar, setFixedToolbar] = useState(true) const [trackedFinish, setTrackedFinish] = useState(false) const [fixedWall, setFixedWall] = useState(false) + const isMediumUp = useResponsive({ type: 'medium-up' })() + const { data, loading, error, subscribeToMore, client } = useQuery< + ArticleDetailType + >(ARTICLE_DETAIL, { + variables: { mediaHash } + }) + const shouldShowWall = !viewer.isAuthed && wall + const article = data && data.article + const authorId = article && article.author.id + const collectionCount = (article && article.collection.totalCount) || 0 + const canEditCollection = viewer.id === authorId + const handleWall = ({ currentPosition }: Waypoint.CallbackArgs) => { + if (shouldShowWall) { + setFixedWall(currentPosition === 'inside') + } + } + + useEffect(() => { + if (article && article.live) { + subscribeToMore({ + document: ARTICLE_EDITED, + variables: { id: article.id }, + updateQuery: (prev, { subscriptionData }) => + _merge(prev, { + article: subscriptionData.data.nodeEdited + }) + }) + } + }) + + useEffect(() => { + if (shouldShowWall && window.location.hash && article) { + jump('#comments', { offset: -10 }) + } + }, [article]) + + useImmersiveMode('article > .content') + + if (loading) { + return ( + + + + ) + } - if (!mediaHash) { + if (error) { + return ( + + + + ) + } + + if (!article) { return null } + if (article.state !== 'active' && viewer.id !== authorId) { + return + } + return ( - - {({ - data, - client, - loading, - subscribeToMore - }: QueryResult & { data: ArticleDetailType }) => { - const authorId = _get(data, 'article.author.id') - const collectionCount = _get(data, 'article.collection.totalCount') - const canEditCollection = viewer.id === authorId - - const handleWall = ({ currentPosition }: Waypoint.CallbackArgs) => { - if (shouldShowWall) { - setFixedWall(currentPosition === 'inside') + <> + + content) + : [] } - } - - return ( -
    - {(() => { - if (loading) { - return ( - - - - ) - } - - if (data.article.state !== 'active' && viewer.id !== authorId) { - return - } - - useEffect(() => { - if (data.article.live) { - subscribeToMore({ - document: gql` - subscription ArticleEdited($id: ID!) { - nodeEdited(input: { id: $id }) { - id - ... on Article { - id - ...ToolbarArticle - } - } - } - ${Toolbar.fragments.article} - `, - variables: { id: data.article.id }, - updateQuery: (prev, { subscriptionData }) => - _merge(prev, { - article: subscriptionData.data.nodeEdited - }) - }) + image={article.cover} + /> + + + +
    + +
    + +
    + {article.title} + +

    + +

    + + {article.live && } + + +
    +
    + +
    + + + {(collectionCount > 0 || canEditCollection) && ( + + )} + + {/* content:end */} + {!isMediumUp && ( + { + if (currentPosition === 'below') { + setFixedToolbar(true) + } else { + setFixedToolbar(false) } - }) + }} + /> + )} + + + + +
    + + + + + +
    + +
    +
    + + {shouldShowWall && } - useEffect(() => { - if (process.browser && shouldShowWall) { - if (window.location.hash) { - jump('#comments', { offset: -10 }) - } + + {shouldShowWall &&
    } + + {!shouldShowWall && ( + <> + + + { + if (!trackedFinish) { + analytics.trackEvent(ANALYTICS_EVENTS.FINISH_COMMENTS, { + entrance: article.id + }) + setTrackedFinish(true) } - }, []) - - useImmersiveMode('article > .content') - - return ( - - {(isMediumUp: boolean) => ( - <> - - content - )} - image={data.article.cover} - /> - - - -
    - -
    - -
    - {data.article.title} - -

    - -

    - - {data.article.live && } - - -
    -
    - -
    - - {(collectionCount > 0 || canEditCollection) && ( - - )} - - {/* content:end */} - {!isMediumUp && ( - { - if (currentPosition === 'below') { - setFixedToolbar(true) - } else { - setFixedToolbar(false) - } - }} - /> - )} - - - -
    - - -
    - - -
    - -
    -
    - - {shouldShowWall && ( - - )} - - - {shouldShowWall &&
    } - {!shouldShowWall && ( - <> - - { - if (!trackedFinish) { - analytics.trackEvent( - ANALYTICS_EVENTS.FINISH_COMMENTS, - { - entrance: data.article.id - } - ) - setTrackedFinish(true) - } - }} - /> - - )} - - - - - )} - - ) - })()} - -
    - {!shouldShowWall &&
    } -
    - - -
    - ) - }} -
    + }} + /> + + )} + + + + + + + + ) } const ArticleDetailContainer = () => { const router = useRouter() const mediaHash = getQuery({ router, key: 'mediaHash' }) - - if (!mediaHash) { - return null - } + const { data } = useQuery(CLIENT_PREFERENCE, { + variables: { id: 'local' } + }) + const { wall } = (data && data.clientPreference) || { wall: true } return ( - - {({ data }: any) => { - const { wall } = _get(data, 'clientPreference', { wall: true }) - return - }} - +
    + + +
    +
    +
    +
    ) } diff --git a/src/views/Auth/Forget/index.tsx b/src/views/Auth/Forget/index.tsx index 12d7434e04..e027f4cd74 100644 --- a/src/views/Auth/Forget/index.tsx +++ b/src/views/Auth/Forget/index.tsx @@ -63,69 +63,68 @@ const Forget = () => { } return ( - <> -
    - +
    + -
    - {step === 'request' && ( - - )} - {step === 'reset' && ( - setStep('complete')} - scrollLock={false} - /> - )} - {step === 'complete' && ( -
    - - - <Translate - zh_hant={TEXT.zh_hant.resetPasswordSuccess} - zh_hans={TEXT.zh_hans.resetPasswordSuccess} - /> - - -

    - - 。 -

    -
    +
    + {step === 'request' && ( + + )} + {step === 'reset' && ( + setStep('complete')} + scrollLock={false} + /> + )} + {step === 'complete' && ( +
    + + + <Translate + zh_hant={TEXT.zh_hant.resetPasswordSuccess} + zh_hans={TEXT.zh_hans.resetPasswordSuccess} + /> + - +

    - -

    - )} -
    -
    + 。 +

    + + + + + + + )} + + - +
    ) } diff --git a/src/views/Auth/Login/index.tsx b/src/views/Auth/Login/index.tsx index 3eb521e032..f9e06447be 100644 --- a/src/views/Auth/Login/index.tsx +++ b/src/views/Auth/Login/index.tsx @@ -28,18 +28,17 @@ const Login = () => { ) return ( - <> -
    - - -
    - -
    -
    +
    + + +
    + +
    + - +
    ) } diff --git a/src/views/Auth/SignUp/index.tsx b/src/views/Auth/SignUp/index.tsx index 6d3e2f1cb0..169698b0e7 100644 --- a/src/views/Auth/SignUp/index.tsx +++ b/src/views/Auth/SignUp/index.tsx @@ -33,49 +33,48 @@ const SignUp = () => { ) return ( - <> -
    - +
    + + +
    + {step === 'signUp' && ( + { + setStep('profile') + }} + scrollLock={false} + /> + )} + {step === 'profile' && ( + { + setStep('setupLikeCoin') + }} + scrollLock={false} + /> + )} + {step === 'setupLikeCoin' && ( + { + setStep('complete') + }} + scrollLock={false} + /> + )} + {step === 'complete' && ( + + )} +
    -
    - {step === 'signUp' && ( - { - setStep('profile') - }} - scrollLock={false} - /> - )} - {step === 'profile' && ( - { - setStep('setupLikeCoin') - }} - scrollLock={false} - /> - )} - {step === 'setupLikeCoin' && ( - { - setStep('complete') - }} - scrollLock={false} - /> - )} - {step === 'complete' && ( - - )} -
    -
    - +
    ) } diff --git a/src/views/Authors/index.tsx b/src/views/Authors/index.tsx index 9d224e492d..21e3320716 100644 --- a/src/views/Authors/index.tsx +++ b/src/views/Authors/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, @@ -11,7 +10,8 @@ import { Translate, UserDigest } from '~/components' -import { Query } from '~/components/GQL' +import EmptyWarning from '~/components/Empty/EmptyWarning' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -43,95 +43,98 @@ const ALL_AUTHORSS = gql` ${UserDigest.FullDesc.fragments.user} ` -const Authors = () => ( -
    -
    - +const Authors = () => { + const { data, loading, error, fetchMore } = useQuery(ALL_AUTHORSS) - - } + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.recommendation.authors' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.recommendation.authors) || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } /> + ) + } -
    - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: AllAuthors }) => { - if (loading) { - return - } + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.ALL_AUTHORS, + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } - const connectionPath = 'viewer.recommendation.authors' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + return ( + +
      + {edges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { type: FEED_TYPE.ALL_AUTHORS, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) + location: i }) } + > + +
    • + ))} +
    +
    + ) +} - return ( - -
      - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.ALL_AUTHORS, - location: i - }) - } - > - -
    • - ) - )} -
    -
    - ) +export default () => { + return ( +
    +
    + -
    -
    + /> + + + } + /> - +
    + +
    + - -
    -) + -export default Authors + + + ) +} diff --git a/src/views/Follow/FollowFeed/index.tsx b/src/views/Follow/FollowFeed/index.tsx index ed9c9d826b..f0ae50287e 100644 --- a/src/views/Follow/FollowFeed/index.tsx +++ b/src/views/Follow/FollowFeed/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, @@ -10,12 +9,13 @@ import { Placeholder, Translate } from '~/components' -import { Query } from '~/components/GQL' +import EmptyArticle from '~/components/Empty/EmptyArticle' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' -import { FollowFeed } from './__generated__/FollowFeed' +import { FollowFeed as FollowFeedType } from './__generated__/FollowFeed' const FOLLOW_FEED = gql` query FollowFeed( @@ -46,86 +46,84 @@ const FOLLOW_FEED = gql` ${ArticleDigest.Feed.fragments.article} ` -export default () => { - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: FollowFeed }) => { - if (loading) { - return - } +const FollowFeed = () => { + const { data, loading, error, fetchMore } = useQuery( + FOLLOW_FEED + ) - const connectionPath = 'viewer.recommendation.followeeArticles' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.FOLLOW, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (loading) { + return + } - return ( - <> - + if (error) { + return + } - - } - /> + const connectionPath = 'viewer.recommendation.followeeArticles' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.recommendation.followeeArticles) || {} - -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.FOLLOW, - location: i - }) - } - > - -
    • - ) - )} -
    -
    - - ) - }} -
    + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_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(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.FOLLOW, + location: i + }) + } + > + +
    • + ))} +
    +
    ) } + +export default () => ( + <> + + + + } + /> + + + +) diff --git a/src/views/Follow/PickAuthors/index.tsx b/src/views/Follow/PickAuthors/index.tsx index a04eed917d..de59b15a4f 100644 --- a/src/views/Follow/PickAuthors/index.tsx +++ b/src/views/Follow/PickAuthors/index.tsx @@ -1,7 +1,5 @@ -import _get from 'lodash/get' - import { Head, Translate } from '~/components' -import { AuthorPicker } from '~/components/Follow' +import AuthorPicker from '~/components/Follow/AuthorPicker' import { TEXT } from '~/common/enums' import IMAGE_ILLUSTRATION_AVATAR from '~/static/images/illustration-avatar.svg' @@ -23,6 +21,7 @@ const PickIntroHeader = () => { zh_hans="欢迎加入 Matters,一个自由、自主、永续的创作与公共讨论空间。" />

    +

    5 @@ -32,6 +31,7 @@ const PickIntroHeader = () => { />

    + ) @@ -55,6 +55,7 @@ const PickAuthors = ({ viewer }: { [key: string]: any }) => ( /> } /> + ) diff --git a/src/views/Follow/index.tsx b/src/views/Follow/index.tsx index f671060271..62c4da4730 100644 --- a/src/views/Follow/index.tsx +++ b/src/views/Follow/index.tsx @@ -1,14 +1,14 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useContext, useEffect } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, Spinner } from '~/components' -import { Mutation, Query } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import viewerUnreadFolloweeArticles from '~/components/GQL/updates/viewerUnreadFolloweeArticles' import { ViewerContext } from '~/components/Viewer' import { MeFollow } from './__generated__/MeFollow' +import { ReadFolloweeArticles } from './__generated__/ReadFolloweeArticles' import FollowFeed from './FollowFeed' import PickAuthors from './PickAuthors' @@ -28,49 +28,48 @@ const ME_FOLLOW = gql` ${PickAuthors.fragments.user} ` -export default () => { +const Follow = () => { const viewer = useContext(ViewerContext) + const [readFolloweeArticles] = useMutation( + READ_FOLLOWEE_ARTICLES, + { + update: viewerUnreadFolloweeArticles + } + ) + const { data, loading } = useQuery(ME_FOLLOW) - return ( -
    -
    - - {(readFolloweeArticles: any) => ( - - {({ data, loading, error }: QueryResult & { data: MeFollow }) => { - if (loading) { - return - } + useEffect(() => { + if (viewer.isAuthed) { + readFolloweeArticles() + } + }, []) - useEffect(() => { - if (viewer.isAuthed) { - readFolloweeArticles() - } - }, []) + if (loading) { + return + } - const followeeCount = _get( - data, - 'viewer.followees.totalCount', - 0 - ) + if (!data) { + return null + } - if (followeeCount < 5) { - return - } else { - return - } - }} - - )} - -
    + const followeeCount = + (data && data.viewer && data.viewer.followees.totalCount) || 0 - -
    - ) + if (followeeCount < 5) { + return + } else { + return + } } + +export default () => ( +
    +
    + +
    + + +
    +) diff --git a/src/views/Home/Feed/SortBy/index.tsx b/src/views/Home/Feed/SortBy/index.tsx index 9dbcf210a5..4f9a68edd1 100644 --- a/src/views/Home/Feed/SortBy/index.tsx +++ b/src/views/Home/Feed/SortBy/index.tsx @@ -42,6 +42,7 @@ const SortBy: React.FC = ({ sortBy, setSortBy }) => { {!isHottest && } + ) diff --git a/src/views/Home/Feed/index.tsx b/src/views/Home/Feed/index.tsx index 175d887d90..e231227a9a 100644 --- a/src/views/Home/Feed/index.tsx +++ b/src/views/Home/Feed/index.tsx @@ -1,23 +1,25 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { InfiniteScroll, LoadMore, PageHeader, Placeholder, - Responsive, Translate } from '~/components' import { ArticleDigest } from '~/components/ArticleDigest' -import { Query } from '~/components/GQL' +import EmptyArticle from '~/components/Empty/EmptyArticle' +import { QueryError } from '~/components/GQL' +import { ClientPreference } from '~/components/GQL/queries/__generated__/ClientPreference' import CLIENT_PREFERENCE from '~/components/GQL/queries/clientPreference' +import { useResponsive } from '~/components/Hook' import { ANALYTICS_EVENTS } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' -import { FeedArticleConnection } from './__generated__/FeedArticleConnection' +import { HottestFeed } from './__generated__/HottestFeed' +import { NewestFeed } from './__generated__/NewestFeed' import SortBy from './SortBy' const feedFragment = gql` @@ -37,7 +39,7 @@ const feedFragment = gql` ${ArticleDigest.Feed.fragments.article} ` -export const queries: { [key: string]: any } = { +export const queries = { hottest: gql` query HottestFeed( $after: String @@ -78,7 +80,85 @@ export const queries: { [key: string]: any } = { type SortBy = 'hottest' | 'newest' -const Feed = ({ feedSortType: sortBy, client }: any) => { +const Feed = ({ feedSortType: sortBy }: { feedSortType: SortBy }) => { + const isMediumUp = useResponsive({ type: 'medium-up' })() + const { data, error, loading, fetchMore } = useQuery< + HottestFeed | NewestFeed + >(queries[sortBy], { + notifyOnNetworkStatusChange: true + }) + + const connectionPath = 'viewer.recommendation.feed' + const result = data && data.viewer && data.viewer.recommendation.feed + const { edges, pageInfo } = result || {} + + if (loading && !result) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: sortBy, + 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(ANALYTICS_EVENTS.CLICK_FEED, { + type: sortBy, + location: i + }) + } + > + +
    • + ))} +
    +
    + + {!isMediumUp && pageInfo.hasNextPage && ( + + )} + + ) +} + +const HomeFeed = () => { + const { data, client } = useQuery(CLIENT_PREFERENCE, { + variables: { id: 'local' } + }) + const { feedSortType } = (data && data.clientPreference) || { + feedSortType: 'hottest' + } const setSortBy = (type: SortBy) => { if (client) { client.writeData({ @@ -90,110 +170,21 @@ const Feed = ({ feedSortType: sortBy, client }: any) => { return ( <> - - {({ - data, - loading, - fetchMore - }: QueryResult & { data: FeedArticleConnection }) => { - if (loading && !_get(data, 'viewer.recommendation.feed')) { - return - } - - const connectionPath = 'viewer.recommendation.feed' - const { edges, pageInfo } = _get(data, connectionPath, { - edges: [], - pageInfo: {} - }) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: sortBy, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - - return ( - <> - - ) : ( - - ) - } - > - - - - - {(match: boolean) => ( - <> - -
      - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
    • - analytics.trackEvent( - ANALYTICS_EVENTS.CLICK_FEED, - { - type: sortBy, - location: i - } - ) - } - > - -
    • - ) - )} -
    -
    - - {!match && pageInfo.hasNextPage && ( - - )} - - )} -
    - + + ) : ( + ) - }} -
    + } + > + +
    + + ) } -export default () => ( - - {({ data, client }: any) => { - const { feedSortType } = _get(data, 'clientPreference', { - feedSortType: 'hottest' - }) - return - }} - -) +export default HomeFeed diff --git a/src/views/Home/MattersToday/index.tsx b/src/views/Home/MattersToday/index.tsx index 000ecc45d2..9f2901459c 100644 --- a/src/views/Home/MattersToday/index.tsx +++ b/src/views/Home/MattersToday/index.tsx @@ -1,16 +1,14 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Placeholder } from '~/components' +import { Error, Placeholder } from '~/components' import { ArticleDigest } from '~/components/ArticleDigest' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' import { analytics } from '~/common/utils' import { HomeToday } from './__generated__/HomeToday' -import styles from './styles.css' export const HOME_TODAY = gql` query HomeToday( @@ -30,33 +28,38 @@ export const HOME_TODAY = gql` ${ArticleDigest.Feature.fragments.article} ` -export default () => ( - <> - - {({ data, loading, error }: QueryResult & { data: HomeToday }) => { - const article = _get(data, 'viewer.recommendation.today') +const MattersToday = () => { + const { data, loading, error } = useQuery(HOME_TODAY) - if (loading || !article) { - return + if (loading) { + return + } + + if (error) { + return + } + + const article = data && data.viewer && data.viewer.recommendation.today + + if (!article) { + return + } + + return ( + <> + + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.TODAY + }) } + hasAuthor + hasDateTime + hasBookmark + /> + + ) +} - return ( - <> - - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.TODAY - }) - } - hasAuthor - hasDateTime - hasBookmark - /> - - ) - }} - - - -) +export default MattersToday diff --git a/src/views/Home/MattersToday/styles.css b/src/views/Home/MattersToday/styles.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/views/Home/Sidebar/Authors/index.tsx b/src/views/Home/Sidebar/Authors/index.tsx index 20e531bb6e..ade3659c56 100644 --- a/src/views/Home/Sidebar/Authors/index.tsx +++ b/src/views/Home/Sidebar/Authors/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Label, @@ -9,7 +8,7 @@ import { Translate, UserDigest } from '~/components' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' import { analytics } from '~/common/utils' @@ -39,66 +38,66 @@ const SIDEBAR_AUTHORS = gql` ${UserDigest.FullDesc.fragments.user} ` -export default () => ( - <> - - {({ - data, - loading, - error, - refetch - }: QueryResult & { data: SidebarAuthors }) => { - const edges = _get(data, 'viewer.recommendation.authors.edges', []) +const Authors = () => { + const { data, loading, error, refetch } = useQuery( + SIDEBAR_AUTHORS, + { + notifyOnNetworkStatusChange: true + } + ) + const edges = data && data.viewer && data.viewer.recommendation.authors.edges - if (!edges || edges.length <= 0) { - return null - } + if (error) { + return + } + + if (!edges || edges.length <= 0) { + return null + } + + return ( + <> +
    + + +
    + { + refetch() + analytics.trackEvent(ANALYTICS_EVENTS.SHUFFLE_AUTHOR, { + type: FEED_TYPE.AUTHORS + }) + }} + /> + +
    +
    - return ( - <> -
    - + {loading && } -
    - { - refetch() - analytics.trackEvent(ANALYTICS_EVENTS.SHUFFLE_AUTHOR, { - type: FEED_TYPE.AUTHORS - }) - }} - /> - -
    -
    + {!loading && ( +
      + {edges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.AUTHORS, + location: i + }) + } + > + +
    • + ))} +
    + )} - {loading && } + + + ) +} - {!loading && ( -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.AUTHORS, - location: i - }) - } - > - -
    • - ) - )} -
    - )} - - ) - }} -
    - - -) +export default Authors diff --git a/src/views/Home/Sidebar/Icymi/index.tsx b/src/views/Home/Sidebar/Icymi/index.tsx index d56f6db74c..dfc126bdf8 100644 --- a/src/views/Home/Sidebar/Icymi/index.tsx +++ b/src/views/Home/Sidebar/Icymi/index.tsx @@ -1,10 +1,8 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Label, Placeholder, Translate } from '~/components' import { ArticleDigest } from '~/components/ArticleDigest' -import { Query } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' import { analytics } from '~/common/utils' @@ -35,46 +33,43 @@ export const SIDEBAR_ICYMI = gql` ${ArticleDigest.Sidebar.fragments.article} ` -export default () => ( - - {({ data, loading, error }: QueryResult & { data: SidebarIcymi }) => { - if (loading) { - return - } +const ICYMI = () => { + const { data, loading } = useQuery(SIDEBAR_ICYMI) + const edges = data && data.viewer && data.viewer.recommendation.icymi.edges - const edges = _get(data, 'viewer.recommendation.icymi.edges', []) + if (loading) { + return + } - if (!edges || edges.length <= 0) { - return null - } + if (!edges || edges.length <= 0) { + return null + } + + return ( + <> +
    + +
    - return ( - <> -
    - -
    +
      + {edges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.ICYMI, + location: i + }) + } + > + +
    • + ))} +
    + + ) +} -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.ICYMI, - location: i - }) - } - > - -
    • - ) - )} -
    - - ) - }} -
    -) +export default ICYMI diff --git a/src/views/Home/Sidebar/Tags/index.tsx b/src/views/Home/Sidebar/Tags/index.tsx index a75111c453..7424e6fd36 100644 --- a/src/views/Home/Sidebar/Tags/index.tsx +++ b/src/views/Home/Sidebar/Tags/index.tsx @@ -1,9 +1,8 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Label, Tag, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { Label, Spinner, Tag, Translate } from '~/components' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics } from '~/common/utils' @@ -31,49 +30,48 @@ const SIDEBAR_TAGS = gql` ${Tag.fragments.tag} ` -export default () => ( - <> - - {({ data, loading, error }: QueryResult & { data: SidebarTags }) => { - const edges = _get(data, 'viewer.recommendation.tags.edges', []) +const Tags = () => { + const { data, loading, error } = useQuery(SIDEBAR_TAGS) + const edges = data && data.viewer && data.viewer.recommendation.tags.edges - if (!edges || edges.length <= 0) { - return null - } + if (error) { + return + } + + if (!edges || edges.length <= 0) { + return null + } + + return ( + <> +
    + + +
    + + {loading && } + +
      + {edges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.TAGS, + location: i + }) + } + > + +
    • + ))} +
    - return ( - <> -
    - - -
    + + + ) +} -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.TAGS, - location: i - }) - } - > - -
    • - ) - )} -
    - - ) - }} -
    - - -) +export default Tags diff --git a/src/views/Home/Sidebar/Topics/index.tsx b/src/views/Home/Sidebar/Topics/index.tsx index a1695b9eae..c3e7060dc0 100644 --- a/src/views/Home/Sidebar/Topics/index.tsx +++ b/src/views/Home/Sidebar/Topics/index.tsx @@ -1,10 +1,9 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Label, Translate } from '~/components' +import { Label, Placeholder, Translate } from '~/components' import { ArticleDigest } from '~/components/ArticleDigest' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics } from '~/common/utils' @@ -37,51 +36,55 @@ export const SIDEBAR_TOPICS = gql` ${ArticleDigest.Sidebar.fragments.article} ` -export default () => ( - <> - - {({ data, loading, error }: QueryResult & { data: SidebarTopics }) => { - const edges = _get(data, 'viewer.recommendation.topics.edges', []) +const Topics = () => { + const { data, loading, error } = useQuery(SIDEBAR_TOPICS) + const edges = data && data.viewer && data.viewer.recommendation.topics.edges - if (!edges || edges.length <= 0) { - return null - } + if (loading) { + return + } + + if (error) { + return + } + + if (!edges || edges.length <= 0) { + return null + } + + return ( + <> +
    + + +
    + +
      + {edges + .filter(({ node }) => !!node.mediaHash) + .map(({ node, cursor }, i) => ( +
    1. + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.TOPICS, + location: i + }) + } + > + +
    2. + ))} +
    - return ( - <> -
    - - -
    + + + ) +} -
      - {edges - .filter(({ node }: { node: any }) => !!node.mediaHash) - .map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    1. - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.TOPICS, - location: i - }) - } - > - -
    2. - ) - )} -
    - - ) - }} -
    - - -) +export default Topics diff --git a/src/views/Home/Sidebar/ViewAllLink/index.tsx b/src/views/Home/Sidebar/ViewAllLink/index.tsx index ba44e74d20..fcfee8bbaf 100644 --- a/src/views/Home/Sidebar/ViewAllLink/index.tsx +++ b/src/views/Home/Sidebar/ViewAllLink/index.tsx @@ -9,7 +9,7 @@ import ICON_ARROW_RIGHT_GREEN_SMALL from '~/static/icons/arrow-right-green-small import styles from './styles.css' -export default ({ type }: { type: 'authors' | 'tags' | 'topics' }) => { +const ViewAllLink = ({ type }: { type: 'authors' | 'tags' | 'topics' }) => { const { lang } = useContext(LanguageContext) const pathMap = { topics: PATHS.TOPICS, @@ -40,8 +40,11 @@ export default ({ type }: { type: 'authors' | 'tags' | 'topics' }) => { })} textPlacement="left" /> + ) } + +export default ViewAllLink diff --git a/src/views/Home/Sidebar/index.tsx b/src/views/Home/Sidebar/index.tsx index 4d784ce48c..0d4f90118c 100644 --- a/src/views/Home/Sidebar/index.tsx +++ b/src/views/Home/Sidebar/index.tsx @@ -11,16 +11,21 @@ export default () => (
    +
    +
    +
    +
    + ) diff --git a/src/views/Me/AppreciationTabs/index.tsx b/src/views/Me/AppreciationTabs/index.tsx index 803d77a1e8..7147bd7b36 100644 --- a/src/views/Me/AppreciationTabs/index.tsx +++ b/src/views/Me/AppreciationTabs/index.tsx @@ -57,6 +57,7 @@ const AppreciationTabs: React.FC & { {activity.appreciationsReceivedTotal} + ) diff --git a/src/views/Me/AppreciationsReceived/index.tsx b/src/views/Me/AppreciationsReceived/index.tsx index e6a6751031..b713d36492 100644 --- a/src/views/Me/AppreciationsReceived/index.tsx +++ b/src/views/Me/AppreciationsReceived/index.tsx @@ -1,16 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, Head, InfiniteScroll, Spinner } from '~/components' import EmptyAppreciation from '~/components/Empty/EmptyAppreciation' -import { Query } from '~/components/GQL' import { Transaction } from '~/components/TransactionDigest' import { ANALYTICS_EVENTS, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' import AppreciationTabs from '../AppreciationTabs' +import { MeAppreciationsReceived } from './__generated__/MeAppreciationsReceived' import styles from './styles.css' const ME_APPRECIATED_RECEIVED = gql` @@ -40,81 +39,81 @@ const ME_APPRECIATED_RECEIVED = gql` ` const AppreciationsReceived = () => { - return ( -
    -
    - + const { data, loading, fetchMore } = useQuery( + ME_APPRECIATED_RECEIVED + ) - - {({ data, loading, error, fetchMore }: QueryResult) => { - if (loading) { - return - } + if (loading) { + return + } - const connectionPath = 'viewer.activity.appreciationsReceived' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: 'appreciationsReceived', - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (!data || !data.viewer) { + return null + } - if (!edges || edges.length <= 0) { - return ( - <> - - - - ) - } + const connectionPath = 'viewer.activity.appreciationsReceived' + const { edges, pageInfo } = data.viewer.activity.appreciationsReceived - return ( - <> - - -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }) => ( -
    • - -
    • - ) - )} -
    -
    - - ) - }} -
    -
    + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + <> + + + + ) + } -
    -
    -
    + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: 'appreciationsReceived', + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } - -
    + return ( + <> + + +
      + {edges.map(({ node, cursor }) => ( +
    • + +
    • + ))} +
    +
    + ) } -export default AppreciationsReceived +export default () => ( +
    +
    + + + +
    + +
    +
    +
    + + +
    +) diff --git a/src/views/Me/AppreciationsSent/index.tsx b/src/views/Me/AppreciationsSent/index.tsx index 2367a10d5f..479b6f08d7 100644 --- a/src/views/Me/AppreciationsSent/index.tsx +++ b/src/views/Me/AppreciationsSent/index.tsx @@ -1,16 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, Head, InfiniteScroll, Spinner } from '~/components' import EmptyAppreciation from '~/components/Empty/EmptyAppreciation' -import { Query } from '~/components/GQL' import { Transaction } from '~/components/TransactionDigest' import { ANALYTICS_EVENTS, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' import AppreciationTabs from '../AppreciationTabs' +import { MeAppreciationsSent } from './__generated__/MeAppreciationsSent' import styles from './styles.css' const ME_APPRECIATIONS_SENT = gql` @@ -40,81 +39,81 @@ const ME_APPRECIATIONS_SENT = gql` ` const AppreciationsSent = () => { - return ( -
    -
    - + const { data, loading, fetchMore } = useQuery( + ME_APPRECIATIONS_SENT + ) - - {({ data, loading, error, fetchMore }: QueryResult) => { - if (loading) { - return - } + if (loading) { + return + } - const connectionPath = 'viewer.activity.appreciationsSent' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: 'appreciationsSent', - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (!data || !data.viewer) { + return null + } - if (!edges || edges.length <= 0) { - return ( - <> - - - - ) - } + const connectionPath = 'viewer.activity.appreciationsSent' + const { edges, pageInfo } = data.viewer.activity.appreciationsSent - return ( - <> - - -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }) => ( -
    • - -
    • - ) - )} -
    -
    - - ) - }} -
    -
    + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + <> + + + + ) + } -
    -
    -
    + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: 'appreciationsSent', + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } - -
    + return ( + <> + + +
      + {edges.map(({ node, cursor }) => ( +
    • + +
    • + ))} +
    +
    + ) } -export default AppreciationsSent +export default () => ( +
    +
    + + + +
    + +
    +
    +
    + + +
    +) diff --git a/src/views/Me/DraftDetail/Content/index.tsx b/src/views/Me/DraftDetail/Content/index.tsx index 96622329cb..ecbf6c3589 100644 --- a/src/views/Me/DraftDetail/Content/index.tsx +++ b/src/views/Me/DraftDetail/Content/index.tsx @@ -4,7 +4,8 @@ import { useContext, useEffect, useState } from 'react' import { fragments as EditorFragments } from '~/components/Editor/fragments' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { SingleFileUpload } from '~/components/GQL/mutations/__generated__/SingleFileUpload' import UPLOAD_FILE from '~/components/GQL/mutations/uploadFile' import { LanguageContext } from '~/components/Language' import { Placeholder } from '~/components/Placeholder' @@ -13,6 +14,7 @@ import { TEXT } from '~/common/enums' import { translate } from '~/common/utils' import { DraftDetailQuery_node_Draft } from '../__generated__/DraftDetailQuery' +import { UpdateDraft } from './__generated__/UpdateDraft' import styles from './styles.css' const Editor = dynamic(() => import('~/components/Editor'), { @@ -51,10 +53,8 @@ const fragments = { const DraftContent: React.FC<{ draft: DraftDetailQuery_node_Draft }> & { fragments: typeof fragments } = ({ draft }) => { - if (!process.browser) { - return null - } - + const [updateDraft] = useMutation(UPDATE_DRAFT) + const [singleFileUpload] = useMutation(UPLOAD_FILE) const { lang } = useContext(LanguageContext) const { updateHeaderState } = useContext(HeaderContext) const [title, setTitle] = useState(draft.title) @@ -66,97 +66,91 @@ const DraftContent: React.FC<{ draft: DraftDetailQuery_node_Draft }> & { setTitle(draft.title) }, [draft.title]) + if (!process.browser) { + return null + } + return ( - - {(updateDraft: any) => ( - <> -
    - +
    + { + setTitle(e.target.value) + }} + onBlur={() => updateDraft({ variables: { id: draft.id, title } })} + /> +
    + + { + const { data } = await singleFileUpload({ + variables: { + input: { + type: 'embed', + entityType: 'draft', + entityId: draft.id, + ...input } - onChange={e => { - setTitle(e.target.value) - }} - onBlur={() => updateDraft({ variables: { id: draft.id, title } })} - /> -
    - - {(singleFileUpload: any, { loading: uploading }: any) => ( - { - const result = await singleFileUpload({ - variables: { - input: { - type: 'embed', - entityType: 'draft', - entityId: draft.id, - ...input - } - } - }) - if (result) { - const { - data: { - singleFileUpload: { id, path } - } - } = result - return { id, path } - } else { - throw new Error('upload not successful') - } - }} - draft={draft} - onSave={async (newDraft: { - title?: string | null - content?: string | null - coverAssetId?: string | null - }) => { - updateHeaderState({ - type: 'draft', - state: 'saving', - draftId - }) - try { - await updateDraft({ - variables: { id: draft.id, ...newDraft } - }) - updateHeaderState({ - type: 'draft', - state: 'saved', - draftId - }) - } catch (e) { - updateHeaderState({ - type: 'draft', - state: 'saveFailed', - draftId - }) - } - }} - /> - )} - + } + }) + const { id, path } = (data && data.singleFileUpload) || {} + + if (id && path) { + return { id, path } + } else { + throw new Error('upload not successful') + } + }} + draft={draft} + onSave={async (newDraft: { + title?: string | null + content?: string | null + coverAssetId?: string | null + }) => { + updateHeaderState({ + type: 'draft', + state: 'saving', + draftId + }) + try { + await updateDraft({ + variables: { id: draft.id, ...newDraft } + }) + updateHeaderState({ + type: 'draft', + state: 'saved', + draftId + }) + } catch (e) { + updateHeaderState({ + type: 'draft', + state: 'saveFailed', + draftId + }) + } + }} + /> - - - )} -
    + + ) } diff --git a/src/views/Me/DraftDetail/PublishState/PendingState.tsx b/src/views/Me/DraftDetail/PublishState/PendingState.tsx index 628b56c683..265072b6bf 100644 --- a/src/views/Me/DraftDetail/PublishState/PendingState.tsx +++ b/src/views/Me/DraftDetail/PublishState/PendingState.tsx @@ -1,7 +1,8 @@ -import { Query } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Translate } from '~/components' import { PublishStateDraft } from '~/components/GQL/fragments/__generated__/PublishStateDraft' +import { DraftPublishState } from '~/components/GQL/queries/__generated__/DraftPublishState' import DRAFT_PUBLISH_STATE from '~/components/GQL/queries/draftPublishState' import { useCountdown } from '~/components/Hook' import { Toast } from '~/components/Toast' @@ -16,41 +17,38 @@ const PendingState = ({ draft }: { draft: PublishStateDraft }) => { } = useCountdown({ timeLeft: Date.parse(scheduledAt) - Date.now() }) const isPublishing = !scheduledAt || !timeLeft || timeLeft <= 0 + useQuery(DRAFT_PUBLISH_STATE, { + variables: { id: draft.id }, + pollInterval: 1000 * 2, + errorPolicy: 'none', + fetchPolicy: 'network-only', + skip: !process.browser || !isPublishing + }) + return ( - - {() => ( - - ) : ( - - ) - } - content={ - - } - buttonPlacement="bottom" + + ) : ( + + ) + } + content={ + - )} - + } + buttonPlacement="bottom" + /> ) } diff --git a/src/views/Me/DraftDetail/PublishState/index.tsx b/src/views/Me/DraftDetail/PublishState/index.tsx index 9b2aee73e5..d515a45d47 100644 --- a/src/views/Me/DraftDetail/PublishState/index.tsx +++ b/src/views/Me/DraftDetail/PublishState/index.tsx @@ -14,8 +14,11 @@ const PublishState = ({ draft }: { draft: PublishStateDraft }) => { return (
    {isPending && } + {isError && } + {isPublished && } +
    ) diff --git a/src/views/Me/DraftDetail/Sidebar/AddCover/index.tsx b/src/views/Me/DraftDetail/Sidebar/AddCover/index.tsx index f4df115e92..79c5b7632a 100644 --- a/src/views/Me/DraftDetail/Sidebar/AddCover/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/AddCover/index.tsx @@ -5,10 +5,11 @@ import { useContext } from 'react' import { Translate } from '~/components' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import Collapsable from '../Collapsable' import { AddCoverDraft } from './__generated__/AddCoverDraft' +import { UpdateDraftCover } from './__generated__/UpdateDraftCover' import styles from './styles.css' const fragments = { @@ -77,6 +78,7 @@ const CoverList = ({ } const AddCover = ({ draft }: { draft: AddCoverDraft }) => { + const [update] = useMutation(UPDATE_COVER) const { updateHeaderState } = useContext(HeaderContext) const { id: draftId, cover, assets } = draft const imageAssets = assets.filter( @@ -101,19 +103,17 @@ const AddCover = ({ draft }: { draft: AddCoverDraft }) => { zh_hans="选择一張圖片作為封面" />

    +
    - - {(update: any) => ( - - )} - +
    + ) diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx b/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx index 7281c7c66a..671aef085b 100644 --- a/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx +++ b/src/views/Me/DraftDetail/Sidebar/AddTags/SearchTags.tsx @@ -1,8 +1,7 @@ import gql from 'graphql-tag' -import _debounce from 'lodash/debounce' -import _get from 'lodash/get' import { useContext, useState } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' +import { useDebounce } from 'use-debounce/lib' import { Dropdown, @@ -12,8 +11,8 @@ import { Spinner, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { INPUT_DEBOUNCE } from '~/common/enums' import { numAbbr, translate } from '~/common/utils' import { @@ -83,7 +82,9 @@ const DropdownContent = ({ ))} + {tags && tags.length > 0 && } + + ) diff --git a/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx b/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx index ff3ede04ad..1e98fcc2c8 100644 --- a/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/AddTags/index.tsx @@ -5,10 +5,11 @@ import { useContext } from 'react' import { Translate } from '~/components' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Mutation } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import Collapsable from '../Collapsable' import { AddTagsDraft } from './__generated__/AddTagsDraft' +import { UpdateDraftTags } from './__generated__/UpdateDraftTags' import SearchTags from './SearchTags' import styles from './styles.css' import Tag from './Tag' @@ -34,8 +35,8 @@ const UPDATE_TAGS = gql` ` const AddTags = ({ draft }: { draft: AddTagsDraft }) => { + const [updateTags] = useMutation(UPDATE_TAGS) const { updateHeaderState } = useContext(HeaderContext) - const draftId = draft.id const tags = draft.tags || [] const hasTags = tags.length > 0 @@ -45,6 +46,28 @@ const AddTags = ({ draft }: { draft: AddTagsDraft }) => { 'tags-container': true, 'u-area-disable': isPending || isPublished }) + const addTag = async (tag: string) => { + updateHeaderState({ type: 'draft', state: 'saving', draftId }) + try { + await updateTags({ + variables: { id: draft.id, tags: _uniq(tags.concat(tag)) } + }) + updateHeaderState({ type: 'draft', state: 'saved', draftId }) + } catch (e) { + updateHeaderState({ type: 'draft', state: 'saveFailed', draftId }) + } + } + const deleteTag = async (tag: string) => { + updateHeaderState({ type: 'draft', state: 'saving', draftId }) + try { + await updateTags({ + variables: { id: draft.id, tags: tags.filter(it => it !== tag) } + }) + updateHeaderState({ type: 'draft', state: 'saved', draftId }) + } catch (e) { + updateHeaderState({ type: 'draft', state: 'saveFailed', draftId }) + } + } return ( { />

    - - {(updateTags: any) => { - const addTag = async (tag: string) => { - updateHeaderState({ type: 'draft', state: 'saving', draftId }) - try { - await updateTags({ - variables: { id: draft.id, tags: _uniq(tags.concat(tag)) } - }) - updateHeaderState({ type: 'draft', state: 'saved', draftId }) - } catch (e) { - updateHeaderState({ type: 'draft', state: 'saveFailed', draftId }) - } - } - const deleteTag = async (tag: string) => { - updateHeaderState({ type: 'draft', state: 'saving', draftId }) - try { - await updateTags({ - variables: { id: draft.id, tags: tags.filter(it => it !== tag) } - }) - updateHeaderState({ type: 'draft', state: 'saved', draftId }) - } catch (e) { - updateHeaderState({ type: 'draft', state: 'saveFailed', draftId }) - } - } - - return ( -
    - {tags.map(tag => ( - - ))} - -
    - ) - }} -
    +
    + {tags.map(tag => ( + + ))} + +
    diff --git a/src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx b/src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx index b22801968e..076791facc 100644 --- a/src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/Collapsable/index.tsx @@ -1,5 +1,4 @@ -import _get from 'lodash/get' -import { FC, ReactNode, useState } from 'react' +import { ReactNode, useState } from 'react' import { Icon, TextIcon } from '~/components' @@ -7,11 +6,10 @@ import ICON_COLLAPSE_BRANCH from '~/static/icons/collapse-branch.svg?sprite' import styles from './styles.css' -const Collapsable: FC<{ title: ReactNode; defaultCollapsed?: boolean }> = ({ - children, - title, - defaultCollapsed = true -}) => { +const Collapsable: React.FC<{ + title: ReactNode + defaultCollapsed?: boolean +}> = ({ children, title, defaultCollapsed = true }) => { const [collapsed, toggleCollapse] = useState(defaultCollapsed) return ( diff --git a/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx b/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx index 815791dd19..6546cc9f97 100644 --- a/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/CollectArticles/index.tsx @@ -1,18 +1,19 @@ import classNames from 'classnames' import gql from 'graphql-tag' -import _get from 'lodash/get' import _uniq from 'lodash/uniq' import dynamic from 'next/dynamic' import { useContext } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, Spinner, Translate } from '~/components' +import { DropdownDigestArticle } from '~/components/ArticleDigest/DropdownDigest/__generated__/DropdownDigestArticle' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Mutation, Query } from '~/components/GQL' +import { QueryError, useMutation } from '~/components/GQL' import Collapsable from '../Collapsable' import { CollectArticlesDraft } from './__generated__/CollectArticlesDraft' import { DraftCollectionQuery } from './__generated__/DraftCollectionQuery' +import { SetDraftCollection } from './__generated__/SetDraftCollection' import styles from './styles.css' const CollectionEditor = dynamic( @@ -78,9 +79,8 @@ const CollectArticles = ({ draft }: { draft: CollectArticlesDraft }) => { container: true, 'u-area-disable': isPending || isPublished }) - - const handleCollectionChange = (setCollection: any) => async ( - articles: any[] + const handleCollectionChange = () => async ( + articles: DropdownDigestArticle[] ) => { updateHeaderState({ type: 'draft', @@ -108,6 +108,20 @@ const CollectArticles = ({ draft }: { draft: CollectArticlesDraft }) => { } } + const [setCollection] = useMutation(SET_DRAFT_COLLECTION) + const { data, loading, error } = useQuery( + DRAFT_COLLECTION, + { + variables: { id: draftId } + } + ) + const edges = + data && + data.node && + data.node.__typename === 'Draft' && + data.node.collection && + data.node.collection.edges + return ( } @@ -121,29 +135,14 @@ const CollectArticles = ({ draft }: { draft: CollectArticlesDraft }) => {

    - - {({ - data, - loading - }: QueryResult & { data: DraftCollectionQuery }) => { - const edges = _get(data, 'node.collection.edges') + {loading && } - if (loading || !edges) { - return - } + {error && } - return ( - - {(setCollection: any) => ( - node)} - onEdit={handleCollectionChange(setCollection)} - /> - )} - - ) - }} - + node)) || []} + onEdit={handleCollectionChange()} + />
    diff --git a/src/views/Me/DraftDetail/Sidebar/DraftList/index.tsx b/src/views/Me/DraftDetail/Sidebar/DraftList/index.tsx index b9aa0512e1..bb9dfcfd50 100644 --- a/src/views/Me/DraftDetail/Sidebar/DraftList/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/DraftList/index.tsx @@ -1,9 +1,7 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { DraftDigest, Spinner, Translate } from '~/components' -import { Query } from '~/components/GQL' import { TEXT } from '~/common/enums' @@ -30,31 +28,28 @@ const ME_DRAFTS_SIDEBAR = gql` ${DraftDigest.Sidebar.fragments.draft} ` -const DraftList = ({ currentId }: { currentId: string }) => ( - - } - > - - {({ data, loading }: QueryResult & { data: MeDraftsSidebar }) => { - const edges = _get(data, 'viewer.drafts.edges') - - if (loading || !edges) { - return - } +const DraftList = ({ currentId }: { currentId: string }) => { + const { data, loading } = useQuery(ME_DRAFTS_SIDEBAR) + const edges = data && data.viewer && data.viewer.drafts.edges - return edges + return ( + + } + > + {loading && } + {edges && + edges .filter( ({ node }: MeDraftsSidebar_viewer_drafts_edges) => node.id !== currentId ) .map(({ node }: MeDraftsSidebar_viewer_drafts_edges, i: number) => ( - )) - }} - - -) + ))} +
    + ) +} export default DraftList diff --git a/src/views/Me/DraftDetail/index.tsx b/src/views/Me/DraftDetail/index.tsx index bf66a12b25..05e6a9345a 100644 --- a/src/views/Me/DraftDetail/index.tsx +++ b/src/views/Me/DraftDetail/index.tsx @@ -1,17 +1,16 @@ import gql from 'graphql-tag' import { useRouter } from 'next/router' import { useContext, useEffect } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { fragments as EditorFragments } from '~/components/Editor/fragments' -import EmptyDraft from '~/components/Empty/EmptyDraft' import { HeaderContext } from '~/components/GlobalHeader/Context' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { Head } from '~/components/Head' -import { Translate } from '~/components/Language' import { PublishModal } from '~/components/Modal/PublishModal' import { ModalInstance } from '~/components/ModalManager' import { Placeholder } from '~/components/Placeholder' +import Throw404 from '~/components/Throw404' import { getQuery } from '~/common/utils' @@ -40,57 +39,58 @@ const DRAFT_DETAIL = gql` const DraftDetail = () => { const router = useRouter() const id = getQuery({ router, key: 'id' }) - - if (!id) { - return null - } - const { updateHeaderState } = useContext(HeaderContext) useEffect(() => { updateHeaderState({ type: 'draft', state: '', draftId: id }) - return () => updateHeaderState({ type: 'default' }) }, []) + const { data, loading, error } = useQuery(DRAFT_DETAIL, { + variables: { id } + }) + + if (error) { + return + } + + if (!loading && (!data || !data.node || data.node.__typename !== 'Draft')) { + return + } + + const draft = + data && data.node && data.node.__typename === 'Draft' && data.node + return ( - - {({ data, loading, error }: QueryResult & { data: DraftDetailQuery }) => ( -
    - - -
    - {loading && } - {!loading && data && data.node && ( - <> - - - - )} - {!loading && (error || (data && !data.node)) && ( - - } - /> - )} -
    - - - - - {(props: ModalInstanceProps) => ( - - )} - - - -
    +
    + + +
    + {loading && } + + {!loading && draft && ( + <> + + + + )} +
    + + + + {draft && ( + + {(props: ModalInstanceProps) => ( + + )} + )} - + + +
    ) } diff --git a/src/views/Me/Notifications/index.tsx b/src/views/Me/Notifications/index.tsx index 080da86bf0..d778eb0f9f 100644 --- a/src/views/Me/Notifications/index.tsx +++ b/src/views/Me/Notifications/index.tsx @@ -1,6 +1,5 @@ -import _get from 'lodash/get' import { useEffect } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, @@ -11,8 +10,10 @@ import { Translate } from '~/components' import EmptyNotice from '~/components/Empty/EmptyNotice' -import { Mutation, Query } from '~/components/GQL' +import { useMutation } from '~/components/GQL' +import { MarkAllNoticesAsRead } from '~/components/GQL/mutations/__generated__/MarkAllNoticesAsRead' import MARK_ALL_NOTICES_AS_READ from '~/components/GQL/mutations/markAllNoticesAsRead' +import { MeNotifications } from '~/components/GQL/queries/__generated__/MeNotifications' import { ME_NOTIFICATIONS } from '~/components/GQL/queries/notice' import updateViewerUnreadNoticeCount from '~/components/GQL/updates/viewerUnreadNoticeCount' import NoticeDigest from '~/components/NoticeDigest' @@ -21,7 +22,67 @@ import { mergeConnections } from '~/common/utils' import styles from './styles.css' -const Notifications = () => ( +const Notifications = () => { + const [markAllNoticesAsRead] = useMutation( + MARK_ALL_NOTICES_AS_READ, + { + update: updateViewerUnreadNoticeCount + } + ) + const { data, loading, fetchMore } = useQuery< + MeNotifications, + { first: number; after?: number } + >(ME_NOTIFICATIONS, { + variables: { first: 20 } + }) + + useEffect(() => { + if (!loading) { + markAllNoticesAsRead() + } + }, []) + + const connectionPath = 'viewer.notices' + const { edges, pageInfo } = (data && data.viewer && data.viewer.notices) || {} + + if (loading) { + return + } + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => + fetchMore({ + variables: { + first: 20, + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + + return ( + +
      + {edges.map(({ node, cursor }) => ( +
    • + +
    • + ))} + + +
    +
    + ) +} + +export default () => (
    @@ -31,63 +92,7 @@ const Notifications = () => ( />
    - - {({ data, loading, error, fetchMore }: QueryResult) => { - if (loading) { - return - } - - const connectionPath = 'viewer.notices' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => - fetchMore({ - variables: { - first: 20, - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - - if (!edges || edges.length <= 0) { - return - } - - return ( - - {(markAllNoticesAsRead: any) => { - useEffect(() => { - markAllNoticesAsRead() - }, []) - - return ( - -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }) => ( -
    • - -
    • - ) - )} -
    -
    - ) - }} -
    - ) - }} -
    +
    @@ -98,5 +103,3 @@ const Notifications = () => (
    ) - -export default Notifications diff --git a/src/views/Me/Settings/Account/AccountSettings.tsx b/src/views/Me/Settings/Account/AccountSettings.tsx index 988ddcfab7..d171774f71 100644 --- a/src/views/Me/Settings/Account/AccountSettings.tsx +++ b/src/views/Me/Settings/Account/AccountSettings.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import { useContext } from 'react' import { Button, PageHeader, Translate } from '~/components' @@ -51,7 +50,7 @@ const ChangePasswrodButton = () => ( ) -export default () => { +const AccountSettings = () => { const viewer = useContext(ViewerContext) return ( @@ -89,7 +88,7 @@ export default () => { zh_hans={TEXT.zh_hans.email} /> - {_get(viewer, 'info.email')} + {viewer.info.email} @@ -122,3 +121,5 @@ export default () => { ) } + +export default AccountSettings diff --git a/src/views/Me/Settings/Account/LanguageSwitch.tsx b/src/views/Me/Settings/Account/LanguageSwitch.tsx index 1b5fdd349d..bfeea96b9f 100644 --- a/src/views/Me/Settings/Account/LanguageSwitch.tsx +++ b/src/views/Me/Settings/Account/LanguageSwitch.tsx @@ -76,6 +76,7 @@ const DropdownContent: React.FC<{ hideDropdown: () => void }> = ({ {textMap.zh_hant}
    + + ) diff --git a/src/views/Me/Settings/Account/SettingsAccount.tsx b/src/views/Me/Settings/Account/SettingsAccount.tsx index 45f0973a45..a5b1865444 100644 --- a/src/views/Me/Settings/Account/SettingsAccount.tsx +++ b/src/views/Me/Settings/Account/SettingsAccount.tsx @@ -1,5 +1,3 @@ -import _get from 'lodash/get' - import { Head } from '~/components' import { TEXT } from '~/common/enums' diff --git a/src/views/Me/Settings/Account/UISettings.tsx b/src/views/Me/Settings/Account/UISettings.tsx index ff3c4dfdca..cfe085e868 100644 --- a/src/views/Me/Settings/Account/UISettings.tsx +++ b/src/views/Me/Settings/Account/UISettings.tsx @@ -1,5 +1,3 @@ -import _get from 'lodash/get' - import { PageHeader, Translate } from '~/components' import { TEXT } from '~/common/enums' diff --git a/src/views/Me/Settings/Account/WalletSettings.tsx b/src/views/Me/Settings/Account/WalletSettings.tsx index 91d69d733a..2802cf3381 100644 --- a/src/views/Me/Settings/Account/WalletSettings.tsx +++ b/src/views/Me/Settings/Account/WalletSettings.tsx @@ -1,16 +1,15 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useContext } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Icon, PageHeader, TextIcon, Translate } from '~/components' -import { Query } from '~/components/GQL' import { ModalSwitch } from '~/components/ModalManager' import { ViewerContext } from '~/components/Viewer' import { TEXT } from '~/common/enums' import ICON_ARROW_RIGHT_GREEN from '~/static/icons/arrow-right-green.svg?sprite' +import { ViewerLikeInfo } from './__generated__/ViewerLikeInfo' import styles from './styles.css' const VIEWER_LIKE_INFO = gql` @@ -39,7 +38,73 @@ const SetupLikerIdButton = () => ( const WalletSetting = () => { const viewer = useContext(ViewerContext) - const likerId = _get(viewer, 'likerId') + const likerId = viewer.likerId + const { data, loading, error } = useQuery(VIEWER_LIKE_INFO, { + errorPolicy: 'none' + }) + const LIKE = + data && data.viewer && data.viewer.status && data.viewer.status.LIKE + + if (loading || error || !LIKE) { + return null + } + + const USDPrice = (LIKE.rateUSD * LIKE.total).toFixed(2) + + return ( +
    +
    + + + + + {likerId && ( + + {LIKE.total} LikeCoin + {USDPrice && ( + + {' '} + {LIKE.total > 0 ? '≈' : '='} {USDPrice} USD + + )} + + )} + {!likerId && ( + + + + )} +
    + + {likerId && ( + + + } + textPlacement="left" + weight="medium" + > + + + + )} + + +
    + ) +} + +const WalletSettings = () => { + const viewer = useContext(ViewerContext) + const likerId = viewer.likerId return (
    @@ -61,75 +126,11 @@ const WalletSetting = () => {
    - - {({ data, loading, error }: QueryResult) => { - const LIKE = _get(data, 'viewer.status.LIKE') - - if (loading || error || !LIKE) { - return null - } - - const USDPrice = (LIKE.rateUSD * LIKE.total).toFixed(2) - - return ( -
    -
    - - - - {likerId && ( - - {LIKE.total} LikeCoin - {USDPrice && ( - - {' '} - {LIKE.total > 0 ? '≈' : '='} {USDPrice} USD - - )} - - )} - {!likerId && ( - - - - )} -
    - - {likerId && ( - - - } - textPlacement="left" - weight="medium" - > - - - - )} -
    - ) - }} -
    + ) } -export default WalletSetting +export default WalletSettings diff --git a/src/views/Me/Settings/Blocked/SettingsBlocked.tsx b/src/views/Me/Settings/Blocked/SettingsBlocked.tsx new file mode 100644 index 0000000000..588b9f7a16 --- /dev/null +++ b/src/views/Me/Settings/Blocked/SettingsBlocked.tsx @@ -0,0 +1,131 @@ +import gql from 'graphql-tag' +import { useQuery } from 'react-apollo' + +import { + Head, + InfiniteScroll, + PageHeader, + Spinner, + Translate +} from '~/components' +import EmptyWarning from '~/components/Empty/EmptyWarning' +import { QueryError } from '~/components/GQL' +import { UserDigest } from '~/components/UserDigest' + +import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' +import { analytics, mergeConnections } from '~/common/utils' + +import { ViewerBlockList } from './__generated__/ViewerBlockList' + +const VIEWER_BLOCK_LIST = gql` + query ViewerBlockList($after: String) { + viewer { + id + blockList(input: { first: 10, after: $after }) { + pageInfo { + startCursor + endCursor + hasNextPage + } + edges { + cursor + node { + ...UserDigestFullDescUser + } + } + } + } + } + ${UserDigest.FullDesc.fragments.user} +` + +const SettingsBlocked = () => { + const { data, loading, error, fetchMore } = useQuery( + VIEWER_BLOCK_LIST + ) + + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.blockList' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.blockList) || {} + + const filteredUsers = (edges || []).filter(({ node }) => node.isBlocked) + + if (!edges || edges.length <= 0 || filteredUsers.length <= 0 || !pageInfo) { + return ( + + } + /> + ) + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.ALL_AUTHORS, + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + + return ( + +
      + {filteredUsers.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.ALL_AUTHORS, + location: i + }) + } + > + +
    • + ))} +
    +
    + ) +} + +export default () => ( + <> + + + + } + is="h2" + /> + + + +) diff --git a/src/views/Me/Settings/Blocked/index.tsx b/src/views/Me/Settings/Blocked/index.tsx new file mode 100644 index 0000000000..fb3fdca1cf --- /dev/null +++ b/src/views/Me/Settings/Blocked/index.tsx @@ -0,0 +1,15 @@ +import SettingsTab from '../SettingsTab' +import SettingsBlocked from './SettingsBlocked' + +export default () => ( +
    +
    +
    + +
    +
    + +
    +
    +
    +) diff --git a/src/views/Me/Settings/Notification/SettingsNotification.tsx b/src/views/Me/Settings/Notification/SettingsNotification.tsx index a41d6e0ba2..1de838ef6d 100644 --- a/src/views/Me/Settings/Notification/SettingsNotification.tsx +++ b/src/views/Me/Settings/Notification/SettingsNotification.tsx @@ -1,13 +1,13 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Head, PageHeader, Translate } from '~/components' -import { Mutation, Query } from '~/components/GQL' +import { useMutation } from '~/components/GQL' import { Switch } from '~/components/Switch' import { TEXT } from '~/common/enums' +import { UpdateViewerNotification } from './__generated__/UpdateViewerNotification' import { ViewerNotificationSettings } from './__generated__/ViewerNotificationSettings' import styles from './styles.css' @@ -125,126 +125,127 @@ const settingsMap = { ] } -const SettingsNotification = () => ( - - {({ data }: QueryResult & { data: ViewerNotificationSettings }) => { - const settings = _get(data, 'viewer.settings.notification', {}) - - return ( - - {(updateNotification: any) => { - const onChange = (type: string) => - updateNotification({ - variables: { - type, - enabled: !settings[type] - }, - optimisticResponse: { - updateNotificationSetting: { - id: data.viewer.id, - settings: { - notification: { - [type]: !settings[type], - __typename: 'NotificationSetting' - }, - __typename: 'UserSettings' - }, - __typename: 'User' - } - } - }) - - return ( - <> - - -
    -
    - - } - is="h2" - /> - - {settingsMap.me.map(setting => ( -
    - {setting.title} - onChange(setting.key)} - /> -
    - ))} -
    - -
    - } - is="h2" - /> - - {settingsMap.others.map(setting => ( -
    - {setting.title} - onChange(setting.key)} - /> -
    - ))} -
    -
    - -
    -
    - } - is="h2" - /> - - {settingsMap.article.map(setting => ( -
    - {setting.title} - onChange(setting.key)} - /> -
    - ))} -
    - -
    - } - is="h2" - /> - - {settingsMap.comment.map(setting => ( -
    - {setting.title} - onChange(setting.key)} - /> -
    - ))} -
    -
    - - - - ) - }} -
    - ) - }} -
    -) +const SettingsNotification = () => { + const [updateNotification] = useMutation( + UPDATE_VIEWER_NOTIFICATION + ) + const { data } = useQuery( + VIEWER_NOTIFICATION_SETTINGS + ) + const settings: any = + (data && data.viewer && data.viewer.settings.notification) || {} + const id = data && data.viewer && data.viewer.id + + const onChange = (type: string) => { + if (!id) { + return + } + updateNotification({ + variables: { + type, + enabled: !settings[type] + }, + optimisticResponse: { + updateNotificationSetting: { + id, + settings: { + notification: { + ...settings, + [type]: !settings[type], + __typename: 'NotificationSetting' + }, + __typename: 'UserSettings' + }, + __typename: 'User' + } + } + }) + } + + return ( + <> + + +
    +
    + } + is="h2" + /> + + {settingsMap.me.map(setting => ( +
    + {setting.title} + onChange(setting.key)} + /> +
    + ))} +
    + +
    + } + is="h2" + /> + + {settingsMap.others.map(setting => ( +
    + {setting.title} + onChange(setting.key)} + /> +
    + ))} +
    +
    + +
    +
    + } + is="h2" + /> + + {settingsMap.article.map(setting => ( +
    + {setting.title} + onChange(setting.key)} + /> +
    + ))} +
    + +
    + } + is="h2" + /> + + {settingsMap.comment.map(setting => ( +
    + {setting.title} + onChange(setting.key)} + /> +
    + ))} +
    +
    + + + + ) +} export default SettingsNotification diff --git a/src/views/Me/Settings/SettingsTab/index.tsx b/src/views/Me/Settings/SettingsTab/index.tsx index babcf357c0..0e90eef85d 100644 --- a/src/views/Me/Settings/SettingsTab/index.tsx +++ b/src/views/Me/Settings/SettingsTab/index.tsx @@ -20,6 +20,7 @@ const SettingsTabs = () => { + @@ -32,6 +33,17 @@ const SettingsTabs = () => { + + + + + + + + ) } diff --git a/src/views/Misc/About/index.tsx b/src/views/Misc/About/index.tsx index 0d6a8c16e5..8e64f9b525 100644 --- a/src/views/Misc/About/index.tsx +++ b/src/views/Misc/About/index.tsx @@ -12,8 +12,9 @@ import Reports from './Reports' import Slogan from './Slogan' import styles from './styles.css' -export default () => { +const About = () => { const { updateHeaderState } = useContext(HeaderContext) + useEffect(() => { updateHeaderState({ type: 'about', bgColor: 'transparent' }) return () => updateHeaderState({ type: 'default' }) @@ -37,3 +38,5 @@ export default () => { ) } + +export default About diff --git a/src/views/Misc/FAQ/index.tsx b/src/views/Misc/FAQ/index.tsx index ce7d4903ad..893dc157f3 100644 --- a/src/views/Misc/FAQ/index.tsx +++ b/src/views/Misc/FAQ/index.tsx @@ -10,7 +10,7 @@ import { translate } from '~/common/utils' import MiscTab from '../MiscTab' import content from './content' -export default () => { +const FAQ = () => { const { lang } = useContext(LanguageContext) return ( @@ -50,3 +50,5 @@ export default () => { ) } + +export default FAQ diff --git a/src/views/Misc/Guide/index.tsx b/src/views/Misc/Guide/index.tsx index dfa6e2f862..4777b0bc9a 100644 --- a/src/views/Misc/Guide/index.tsx +++ b/src/views/Misc/Guide/index.tsx @@ -9,7 +9,7 @@ import { translate } from '~/common/utils' import MiscTab from '../MiscTab' import content from './content' -export default () => { +const Guide = () => { const { lang } = useContext(LanguageContext) return ( @@ -41,8 +41,11 @@ export default () => { className="u-content" /> + ) } + +export default Guide diff --git a/src/views/OAuth/Authorize/index.tsx b/src/views/OAuth/Authorize/index.tsx index b0b8c9570c..6fe5169f08 100644 --- a/src/views/OAuth/Authorize/index.tsx +++ b/src/views/OAuth/Authorize/index.tsx @@ -4,16 +4,16 @@ import Link from 'next/link' import { useRouter } from 'next/router' import queryString from 'query-string' import { useContext } from 'react' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { LanguageContext, Modal, Spinner, Translate } from '~/components' -import { Query } from '~/components/GQL' import OAuth from '~/components/OAuth' import Throw404 from '~/components/Throw404' import { PATHS, TEXT } from '~/common/enums' import { appendTarget, getQuery, toReadableScope } from '~/common/utils' +import { OAuthClientInfo } from './__generated__/OAuthClientInfo' import styles from './styles.css' const { @@ -44,118 +44,114 @@ const OAuthAuthorize = () => { const scope = getQuery({ router, key: 'scope' }) const redirectUri = getQuery({ router, key: 'redirect_uri' }) + const { data, loading } = useQuery(OAUTH_CLIENT_INFO, { + variables: { id: clientId } + }) + if (!clientId) { return } - return ( - - {({ data, loading }: QueryResult) => { - if (loading) { - return ( -
    - - - -
    - ) - } + if (loading) { + return ( +
    + + + +
    + ) + } - if (!data || !data.oauthClient || !data.oauthClient.id) { - return - } + if (!data || !data.oauthClient || !data.oauthClient.id) { + return + } - const { avatar, website, name, scope: scopes } = data.oauthClient + const { avatar, website, name, scope: scopes } = data.oauthClient - return ( -
    - - - {name} + return ( +
    + + + {name} + + + + } + titleAlign="left" + > +
    + + {state && } + {scope && } + {redirectUri && ( + + )} + + +
    +
      +
    • + +
    • + {scopes && + scopes.map((s: any) => { + const readableScope = toReadableScope({ + scope: s, + lang + }) + + if (!readableScope) { + return null + } + + return
    • {readableScope}
    • + })} +
    + +
    + +

    + + + + {/* FIXME: only render at CSR to get correct `appendTarget` */} + {process.browser && ( + + + - - - } - titleAlign="left" - > - - - {state && } - {scope && } - {redirectUri && ( - - )} - - -

    -
      -
    • - -
    • - {scopes.map((s: any) => { - const readableScope = toReadableScope({ - scope: s, - lang - }) - - if (!readableScope) { - return null - } - - return
    • {readableScope}
    • - })} -
    - -
    - -

    - - - - {/* FIXME: only render at CSR to get correct `appendTarget` */} - {process.browser && ( - - - - - - )} -

    -
    - -
    - - - -
    - - - -
    - ) - }} - + + )} +

    + + +
    + + + +
    + +
    + + +
    ) } diff --git a/src/views/OAuth/Callback/Failure/index.tsx b/src/views/OAuth/Callback/Failure/index.tsx index fe64211510..0d61f0e5c2 100644 --- a/src/views/OAuth/Callback/Failure/index.tsx +++ b/src/views/OAuth/Callback/Failure/index.tsx @@ -62,6 +62,7 @@ const OAuthCallbackFailure = () => { )} + ) diff --git a/src/views/OAuth/Callback/Success/index.tsx b/src/views/OAuth/Callback/Success/index.tsx index 228cbe2c16..6f5d0af5ed 100644 --- a/src/views/OAuth/Callback/Success/index.tsx +++ b/src/views/OAuth/Callback/Success/index.tsx @@ -40,6 +40,7 @@ const OAuthCallbackSuccess = () => {

    + ) diff --git a/src/views/Search/SearchArticles/index.tsx b/src/views/Search/SearchArticles/index.tsx index 1340133365..2466f72702 100644 --- a/src/views/Search/SearchArticles/index.tsx +++ b/src/views/Search/SearchArticles/index.tsx @@ -1,6 +1,6 @@ import gql from 'graphql-tag' import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, @@ -8,10 +8,9 @@ import { LoadMore, PageHeader, Placeholder, - Responsive, Translate } from '~/components' -import { Query } from '~/components/GQL' +import { useResponsive } from '~/components/Hook' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -48,108 +47,93 @@ const SEARCH_ARTICLES = gql` ` const SearchArticles = ({ q }: { q: string }) => { - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: SeachArticles }) => { - if (loading && !_get(data, 'search')) { - return - } + const isMediumUp = useResponsive({ type: 'medium-up' })() + const { data, loading, fetchMore } = useQuery( + SEARCH_ARTICLES, + { + variables: { key: q, first: 10 }, + notifyOnNetworkStatusChange: true + } + ) - const connectionPath = 'search' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.SEARCH_ARTICLE, - location: edges.length, - entrance: q - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (loading && !(data && data.search)) { + return + } - if (!edges || edges.length <= 0) { - return ( - - } - /> - ) + const connectionPath = 'search' + const { edges, pageInfo } = (data && data.search) || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } + /> + ) + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.SEARCH_ARTICLE, + location: edges.length, + entrance: q + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } - return ( - - {(match: boolean) => ( - + + } + /> +
      + {edges.map( + ({ node, cursor }, i) => + node.__typename === 'Article' && ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.SEARCH_ARTICLE, + location: i, + entrance: q + }) + } > - - } - /> -
        - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
      • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.SEARCH_ARTICLE, - location: i, - entrance: q - }) - } - > - -
      • - ) - )} -
      + +
    • + ) + )} +
    - {!match && pageInfo.hasNextPage && ( - - )} -
    - )} -
    - ) - }} -
    + {!isMediumUp && pageInfo.hasNextPage && ( + + )} + ) } diff --git a/src/views/Search/SearchPageHeader/index.tsx b/src/views/Search/SearchPageHeader/index.tsx index bc5ff1a956..8529ced9cd 100644 --- a/src/views/Search/SearchPageHeader/index.tsx +++ b/src/views/Search/SearchPageHeader/index.tsx @@ -1,4 +1,3 @@ -import _get from 'lodash/get' import Link from 'next/link' import { Icon, TextIcon, Translate } from '~/components' @@ -48,9 +47,11 @@ const SearchPageHeader = ({ {q}  +
    {!isAggregate && }
    + ) diff --git a/src/views/Search/SearchTags/index.tsx b/src/views/Search/SearchTags/index.tsx index 687397b4f2..c374fff5e9 100644 --- a/src/views/Search/SearchTags/index.tsx +++ b/src/views/Search/SearchTags/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { InfiniteScroll, @@ -9,14 +8,16 @@ import { Tag, Translate } from '~/components' -import { Query } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' import EmptySearch from '../EmptySearch' import ViewAll from '../ViewAll' -import { SeachTags } from './__generated__/SeachTags' +import { + SeachTags, + SeachTags_search_edges_node_Tag +} from './__generated__/SeachTags' import styles from './styles.css' const SEARCH_TAGS = gql` @@ -69,123 +70,117 @@ const EmptySearchResult = () => { } const SearchTag = ({ q, isAggregate }: { q: string; isAggregate: boolean }) => { - return ( - <> - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: SeachTags }) => { - if (loading) { - return - } + const { data, loading, fetchMore } = useQuery(SEARCH_TAGS, { + variables: { key: q, first: isAggregate ? 3 : 20 } + }) - const connectionPath = 'search' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.SEARCH_TAG, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - const leftEdges = edges.filter((_: any, i: number) => i % 2 === 0) - const rightEdges = edges.filter((_: any, i: number) => i % 2 === 1) + if (loading) { + return + } - if (data.search.edges.length <= 0) { - return isAggregate ? null : - } + const connectionPath = 'search' + const { edges, pageInfo } = (data && data.search) || {} - if (isAggregate) { - return ( -
    -
    -
      - {data.search.edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.SEARCH_TAG, - location: i, - entrance: q - }) - } - > - -
    • - ) - )} -
    -
    - ) - } + if (!edges || edges.length <= 0 || !pageInfo) { + return null + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.SEARCH_TAG, + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + const leftEdges = edges.filter((_, i) => i % 2 === 0) + const rightEdges = edges.filter((_, i) => i % 2 === 1) - return ( -
    - + } + + if (isAggregate) { + return ( +
    +
    + +
      + {edges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.SEARCH_TAG, + location: i, + entrance: q + }) + } + > + +
    • + ))} +
    + + +
    + ) + } + + return ( +
    + +
    +
    +
      + {leftEdges.map(({ node, cursor }) => ( +
    • + +
    • + ))} +
    +
      + {rightEdges.map(({ node, cursor }, i) => ( +
    • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.SEARCH_TAG, + location: i, + entrance: q + }) + } > -
      -
      -
        - {leftEdges.map( - ({ node, cursor }: { node: any; cursor: any }) => ( -
      • - -
      • - ) - )} -
      -
        - {rightEdges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
      • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.SEARCH_TAG, - location: i, - entrance: q - }) - } - > - -
      • - ) - )} -
      -
      - -
    - ) - }} - + + + ))} +
+ + + - + ) } diff --git a/src/views/Search/SearchUsers/index.tsx b/src/views/Search/SearchUsers/index.tsx index 79f8b4d13c..769a490902 100644 --- a/src/views/Search/SearchUsers/index.tsx +++ b/src/views/Search/SearchUsers/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { InfiniteScroll, @@ -9,7 +8,6 @@ import { Translate, UserDigest } from '~/components' -import { Query } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -74,82 +72,70 @@ const SearchUser = ({ q: string isAggregate: boolean }) => { - return ( - <> - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: SeachUsers }) => { - if (loading) { - return - } + const { data, loading, fetchMore } = useQuery(SEARCH_USERS, { + variables: { key: q, first: isAggregate ? 3 : 10 } + }) - const connectionPath = 'search' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.SEARCH_USER, - location: edges.length, - entrance: q - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (loading) { + return + } - if (!edges || edges.length <= 0) { - return isAggregate ? null : - } + const connectionPath = 'search' + const { edges, pageInfo } = (data && data.search) || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return isAggregate ? null : + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.SEARCH_USER, + location: edges.length, + entrance: q + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + + return ( +
+ +
+
    + {edges.map( + ({ node, cursor }, i) => + node.__typename === 'User' && ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.SEARCH_USER, + location: i, + entrance: q + }) + } + > + +
  • + ) + )} +
+ - return ( -
- -
-
    - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
  • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.SEARCH_USER, - location: i, - entrance: q - }) - } - > - -
  • - ) - )} -
- -
- ) - }} - - +
) } diff --git a/src/views/Search/index.tsx b/src/views/Search/index.tsx index 05200f916c..8ea9f58049 100644 --- a/src/views/Search/index.tsx +++ b/src/views/Search/index.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router' -import { Footer, Head, Responsive, SearchBar } from '~/components' +import { Footer, Head, SearchBar } from '~/components' +import { useResponsive } from '~/components/Hook' import { getQuery } from '~/common/utils' @@ -30,6 +31,7 @@ const EmptySeachPage = () => { } const Search = () => { + const isMedium = useResponsive({ type: 'medium' })() const router = useRouter() const type = getQuery({ router, key: 'type' }) const q = getQuery({ router, key: 'q' }) @@ -46,11 +48,11 @@ const Search = () => {
- + {isMedium && (
-
+ )} @@ -67,6 +69,7 @@ const Search = () => {
+
) diff --git a/src/views/TagDetail/index.tsx b/src/views/TagDetail/index.tsx index 2256a45cb1..a18aacf174 100644 --- a/src/views/TagDetail/index.tsx +++ b/src/views/TagDetail/index.tsx @@ -1,7 +1,6 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, @@ -12,8 +11,7 @@ import { Placeholder } from '~/components' import EmptyTag from '~/components/Empty/EmptyTag' -import { Query } from '~/components/GQL' -import Throw404 from '~/components/Throw404' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -54,94 +52,87 @@ const TAG_DETAIL = gql` const TagDetail = () => { const router = useRouter() - if (!router || !router.query || !router.query.id) { + const { data, loading, error, fetchMore } = useQuery( + TAG_DETAIL, + { + variables: { id: router.query.id } + } + ) + + if (loading) { + return + } + + if (error) { + return + } + + if (!data || !data.node || data.node.__typename !== 'Tag') { + return + } + + const id = data.node.id + const connectionPath = 'node.articles' + const { edges, pageInfo } = data.node.articles || {} + + if (!edges || edges.length <= 0 || !pageInfo) { return } + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.TAG_DETAIL, + location: edges.length, + entrance: id + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + return ( -
-
- - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: TagDetailArticles }) => { - if (loading) { - return - } + <> + - if (!data.node) { - return - } + - const connectionPath = 'node.articles' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.TAG_DETAIL, - location: edges.length, - entrance: data.node.id - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath +
+ +
    + {edges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.TAG_DETAIL, + location: i, + entrance: id }) - }) - } + } + > + +
  • + ))} +
+
+
+ + ) +} - return ( - <> - - - - -
- -
    - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
  • - analytics.trackEvent( - ANALYTICS_EVENTS.CLICK_FEED, - { - type: FEED_TYPE.TAG_DETAIL, - location: i, - entrance: data.node.id - } - ) - } - > - -
  • - ) - )} -
-
-
- - ) - }} -
+export default () => { + return ( +
+
+
) } - -export default TagDetail diff --git a/src/views/Tags/index.tsx b/src/views/Tags/index.tsx index 8041423199..8ea0b5db3e 100644 --- a/src/views/Tags/index.tsx +++ b/src/views/Tags/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Footer, @@ -11,7 +10,8 @@ import { Tag, Translate } from '~/components' -import { Query } from '~/components/GQL' +import EmptyTag from '~/components/Empty/EmptyTag' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -43,116 +43,113 @@ const ALL_TAGSS = gql` ${Tag.fragments.tag} ` -const Tags = () => ( -
-
- - - - } - /> - -
- - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: AllTags }) => { - if (loading) { - return - } +const Tags = () => { + const { data, loading, error, fetchMore } = useQuery(ALL_TAGSS) - const connectionPath = 'viewer.recommendation.tags' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.TAGS, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - const leftEdges = edges.filter((_: any, i: number) => i % 2 === 0) - const rightEdges = edges.filter((_: any, i: number) => i % 2 === 1) - - return ( - -
-
    - {leftEdges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
  • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.ALL_TAGS, - location: i * 2 - }) - } - > - -
  • - ) - )} -
-
    - {rightEdges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
  • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.ALL_TAGS, - location: i * 2 + 1 - }) - } - > - -
  • - ) - )} -
-
-
- ) + if (loading) { + return + } + + if (error) { + return + } + + const connectionPath = 'viewer.recommendation.tags' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.recommendation.tags) || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.TAGS, + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + const leftEdges = edges.filter((_: any, i: number) => i % 2 === 0) + const rightEdges = edges.filter((_: any, i: number) => i % 2 === 1) + + return ( + +
+
    + {leftEdges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.ALL_TAGS, + location: i * 2 + }) + } + > + +
  • + ))} +
+
    + {rightEdges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.ALL_TAGS, + location: i * 2 + 1 + }) + } + > + +
  • + ))} +
+
+
+ ) +} + +export default () => { + return ( +
+
+ -
-
+ /> + + + } + /> - +
+ +
+
- -
-) + -export default Tags + + + ) +} diff --git a/src/views/Topics/index.tsx b/src/views/Topics/index.tsx index 3af0f9c83b..6a58138e4c 100644 --- a/src/views/Topics/index.tsx +++ b/src/views/Topics/index.tsx @@ -1,6 +1,5 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, @@ -11,7 +10,8 @@ import { Placeholder, Translate } from '~/components' -import { Query } from '~/components/GQL' +import EmptyArticle from '~/components/Empty/EmptyArticle' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE, TEXT } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -47,98 +47,97 @@ const ALL_TOPICSS = gql` ${ArticleDigest.Feed.fragments.article} ` -const Topics = () => ( -
-
- +const Topics = () => { + const { data, loading, error, fetchMore } = useQuery(ALL_TOPICSS) - - } - /> + if (loading) { + return + } -
- - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: AllTopics }) => { - if (loading) { - return - } + if (error) { + return + } - const connectionPath = 'viewer.recommendation.topics' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.TOPICS, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) + const connectionPath = 'viewer.recommendation.topics' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.recommendation.topics) || {} + + if (!edges || edges.length <= 0 || !pageInfo) { + return + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.TOPICS, + 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(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.ALL_TOPICS, + location: i }) } + > + +
  • + ))} +
+
+ ) +} - return ( - -
    - {edges.map( - ( - { node, cursor }: { node: any; cursor: any }, - i: number - ) => ( -
  • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.ALL_TOPICS, - location: i - }) - } - > - -
  • - ) - )} -
-
- ) +export default () => { + return ( +
+
+ -
-
+ /> + + + } + /> - -
-) +
+ +
+ -export default Topics + + + ) +} diff --git a/src/views/User/Articles/UserArticles.tsx b/src/views/User/Articles/UserArticles.tsx index 38120e66f0..0e0947da1a 100644 --- a/src/views/User/Articles/UserArticles.tsx +++ b/src/views/User/Articles/UserArticles.tsx @@ -1,6 +1,5 @@ -import _get from 'lodash/get' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, @@ -10,7 +9,7 @@ import { Placeholder } from '~/components' import EmptyArticle from '~/components/Empty/EmptyArticle' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { UserArticles as UserArticlesTypes } from '~/components/GQL/queries/__generated__/UserArticles' import USER_ARTICLES from '~/components/GQL/queries/userArticles' import { Translate } from '~/components/Language' @@ -24,31 +23,31 @@ import ICON_DOT_DIVIDER from '~/static/icons/dot-divider.svg?sprite' import styles from './styles.css' const ArticleSummaryInfo = ({ data }: { data: UserArticlesTypes }) => { - const { articleCount: articles, totalWordCount: words } = _get( - data, - 'user.status', - { - articleCount: 0, - totalWordCount: 0 - } - ) + const { articleCount: articles, totalWordCount: words } = (data && + data.user && + data.user.status) || { + articleCount: 0, + totalWordCount: 0 + } + return ( - <> -
- - {articles} - - - - {words} - -
+
+ +  {articles}  + + + + + +  {words}  + + - +
) } @@ -56,101 +55,104 @@ const UserArticles = () => { const router = useRouter() const userName = getQuery({ router, key: 'userName' }) + const { data, loading, error, fetchMore } = useQuery( + USER_ARTICLES, + { variables: { userName } } + ) + if (!userName) { return } + if (loading) { + return + } + + if (error) { + return + } + + if (!data || !data.user) { + return + } + + const user = data.user + const connectionPath = 'user.articles' + const { edges, pageInfo } = data.user.articles + + if (!edges || edges.length <= 0 || !pageInfo) { + return null + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.USER_ARTICLE, + location: edges.length + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + + const CustomHead = () => ( + + ) + + if (!edges || edges.length <= 0) { + return ( + <> + + + + + ) + } + return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: UserArticlesTypes }) => { - if (loading) { - return - } - - const connectionPath = 'user.articles' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.USER_ARTICLE, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } - - const CustomHead = () => ( - - ) - - if (!edges || edges.length <= 0) { - return ( - <> - - - - - ) - } - - return ( - <> - - - + + + +
    + {edges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.USER_ARTICLE, + location: i + }) + } > -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.USER_ARTICLE, - location: i - }) - } - > - -
    • - ) - )} -
    - - - ) - }} - + +
  • + ))} +
+
+ ) } diff --git a/src/views/User/Articles/styles.css b/src/views/User/Articles/styles.css index 2a7fe1fa22..6cfe3edff4 100644 --- a/src/views/User/Articles/styles.css +++ b/src/views/User/Articles/styles.css @@ -2,14 +2,13 @@ @mixin border-bottom-grey; display: flex; align-items: center; - - color: var(--color-grey); padding-bottom: var(--spacing-default); margin-bottom: var(--spacing-default); - & > span { + color: var(--color-grey); + + & .num { color: var(--color-grey-darker); - font-weight: 600; - margin: 0 0.3rem; + font-weight: var(--font-weight-semibold); } } diff --git a/src/views/User/Bookmarks/MeBookmarks.tsx b/src/views/User/Bookmarks/MeBookmarks.tsx index ec52d72964..e0f2bc99ce 100644 --- a/src/views/User/Bookmarks/MeBookmarks.tsx +++ b/src/views/User/Bookmarks/MeBookmarks.tsx @@ -1,10 +1,9 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, InfiniteScroll, Placeholder } from '~/components' import EmptyBookmark from '~/components/Empty/EmptyBookmark' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { mergeConnections } from '~/common/utils' @@ -37,53 +36,51 @@ const ME_BOOKMARK_FEED = gql` ${ArticleDigest.Feed.fragments.article} ` -export default () => { - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: MeBookmarkFeed }) => { - if (loading) { - return - } +const MeBookmarks = () => { + const { data, loading, error, fetchMore } = useQuery( + ME_BOOKMARK_FEED + ) - const connectionPath = 'viewer.subscriptions' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => - fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) + if (loading) { + return + } - if (edges <= 0) { - return - } + if (error) { + return + } - return ( - -
    - {edges.map(({ node, cursor }: { node: any; cursor: any }) => ( -
  • - -
  • - ))} -
-
- ) - }} -
+ const connectionPath = 'viewer.subscriptions' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.subscriptions) || {} + + if (!edges || edges.length <= 0 || !pageInfo || edges.length <= 0) { + return + } + + const loadMore = () => + fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + + return ( + +
    + {edges.map(({ node, cursor }) => ( +
  • + +
  • + ))} +
+
) } + +export default MeBookmarks diff --git a/src/views/User/Comments/UserComments/index.tsx b/src/views/User/Comments/UserComments/index.tsx index 8edeb8648d..52c8bfa99e 100644 --- a/src/views/User/Comments/UserComments/index.tsx +++ b/src/views/User/Comments/UserComments/index.tsx @@ -1,13 +1,12 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import Link from 'next/link' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { Head, Icon, InfiniteScroll, Placeholder } from '~/components' import { CommentDigest } from '~/components/CommentDigest' import EmptyComment from '~/components/Empty/EmptyComment' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { filterComments, @@ -80,117 +79,133 @@ const UserCommentsWrap = () => { const router = useRouter() const userName = getQuery({ router, key: 'userName' }) - return ( - - {({ data, loading, error }: any) => { - if (loading) { - return - } + const { data, loading, error } = useQuery(USER_ID, { + variables: { userName } + }) + + if (loading) { + return + } - return ( - <> - - - - ) - }} - + if (error) { + return + } + + if (!data || !data.user) { + return null + } + + return ( + <> + + + ) } const UserComments = ({ user }: UserIdUser) => { + const { data, loading, error, fetchMore } = useQuery( + USER_COMMENT_FEED, + { + variables: { id: user && user.id } + } + ) + 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 ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: UserCommentFeed }) => { - if (loading) { - return - } + +
    + {edges.map(articleEdge => { + const commentEdges = articleEdge.node.comments.edges + + if (!commentEdges) { + return null + } - const connectionPath = 'node.commentedArticles' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => - fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) + const articlePath = toPath({ + page: 'articleDetail', + userName: articleEdge.node.author.userName || '', + slug: articleEdge.node.slug, + mediaHash: articleEdge.node.mediaHash || '' }) + const filteredComments = filterComments( + (commentEdges || []).map(({ node }) => node) + ) - if (!edges || edges.length <= 0) { - return - } + return ( +
  • + + +

    + {articleEdge.node.title} + +

    +
    + + +
      + {filteredComments && + filteredComments.map(comment => ( +
    • + +
    • + ))} +
    +
  • + ) + })} - return ( - -
      - {edges.map((articleEdge: { node: any; cursor: any }) => { - const commentEdges = _get(articleEdge, 'node.comments.edges') - const articlePath = toPath({ - page: 'articleDetail', - userName: articleEdge.node.author.userName, - slug: articleEdge.node.slug, - mediaHash: articleEdge.node.mediaHash - }) - const filteredComments = filterComments( - (commentEdges || []).map(({ node }: { node: any }) => node) - ) - - return ( -
    • - - -

      - {articleEdge.node.title} - -

      -
      - - -
        - {filteredComments && - filteredComments.map(comment => ( -
      • - -
      • - ))} -
      -
    • - ) - })} - -
    -
    - ) - }} - + +
+
) } diff --git a/src/views/User/Drafts/MeDrafts.tsx b/src/views/User/Drafts/MeDrafts.tsx index 3865070c29..4aa632b257 100644 --- a/src/views/User/Drafts/MeDrafts.tsx +++ b/src/views/User/Drafts/MeDrafts.tsx @@ -1,10 +1,9 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { DraftDigest, InfiniteScroll, Placeholder } from '~/components' import EmptyDraft from '~/components/Empty/EmptyDraft' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { mergeConnections } from '~/common/utils' @@ -33,53 +32,50 @@ const ME_DRAFTS_FEED = gql` ${DraftDigest.Feed.fragments.draft} ` -export default () => { - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: MeDraftFeed }) => { - if (loading) { - return - } +const MeDrafts = () => { + const { data, loading, error, fetchMore } = useQuery( + ME_DRAFTS_FEED + ) - const connectionPath = 'viewer.drafts' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => - fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) + if (loading) { + return + } - if (!edges || edges.length <= 0) { - return - } + if (error) { + return + } - return ( - -
    - {edges.map(({ node, cursor }: { node: any; cursor: any }) => ( -
  • - -
  • - ))} -
-
- ) - }} -
+ const connectionPath = 'viewer.drafts' + const { edges, pageInfo } = (data && data.viewer && data.viewer.drafts) || {} + + 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(({ node, cursor }) => ( +
  • + +
  • + ))} +
+
) } + +export default MeDrafts diff --git a/src/views/User/Followees/UserFollowees.tsx b/src/views/User/Followees/UserFollowees.tsx index d71aab7779..aa1ffd1ea2 100644 --- a/src/views/User/Followees/UserFollowees.tsx +++ b/src/views/User/Followees/UserFollowees.tsx @@ -1,11 +1,10 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Head, InfiniteScroll, Placeholder } from '~/components' -import EmptyFollowee from '~/components/Empty/EmptyFollowee' -import { Query } from '~/components/GQL' +import { Head, InfiniteScroll, Placeholder, Translate } from '~/components' +import EmptyWarning from '~/components/Empty/EmptyWarning' +import { QueryError } from '~/components/GQL' import { UserDigest } from '~/components/UserDigest' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' @@ -39,79 +38,81 @@ const USER_FOLLOWEES_FEED = gql` const UserFollowees = () => { const router = useRouter() const userName = getQuery({ router, key: 'userName' }) + const { data, loading, error, fetchMore } = useQuery( + USER_FOLLOWEES_FEED, + { + variables: { userName } + } + ) - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: UserFolloweeFeed }) => { - if (loading) { - return - } + if (loading || !data || !data.user) { + return + } - const connectionPath = 'user.followees' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.FOLLOWEE, - location: edges.length, - entrance: data.user.id - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (error) { + return + } + + const user = data.user + const connectionPath = 'user.followees' + const { edges, pageInfo } = user.followees - if (!edges || edges.length <= 0) { - return + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } + /> + ) + } - return ( - <> - - { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.FOLLOWEE, + location: edges.length, + entrance: user.id + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + + return ( + <> + + +
    + {edges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.FOLLOWEE, + location: i, + entrance: user.id + }) + } > -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.FOLLOWEE, - location: i, - entrance: data.user.id - }) - } - > - -
    • - ) - )} -
    - - - ) - }} - + +
  • + ))} +
+
+ ) } diff --git a/src/views/User/Followers/UserFollowers.tsx b/src/views/User/Followers/UserFollowers.tsx index 16cb3fdb7c..e4623f34cb 100644 --- a/src/views/User/Followers/UserFollowers.tsx +++ b/src/views/User/Followers/UserFollowers.tsx @@ -1,11 +1,10 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' import { useRouter } from 'next/router' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' -import { Head, InfiniteScroll, Placeholder } from '~/components' -import EmptyFollower from '~/components/Empty/EmptyFollower' -import { Query } from '~/components/GQL' +import { Head, InfiniteScroll, Placeholder, Translate } from '~/components' +import EmptyWarning from '~/components/Empty/EmptyWarning' +import { QueryError } from '~/components/GQL' import { UserDigest } from '~/components/UserDigest' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' @@ -39,79 +38,81 @@ const USER_FOLLOWERS_FEED = gql` const UserFollowers = () => { const router = useRouter() const userName = getQuery({ router, key: 'userName' }) + const { data, loading, error, fetchMore } = useQuery( + USER_FOLLOWERS_FEED, + { + variables: { userName } + } + ) - return ( - - {({ - data, - loading, - error, - fetchMore - }: QueryResult & { data: UserFollowerFeed }) => { - if (loading) { - return - } + if (loading || !data || !data.user) { + return + } - const connectionPath = 'user.followers' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.FOLLOWER, - location: edges.length, - entrance: data.user.id - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (error) { + return + } + + const user = data.user + const connectionPath = 'user.followers' + const { edges, pageInfo } = user.followers - if (!edges || edges.length <= 0) { - return + if (!edges || edges.length <= 0 || !pageInfo) { + return ( + } + /> + ) + } - return ( - <> - - { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.FOLLOWER, + location: edges.length, + entrance: user.id + }) + return fetchMore({ + variables: { + after: pageInfo.endCursor + }, + updateQuery: (previousResult, { fetchMoreResult }) => + mergeConnections({ + oldData: previousResult, + newData: fetchMoreResult, + path: connectionPath + }) + }) + } + + return ( + <> + + +
    + {edges.map(({ node, cursor }, i) => ( +
  • + analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.FOLLOWER, + location: i, + entrance: user.id + }) + } > -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.FOLLOWER, - location: i, - entrance: data.user.id - }) - } - > - -
    • - ) - )} -
    - - - ) - }} - + +
  • + ))} +
+
+ ) } diff --git a/src/views/User/History/MeHistory.tsx b/src/views/User/History/MeHistory.tsx index 3326df3b68..de611acc2e 100644 --- a/src/views/User/History/MeHistory.tsx +++ b/src/views/User/History/MeHistory.tsx @@ -1,10 +1,9 @@ import gql from 'graphql-tag' -import _get from 'lodash/get' -import { QueryResult } from 'react-apollo' +import { useQuery } from 'react-apollo' import { ArticleDigest, InfiniteScroll, Placeholder } from '~/components' import EmptyHistory from '~/components/Empty/EmptyHistory' -import { Query } from '~/components/GQL' +import { QueryError } from '~/components/GQL' import { ANALYTICS_EVENTS, FEED_TYPE } from '~/common/enums' import { analytics, mergeConnections } from '~/common/utils' @@ -42,71 +41,68 @@ const ME_HISTORY_FEED = gql` ${ArticleDigest.Feed.fragments.article} ` -export default () => { - return ( - - {({ - data, - loading, - fetchMore - }: QueryResult & { data: MeHistoryFeed }) => { - if (loading) { - return - } +const MeHistory = () => { + const { data, loading, error, fetchMore } = useQuery( + ME_HISTORY_FEED + ) - const connectionPath = 'viewer.activity.history' - const { edges, pageInfo } = _get(data, connectionPath, {}) - const loadMore = () => { - analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { - type: FEED_TYPE.READ_HISTORY, - location: edges.length - }) - return fetchMore({ - variables: { - after: pageInfo.endCursor - }, - updateQuery: (previousResult, { fetchMoreResult }) => - mergeConnections({ - oldData: previousResult, - newData: fetchMoreResult, - path: connectionPath - }) - }) - } + if (loading) { + return + } - if (!edges || edges.length <= 0) { - return - } + if (error) { + return + } + + const connectionPath = 'viewer.activity.history' + const { edges, pageInfo } = + (data && data.viewer && data.viewer.activity.history) || {} - return ( - + } + + const loadMore = () => { + analytics.trackEvent(ANALYTICS_EVENTS.LOAD_MORE, { + type: FEED_TYPE.READ_HISTORY, + 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(ANALYTICS_EVENTS.CLICK_FEED, { + type: FEED_TYPE.READ_HISTORY, + location: i + }) + } > -
      - {edges.map( - ({ node, cursor }: { node: any; cursor: any }, i: number) => ( -
    • - analytics.trackEvent(ANALYTICS_EVENTS.CLICK_FEED, { - type: FEED_TYPE.READ_HISTORY, - location: i - }) - } - > - -
    • - ) - )} -
    - - ) - }} - + +
  • + ))} +
+
) } + +export default MeHistory diff --git a/tslint.json b/tslint.json index 87dfda55a1..6d828f9033 100644 --- a/tslint.json +++ b/tslint.json @@ -1,15 +1,21 @@ { - "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"], + "extends": [ + "tslint:latest", + "tslint-react", + "tslint-config-prettier", + "tslint-react-hooks" + ], "rules": { "no-implicit-dependencies": [ true, [ "~", - "@testing-library/react", "tippy.js", "dotenv", "apollo-cache", - "svg-sprite-loader" + "svg-sprite-loader", + "@testing-library", + "@apollo" ] ], "no-bitwise": false, @@ -27,8 +33,7 @@ "import-sources-order": "case-insensitive", "named-imports-order": "case-insensitive", "grouped-imports": true, - "groups": [ - { + "groups": [{ "name": "local", "match": "^[.]", "order": 20 @@ -50,6 +55,7 @@ } ] } - ] + ], + "react-hooks-nesting": "error" } }