From d7eb1206fe3b4d78ea90e4d5d0fb3a3c35254ac8 Mon Sep 17 00:00:00 2001 From: nimbleghost <132819643+nimbleghost@users.noreply.github.com> Date: Tue, 23 May 2023 21:20:20 +0200 Subject: [PATCH 01/28] Add eslint with eslint-config-airbnb --- Makefile | 5 ++- web/.eslintignore | 1 + web/.eslintrc | 31 +++++++++++++ web/.prettierignore | 1 + web/package-lock.json | 101 ++++++++++++++++++++++++++++++++++-------- web/package.json | 18 +++++--- 6 files changed, 132 insertions(+), 25 deletions(-) create mode 100644 web/.eslintignore create mode 100644 web/.eslintrc diff --git a/Makefile b/Makefile index 6786acbe..cc571c16 100644 --- a/Makefile +++ b/Makefile @@ -145,6 +145,9 @@ web-format: web-format-check: cd web && npm run format:check +web-lint: + cd web && npm run lint + # Main server/client build cli: cli-deps @@ -233,7 +236,7 @@ cli-build-results: # Test/check targets -check: test web-format-check fmt-check vet lint staticcheck +check: test web-format-check fmt-check vet web-lint lint staticcheck test: .PHONY go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)') diff --git a/web/.eslintignore b/web/.eslintignore new file mode 100644 index 00000000..29c9584b --- /dev/null +++ b/web/.eslintignore @@ -0,0 +1 @@ +src/app/emojis.js \ No newline at end of file diff --git a/web/.eslintrc b/web/.eslintrc new file mode 100644 index 00000000..52e2c6b0 --- /dev/null +++ b/web/.eslintrc @@ -0,0 +1,31 @@ +{ + "extends": ["airbnb", "prettier"], + "env": { + "browser": true + }, + "globals": { + "config": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2023 + }, + "rules": { + "no-console": "off", + "class-methods-use-this": "off", + "func-style": ["error", "expression"], + "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"], + "no-await-in-loop": "error", + "import/no-cycle": "warn", + "react/prop-types": "off", + "react/destructuring-assignment": "off", + "react/jsx-no-useless-fragment": "off", + "react/jsx-props-no-spreading": "off", + "react/function-component-definition": [ + "error", + { + "namedComponents": "arrow-function", + "unnamedComponents": "arrow-function" + } + ] + } +} diff --git a/web/.prettierignore b/web/.prettierignore index 14652726..802cdb8d 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -1,3 +1,4 @@ build/ dist/ public/static/langs/ +src/app/emojis.js diff --git a/web/package-lock.json b/web/package-lock.json index d830d63c..1907a8d4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -26,6 +26,13 @@ "stacktrace-js": "^2.0.2" }, "devDependencies": { + "eslint": "^8.41.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", "react-scripts": "^5.0.0" } @@ -2531,9 +2538,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", + "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7590,15 +7597,15 @@ } }, "node_modules/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", + "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", + "@eslint/js": "8.41.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -7618,13 +7625,12 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -7646,6 +7652,67 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, + "engines": { + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -9210,6 +9277,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -12454,16 +12527,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/web/package.json b/web/package.json index 727e790f..be971bda 100644 --- a/web/package.json +++ b/web/package.json @@ -3,12 +3,13 @@ "version": "1.0.0", "private": true, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", + "start": "DISABLE_ESLINT_PLUGIN=true react-scripts start", + "build": "DISABLE_ESLINT_PLUGIN=true react-scripts build", + "test": "DISABLE_ESLINT_PLUGIN=true react-scripts test", + "eject": "DISABLE_ESLINT_PLUGIN=true react-scripts eject", "format": "prettier . --write", - "format:check": "prettier . --check" + "format:check": "prettier . --check", + "lint": "eslint --report-unused-disable-directives --ext .js,.jsx ./src/" }, "dependencies": { "@mui/icons-material": "^5.4.2", @@ -29,6 +30,13 @@ "stacktrace-js": "^2.0.2" }, "devDependencies": { + "eslint": "^8.41.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", "react-scripts": "^5.0.0" }, From f558b4dbe9bb5b9e0e87fada1215de2558353173 Mon Sep 17 00:00:00 2001 From: nimbleghost <132819643+nimbleghost@users.noreply.github.com> Date: Wed, 24 May 2023 09:02:33 +0200 Subject: [PATCH 02/28] Add `.jsx` filename extension (This is also required for Vite later) --- web/src/components/{Account.js => Account.jsx} | 0 web/src/components/{ActionBar.js => ActionBar.jsx} | 0 web/src/components/{App.js => App.jsx} | 0 web/src/components/{AttachmentIcon.js => AttachmentIcon.jsx} | 0 web/src/components/{AvatarBox.js => AvatarBox.jsx} | 0 web/src/components/{DialogFooter.js => DialogFooter.jsx} | 0 web/src/components/{EmojiPicker.js => EmojiPicker.jsx} | 0 web/src/components/{ErrorBoundary.js => ErrorBoundary.jsx} | 0 web/src/components/{Login.js => Login.jsx} | 0 web/src/components/{Messaging.js => Messaging.jsx} | 0 web/src/components/{Navigation.js => Navigation.jsx} | 0 web/src/components/{Notifications.js => Notifications.jsx} | 0 web/src/components/{PopupMenu.js => PopupMenu.jsx} | 0 web/src/components/{Pref.js => Pref.jsx} | 0 web/src/components/{Preferences.js => Preferences.jsx} | 0 web/src/components/{PublishDialog.js => PublishDialog.jsx} | 0 web/src/components/{ReserveDialogs.js => ReserveDialogs.jsx} | 0 web/src/components/{ReserveIcons.js => ReserveIcons.jsx} | 0 .../components/{ReserveTopicSelect.js => ReserveTopicSelect.jsx} | 0 web/src/components/{Signup.js => Signup.jsx} | 0 web/src/components/{SubscribeDialog.js => SubscribeDialog.jsx} | 0 .../components/{SubscriptionPopup.js => SubscriptionPopup.jsx} | 0 web/src/components/{UpgradeDialog.js => UpgradeDialog.jsx} | 0 web/src/components/{i18n.js => i18n.jsx} | 0 web/src/{index.js => index.jsx} | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename web/src/components/{Account.js => Account.jsx} (100%) rename web/src/components/{ActionBar.js => ActionBar.jsx} (100%) rename web/src/components/{App.js => App.jsx} (100%) rename web/src/components/{AttachmentIcon.js => AttachmentIcon.jsx} (100%) rename web/src/components/{AvatarBox.js => AvatarBox.jsx} (100%) rename web/src/components/{DialogFooter.js => DialogFooter.jsx} (100%) rename web/src/components/{EmojiPicker.js => EmojiPicker.jsx} (100%) rename web/src/components/{ErrorBoundary.js => ErrorBoundary.jsx} (100%) rename web/src/components/{Login.js => Login.jsx} (100%) rename web/src/components/{Messaging.js => Messaging.jsx} (100%) rename web/src/components/{Navigation.js => Navigation.jsx} (100%) rename web/src/components/{Notifications.js => Notifications.jsx} (100%) rename web/src/components/{PopupMenu.js => PopupMenu.jsx} (100%) rename web/src/components/{Pref.js => Pref.jsx} (100%) rename web/src/components/{Preferences.js => Preferences.jsx} (100%) rename web/src/components/{PublishDialog.js => PublishDialog.jsx} (100%) rename web/src/components/{ReserveDialogs.js => ReserveDialogs.jsx} (100%) rename web/src/components/{ReserveIcons.js => ReserveIcons.jsx} (100%) rename web/src/components/{ReserveTopicSelect.js => ReserveTopicSelect.jsx} (100%) rename web/src/components/{Signup.js => Signup.jsx} (100%) rename web/src/components/{SubscribeDialog.js => SubscribeDialog.jsx} (100%) rename web/src/components/{SubscriptionPopup.js => SubscriptionPopup.jsx} (100%) rename web/src/components/{UpgradeDialog.js => UpgradeDialog.jsx} (100%) rename web/src/components/{i18n.js => i18n.jsx} (100%) rename web/src/{index.js => index.jsx} (100%) diff --git a/web/src/components/Account.js b/web/src/components/Account.jsx similarity index 100% rename from web/src/components/Account.js rename to web/src/components/Account.jsx diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.jsx similarity index 100% rename from web/src/components/ActionBar.js rename to web/src/components/ActionBar.jsx diff --git a/web/src/components/App.js b/web/src/components/App.jsx similarity index 100% rename from web/src/components/App.js rename to web/src/components/App.jsx diff --git a/web/src/components/AttachmentIcon.js b/web/src/components/AttachmentIcon.jsx similarity index 100% rename from web/src/components/AttachmentIcon.js rename to web/src/components/AttachmentIcon.jsx diff --git a/web/src/components/AvatarBox.js b/web/src/components/AvatarBox.jsx similarity index 100% rename from web/src/components/AvatarBox.js rename to web/src/components/AvatarBox.jsx diff --git a/web/src/components/DialogFooter.js b/web/src/components/DialogFooter.jsx similarity index 100% rename from web/src/components/DialogFooter.js rename to web/src/components/DialogFooter.jsx diff --git a/web/src/components/EmojiPicker.js b/web/src/components/EmojiPicker.jsx similarity index 100% rename from web/src/components/EmojiPicker.js rename to web/src/components/EmojiPicker.jsx diff --git a/web/src/components/ErrorBoundary.js b/web/src/components/ErrorBoundary.jsx similarity index 100% rename from web/src/components/ErrorBoundary.js rename to web/src/components/ErrorBoundary.jsx diff --git a/web/src/components/Login.js b/web/src/components/Login.jsx similarity index 100% rename from web/src/components/Login.js rename to web/src/components/Login.jsx diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.jsx similarity index 100% rename from web/src/components/Messaging.js rename to web/src/components/Messaging.jsx diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.jsx similarity index 100% rename from web/src/components/Navigation.js rename to web/src/components/Navigation.jsx diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.jsx similarity index 100% rename from web/src/components/Notifications.js rename to web/src/components/Notifications.jsx diff --git a/web/src/components/PopupMenu.js b/web/src/components/PopupMenu.jsx similarity index 100% rename from web/src/components/PopupMenu.js rename to web/src/components/PopupMenu.jsx diff --git a/web/src/components/Pref.js b/web/src/components/Pref.jsx similarity index 100% rename from web/src/components/Pref.js rename to web/src/components/Pref.jsx diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.jsx similarity index 100% rename from web/src/components/Preferences.js rename to web/src/components/Preferences.jsx diff --git a/web/src/components/PublishDialog.js b/web/src/components/PublishDialog.jsx similarity index 100% rename from web/src/components/PublishDialog.js rename to web/src/components/PublishDialog.jsx diff --git a/web/src/components/ReserveDialogs.js b/web/src/components/ReserveDialogs.jsx similarity index 100% rename from web/src/components/ReserveDialogs.js rename to web/src/components/ReserveDialogs.jsx diff --git a/web/src/components/ReserveIcons.js b/web/src/components/ReserveIcons.jsx similarity index 100% rename from web/src/components/ReserveIcons.js rename to web/src/components/ReserveIcons.jsx diff --git a/web/src/components/ReserveTopicSelect.js b/web/src/components/ReserveTopicSelect.jsx similarity index 100% rename from web/src/components/ReserveTopicSelect.js rename to web/src/components/ReserveTopicSelect.jsx diff --git a/web/src/components/Signup.js b/web/src/components/Signup.jsx similarity index 100% rename from web/src/components/Signup.js rename to web/src/components/Signup.jsx diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.jsx similarity index 100% rename from web/src/components/SubscribeDialog.js rename to web/src/components/SubscribeDialog.jsx diff --git a/web/src/components/SubscriptionPopup.js b/web/src/components/SubscriptionPopup.jsx similarity index 100% rename from web/src/components/SubscriptionPopup.js rename to web/src/components/SubscriptionPopup.jsx diff --git a/web/src/components/UpgradeDialog.js b/web/src/components/UpgradeDialog.jsx similarity index 100% rename from web/src/components/UpgradeDialog.js rename to web/src/components/UpgradeDialog.jsx diff --git a/web/src/components/i18n.js b/web/src/components/i18n.jsx similarity index 100% rename from web/src/components/i18n.js rename to web/src/components/i18n.jsx diff --git a/web/src/index.js b/web/src/index.jsx similarity index 100% rename from web/src/index.js rename to web/src/index.jsx From 8319f1cf26113167fb29fe12edaff5db74caf35f Mon Sep 17 00:00:00 2001 From: nimbleghost <132819643+nimbleghost@users.noreply.github.com> Date: Wed, 24 May 2023 09:03:28 +0200 Subject: [PATCH 03/28] Run eslint autofixes --- web/src/app/AccountApi.js | 40 ++++----- web/src/app/Api.js | 11 +-- web/src/app/Connection.js | 3 +- web/src/app/ConnectionManager.js | 13 ++- web/src/app/SubscriptionManager.js | 22 ++--- web/src/app/config.js | 2 +- web/src/app/errors.js | 4 + web/src/app/utils.js | 88 ++++++++----------- web/src/components/Account.jsx | 53 ++++++------ web/src/components/ActionBar.jsx | 16 ++-- web/src/components/App.jsx | 55 ++++++------ web/src/components/AttachmentIcon.jsx | 7 +- web/src/components/AvatarBox.jsx | 34 ++++---- web/src/components/DialogFooter.jsx | 44 +++++----- web/src/components/EmojiPicker.jsx | 8 +- web/src/components/ErrorBoundary.jsx | 9 +- web/src/components/Login.jsx | 8 +- web/src/components/Messaging.jsx | 12 +-- web/src/components/Navigation.jsx | 22 +++-- web/src/components/Notifications.jsx | 102 ++++++++++++----------- web/src/components/PopupMenu.jsx | 4 +- web/src/components/Pref.jsx | 8 +- web/src/components/Preferences.jsx | 46 +++++----- web/src/components/PublishDialog.jsx | 52 +++++------- web/src/components/ReserveDialogs.jsx | 16 ++-- web/src/components/ReserveIcons.jsx | 16 +--- web/src/components/Signup.jsx | 8 +- web/src/components/SubscribeDialog.jsx | 22 +++-- web/src/components/SubscriptionPopup.jsx | 40 ++++----- web/src/components/UpgradeDialog.jsx | 60 +++++++------ web/src/components/hooks.js | 2 +- web/src/components/styles.js | 2 +- 32 files changed, 394 insertions(+), 435 deletions(-) diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 9af220a0..d3d5d4b6 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -1,3 +1,4 @@ +import i18n from "i18next"; import { accountBillingPortalUrl, accountBillingSubscriptionUrl, @@ -17,7 +18,6 @@ import { } from "./utils"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; -import i18n from "i18next"; import prefs from "./Prefs"; import routes from "../components/routes"; import { fetchOrThrow, UnauthorizedError } from "./errors"; @@ -66,13 +66,13 @@ class AccountApi { async create(username, password) { const url = accountUrl(config.base_url); const body = JSON.stringify({ - username: username, - password: password, + username, + password, }); console.log(`[AccountApi] Creating user account ${url}`); await fetchOrThrow(url, { method: "POST", - body: body, + body, }); } @@ -97,7 +97,7 @@ class AccountApi { method: "DELETE", headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - password: password, + password, }), }); } @@ -118,7 +118,7 @@ class AccountApi { async createToken(label, expires) { const url = accountTokenUrl(config.base_url); const body = { - label: label, + label, expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, }; console.log(`[AccountApi] Creating user access token ${url}`); @@ -132,8 +132,8 @@ class AccountApi { async updateToken(token, label, expires) { const url = accountTokenUrl(config.base_url); const body = { - token: token, - label: label, + token, + label, }; if (expires > 0) { body.expires = Math.floor(Date.now() / 1000) + expires; @@ -171,7 +171,7 @@ class AccountApi { await fetchOrThrow(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), - body: body, + body, }); } @@ -179,13 +179,13 @@ class AccountApi { const url = accountSubscriptionUrl(config.base_url); const body = JSON.stringify({ base_url: baseUrl, - topic: topic, + topic, }); console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); const response = await fetchOrThrow(url, { method: "POST", headers: withBearerAuth({}, session.token()), - body: body, + body, }); const subscription = await response.json(); // May throw SyntaxError console.log(`[AccountApi] Subscription`, subscription); @@ -196,14 +196,14 @@ class AccountApi { const url = accountSubscriptionUrl(config.base_url); const body = JSON.stringify({ base_url: baseUrl, - topic: topic, + topic, ...payload, }); console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); const response = await fetchOrThrow(url, { method: "PATCH", headers: withBearerAuth({}, session.token()), - body: body, + body, }); const subscription = await response.json(); // May throw SyntaxError console.log(`[AccountApi] Subscription`, subscription); @@ -230,8 +230,8 @@ class AccountApi { method: "POST", headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - topic: topic, - everyone: everyone, + topic, + everyone, }), }); } @@ -272,11 +272,11 @@ class AccountApi { async upsertBillingSubscription(method, tier, interval) { const url = accountBillingSubscriptionUrl(config.base_url); const response = await fetchOrThrow(url, { - method: method, + method, headers: withBearerAuth({}, session.token()), body: JSON.stringify({ - tier: tier, - interval: interval, + tier, + interval, }), }); return await response.json(); // May throw SyntaxError @@ -309,7 +309,7 @@ class AccountApi { headers: withBearerAuth({}, session.token()), body: JSON.stringify({ number: phoneNumber, - channel: channel, + channel, }), }); } @@ -322,7 +322,7 @@ class AccountApi { headers: withBearerAuth({}, session.token()), body: JSON.stringify({ number: phoneNumber, - code: code, + code, }), }); } diff --git a/web/src/app/Api.js b/web/src/app/Api.js index b956e0bd..ba1cbe61 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -18,7 +18,7 @@ class Api { const messages = []; const headers = maybeWithAuth({}, user); console.log(`[Api] Polling ${url}`); - for await (let line of fetchLinesIterator(url, headers)) { + for await (const line of fetchLinesIterator(url, headers)) { const message = JSON.parse(line); if (message.id) { console.log(`[Api, ${shortUrl}] Received message ${line}`); @@ -33,8 +33,8 @@ class Api { console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); const headers = {}; const body = { - topic: topic, - message: message, + topic, + message, ...options, }; await fetchOrThrow(baseUrl, { @@ -60,7 +60,7 @@ class Api { publishXHR(url, body, headers, onProgress) { console.log(`[Api] Publishing message to ${url}`); const xhr = new XMLHttpRequest(); - const send = new Promise(function (resolve, reject) { + const send = new Promise((resolve, reject) => { xhr.open("PUT", url); if (body.type) { xhr.overrideMimeType(body.type); @@ -106,7 +106,8 @@ class Api { }); if (response.status >= 200 && response.status <= 299) { return true; - } else if (response.status === 401 || response.status === 403) { + } + if (response.status === 401 || response.status === 403) { // See server/server.go return false; } diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index 7b25467c..dd3cf63d 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -77,7 +77,7 @@ class Connection { close() { console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); const socket = this.ws; - const retryTimeout = this.retryTimeout; + const { retryTimeout } = this; if (socket !== null) { socket.close(); } @@ -110,6 +110,7 @@ class Connection { export class ConnectionState { static Connected = "connected"; + static Connecting = "connecting"; } diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index f50ed531..f6316aa2 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -55,12 +55,12 @@ class ConnectionManager { // Create and add new connections subscriptionsWithUsersAndConnectionId.forEach((subscription) => { const subscriptionId = subscription.id; - const connectionId = subscription.connectionId; + const { connectionId } = subscription; const added = !this.connections.get(connectionId); if (added) { - const baseUrl = subscription.baseUrl; - const topic = subscription.topic; - const user = subscription.user; + const { baseUrl } = subscription; + const { topic } = subscription; + const { user } = subscription; const since = subscription.last; const connection = new Connection( connectionId, @@ -112,9 +112,8 @@ class ConnectionManager { } } -const makeConnectionId = async (subscription, user) => { - return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); -}; +const makeConnectionId = async (subscription, user) => + user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); const connectionManager = new ConnectionManager(); export default connectionManager; diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index a539362c..aeec3fc9 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -25,8 +25,8 @@ class SubscriptionManager { } const subscription = { id: topicUrl(baseUrl, topic), - baseUrl: baseUrl, - topic: topic, + baseUrl, + topic, mutedUntil: 0, last: null, internal: internal || false, @@ -39,14 +39,14 @@ class SubscriptionManager { console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); // Add remote subscriptions - let remoteIds = []; // = topicUrl(baseUrl, topic) + const remoteIds = []; // = topicUrl(baseUrl, topic) for (let i = 0; i < remoteSubscriptions.length; i++) { const remote = remoteSubscriptions[i]; const local = await this.add(remote.base_url, remote.topic, false); const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null; await this.update(local.id, { displayName: remote.display_name, // May be undefined - reservation: reservation, // May be null! + reservation, // May be null! }); remoteIds.push(local.id); } @@ -63,12 +63,12 @@ class SubscriptionManager { } async updateState(subscriptionId, state) { - db.subscriptions.update(subscriptionId, { state: state }); + db.subscriptions.update(subscriptionId, { state }); } async remove(subscriptionId) { await db.subscriptions.delete(subscriptionId); - await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + await db.notifications.where({ subscriptionId }).delete(); } async first() { @@ -140,7 +140,7 @@ class SubscriptionManager { } async deleteNotifications(subscriptionId) { - await db.notifications.where({ subscriptionId: subscriptionId }).delete(); + await db.notifications.where({ subscriptionId }).delete(); } async markNotificationRead(notificationId) { @@ -148,24 +148,24 @@ class SubscriptionManager { } async markNotificationsRead(subscriptionId) { - await db.notifications.where({ subscriptionId: subscriptionId, new: 1 }).modify({ new: 0 }); + await db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 }); } async setMutedUntil(subscriptionId, mutedUntil) { await db.subscriptions.update(subscriptionId, { - mutedUntil: mutedUntil, + mutedUntil, }); } async setDisplayName(subscriptionId, displayName) { await db.subscriptions.update(subscriptionId, { - displayName: displayName, + displayName, }); } async setReservation(subscriptionId, reservation) { await db.subscriptions.update(subscriptionId, { - reservation: reservation, + reservation, }); } diff --git a/web/src/app/config.js b/web/src/app/config.js index 15225f5b..24e86f3a 100644 --- a/web/src/app/config.js +++ b/web/src/app/config.js @@ -1,4 +1,4 @@ -const config = window.config; +const { config } = window; // The backend returns an empty base_url for the config struct, // so the frontend (hey, that's us!) can use the current location. diff --git a/web/src/app/errors.js b/web/src/app/errors.js index e31949d2..0d443757 100644 --- a/web/src/app/errors.js +++ b/web/src/app/errors.js @@ -48,6 +48,7 @@ export class UnauthorizedError extends Error { export class UserExistsError extends Error { static CODE = 40901; // errHTTPConflictUserExists + constructor() { super("Username already exists"); } @@ -55,6 +56,7 @@ export class UserExistsError extends Error { export class TopicReservedError extends Error { static CODE = 40902; // errHTTPConflictTopicReserved + constructor() { super("Topic already reserved"); } @@ -62,6 +64,7 @@ export class TopicReservedError extends Error { export class AccountCreateLimitReachedError extends Error { static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation + constructor() { super("Account creation limit reached"); } @@ -69,6 +72,7 @@ export class AccountCreateLimitReachedError extends Error { export class IncorrectPasswordError extends Error { static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation + constructor() { super("Password incorrect"); } diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 88e3684b..e8c98ec7 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -1,3 +1,4 @@ +import { Base64 } from "js-base64"; import { rawEmojis } from "./emojis"; import beep from "../sounds/beep.mp3"; import juntos from "../sounds/juntos.mp3"; @@ -7,7 +8,6 @@ import dadum from "../sounds/dadum.mp3"; import pop from "../sounds/pop.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3"; import config from "./config"; -import { Base64 } from "js-base64"; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; export const topicUrlWs = (baseUrl, topic) => @@ -33,9 +33,7 @@ export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; export const expandSecureUrl = (url) => `https://${url}`; -export const validUrl = (url) => { - return url.match(/^https?:\/\/.+/); -}; +export const validUrl = (url) => url.match(/^https?:\/\/.+/); export const validTopic = (topic) => { if (disallowedTopic(topic)) { @@ -44,14 +42,13 @@ export const validTopic = (topic) => { return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! }; -export const disallowedTopic = (topic) => { - return config.disallowed_topics.includes(topic); -}; +export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic); export const topicDisplayName = (subscription) => { if (subscription.displayName) { return subscription.displayName; - } else if (subscription.baseUrl === config.base_url) { + } + if (subscription.baseUrl === config.base_url) { return subscription.topic; } return topicShortUrl(subscription.baseUrl, subscription.topic); @@ -67,7 +64,7 @@ rawEmojis.forEach((emoji) => { const toEmojis = (tags) => { if (!tags) return []; - else return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); + return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]); }; export const formatTitleWithDefault = (m, fallback) => { @@ -81,33 +78,31 @@ export const formatTitle = (m) => { const emojiList = toEmojis(m.tags); if (emojiList.length > 0) { return `${emojiList.join(" ")} ${m.title}`; - } else { - return m.title; } + return m.title; }; export const formatMessage = (m) => { if (m.title) { return m.message; - } else { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.message}`; - } else { - return m.message; - } } + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.message}`; + } + return m.message; }; export const unmatchedTags = (tags) => { if (!tags) return []; - else return tags.filter((tag) => !(tag in emojis)); + return tags.filter((tag) => !(tag in emojis)); }; export const maybeWithAuth = (headers, user) => { if (user && user.password) { return withBasicAuth(headers, user.username, user.password); - } else if (user && user.token) { + } + if (user && user.token) { return withBearerAuth(headers, user.token); } return headers; @@ -121,30 +116,22 @@ export const maybeWithBearerAuth = (headers, token) => { }; export const withBasicAuth = (headers, username, password) => { - headers["Authorization"] = basicAuth(username, password); + headers.Authorization = basicAuth(username, password); return headers; }; -export const basicAuth = (username, password) => { - return `Basic ${encodeBase64(`${username}:${password}`)}`; -}; +export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`; export const withBearerAuth = (headers, token) => { - headers["Authorization"] = bearerAuth(token); + headers.Authorization = bearerAuth(token); return headers; }; -export const bearerAuth = (token) => { - return `Bearer ${token}`; -}; +export const bearerAuth = (token) => `Bearer ${token}`; -export const encodeBase64 = (s) => { - return Base64.encode(s); -}; +export const encodeBase64 = (s) => Base64.encode(s); -export const encodeBase64Url = (s) => { - return Base64.encodeURI(s); -}; +export const encodeBase64Url = (s) => Base64.encodeURI(s); export const maybeAppendActionErrors = (message, notification) => { const actionErrors = (notification.actions ?? []) @@ -153,13 +140,13 @@ export const maybeAppendActionErrors = (message, notification) => { .join("\n"); if (actionErrors.length === 0) { return message; - } else { - return `${message}\n\n${actionErrors}`; } + return `${message}\n\n${actionErrors}`; }; export const shuffle = (arr) => { - let j, x; + let j; + let x; for (let index = arr.length - 1; index > 0; index--) { j = Math.floor(Math.random() * (index + 1)); x = arr[index]; @@ -169,12 +156,11 @@ export const shuffle = (arr) => { return arr; }; -export const splitNoEmpty = (s, delimiter) => { - return s +export const splitNoEmpty = (s, delimiter) => + s .split(delimiter) .map((x) => x.trim()) .filter((x) => x !== ""); -}; /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ export const hashCode = async (s) => { @@ -182,21 +168,18 @@ export const hashCode = async (s) => { for (let i = 0; i < s.length; i++) { const char = s.charCodeAt(i); hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32bit integer + hash &= hash; // Convert to 32bit integer } return hash; }; -export const formatShortDateTime = (timestamp) => { - return new Intl.DateTimeFormat("default", { +export const formatShortDateTime = (timestamp) => + new Intl.DateTimeFormat("default", { dateStyle: "short", timeStyle: "short", }).format(new Date(timestamp * 1000)); -}; -export const formatShortDate = (timestamp) => { - return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); -}; +export const formatShortDate = (timestamp) => new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000)); export const formatBytes = (bytes, decimals = 2) => { if (bytes === 0) return "0 bytes"; @@ -204,13 +187,14 @@ export const formatBytes = (bytes, decimals = 2) => { const dm = decimals < 0 ? 0 : decimals; const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; }; export const formatNumber = (n) => { if (n === 0) { return n; - } else if (n % 1000 === 0) { + } + if (n % 1000 === 0) { return `${n / 1000}k`; } return n.toLocaleString(); @@ -267,7 +251,7 @@ export const playSound = async (id) => { export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder("utf-8"); const response = await fetch(fileURL, { - headers: headers, + headers, }); const reader = response.body.getReader(); let { value: chunk, done: readerDone } = await reader.read(); @@ -277,12 +261,12 @@ export async function* fetchLinesIterator(fileURL, headers) { let startIndex = 0; for (;;) { - let result = re.exec(chunk); + const result = re.exec(chunk); if (!result) { if (readerDone) { break; } - let remainder = chunk.substr(startIndex); + const remainder = chunk.substr(startIndex); ({ value: chunk, done: readerDone } = await reader.read()); chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); startIndex = re.lastIndex = 0; diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 5cb68c13..d6f74843 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -29,34 +29,34 @@ import Container from "@mui/material/Container"; import Card from "@mui/material/Card"; import Button from "@mui/material/Button"; import { Trans, useTranslation } from "react-i18next"; -import session from "../app/Session"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; -import theme from "./theme"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import TextField from "@mui/material/TextField"; -import routes from "./routes"; import IconButton from "@mui/material/IconButton"; -import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; -import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { Pref, PrefGroup } from "./Pref"; -import db from "../app/db"; import i18n from "i18next"; import humanizeDuration from "humanize-duration"; -import UpgradeDialog from "./UpgradeDialog"; import CelebrationIcon from "@mui/icons-material/Celebration"; -import { AccountContext } from "./App"; -import DialogFooter from "./DialogFooter"; -import { Paragraph } from "./styles"; import CloseIcon from "@mui/icons-material/Close"; import { ContentCopy, Public } from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; import DialogContentText from "@mui/material/DialogContentText"; +import AddIcon from "@mui/icons-material/Add"; +import routes from "./routes"; +import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; +import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; +import { Pref, PrefGroup } from "./Pref"; +import db from "../app/db"; +import UpgradeDialog from "./UpgradeDialog"; +import { AccountContext } from "./App"; +import DialogFooter from "./DialogFooter"; +import { Paragraph } from "./styles"; import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; import { ProChip } from "./SubscriptionPopup"; -import AddIcon from "@mui/icons-material/Add"; +import theme from "./theme"; +import session from "../app/Session"; const Account = () => { if (!session.exists()) { @@ -561,9 +561,7 @@ const Stats = () => { return <>; } - const normalize = (value, max) => { - return Math.min((value / max) * 100, 100); - }; + const normalize = (value, max) => Math.min((value / max) * 100, 100); return ( @@ -746,18 +744,16 @@ const Stats = () => { ); }; -const InfoIcon = () => { - return ( - - ); -}; +const InfoIcon = () => ( + +); const Tokens = () => { const { t } = useTranslation(); @@ -814,7 +810,8 @@ const TokensTable = (props) => { const tokens = (props.tokens || []).sort((a, b) => { if (a.token === session.token()) { return -1; - } else if (b.token === session.token()) { + } + if (b.token === session.token()) { return 1; } return a.token.localeCompare(b.token); diff --git a/web/src/components/ActionBar.jsx b/web/src/components/ActionBar.jsx index 24aef720..c9853df8 100644 --- a/web/src/components/ActionBar.jsx +++ b/web/src/components/ActionBar.jsx @@ -1,5 +1,4 @@ import AppBar from "@mui/material/AppBar"; -import Navigation from "./Navigation"; import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; @@ -7,23 +6,24 @@ import Typography from "@mui/material/Typography"; import * as React from "react"; import { useState } from "react"; import Box from "@mui/material/Box"; -import { topicDisplayName } from "../app/utils"; -import db from "../app/db"; import { useLocation, useNavigate } from "react-router-dom"; import MenuItem from "@mui/material/MenuItem"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import NotificationsIcon from "@mui/icons-material/Notifications"; import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; -import routes from "./routes"; -import subscriptionManager from "../app/SubscriptionManager"; -import logo from "../img/ntfy.svg"; import { useTranslation } from "react-i18next"; -import session from "../app/Session"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import { Logout, Person, Settings } from "@mui/icons-material"; import ListItemIcon from "@mui/material/ListItemIcon"; +import session from "../app/Session"; +import logo from "../img/ntfy.svg"; +import subscriptionManager from "../app/SubscriptionManager"; +import routes from "./routes"; +import db from "../app/db"; +import { topicDisplayName } from "../app/utils"; +import Navigation from "./Navigation"; import accountApi from "../app/AccountApi"; import PopupMenu from "./PopupMenu"; import { SubscriptionPopup } from "./SubscriptionPopup"; @@ -86,7 +86,7 @@ const ActionBar = (props) => { const SettingsIcons = (props) => { const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); - const subscription = props.subscription; + const { subscription } = props; const handleToggleMute = async () => { const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx index 50f2ad65..661f6eb7 100644 --- a/web/src/components/App.jsx +++ b/web/src/components/App.jsx @@ -4,16 +4,17 @@ import Box from "@mui/material/Box"; import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; import Toolbar from "@mui/material/Toolbar"; +import { useLiveQuery } from "dexie-react-hooks"; +import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; +import { Backdrop, CircularProgress } from "@mui/material"; import { AllSubscriptions, SingleSubscription } from "./Notifications"; import theme from "./theme"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; import Preferences from "./Preferences"; -import { useLiveQuery } from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; -import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; import { expandUrl } from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; @@ -21,7 +22,6 @@ import { useAccountListener, useBackgroundProcesses, useConnectionListeners } fr import PublishDialog from "./PublishDialog"; import Messaging from "./Messaging"; import "./i18n"; // Translations! -import { Backdrop, CircularProgress } from "@mui/material"; import Login from "./Login"; import Signup from "./Signup"; import Account from "./Account"; @@ -66,12 +66,11 @@ const Layout = () => { const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; - const [selected] = (subscriptionsWithoutInternal || []).filter((s) => { - return ( + const [selected] = (subscriptionsWithoutInternal || []).filter( + (s) => (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || (config.base_url === s.baseUrl && params.topic === s.topic) - ); - }); + ); useConnectionListeners(account, subscriptions, users); useAccountListener(setAccount); @@ -95,7 +94,7 @@ const Layout = () => { @@ -104,30 +103,28 @@ const Layout = () => { ); }; -const Main = (props) => { - return ( - (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), - }} - > - {props.children} - - ); -}; +const Main = (props) => ( + (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), + }} + > + {props.children} + +); const Loader = () => ( (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), diff --git a/web/src/components/AttachmentIcon.jsx b/web/src/components/AttachmentIcon.jsx index 9939b3b3..4d4e428a 100644 --- a/web/src/components/AttachmentIcon.jsx +++ b/web/src/components/AttachmentIcon.jsx @@ -1,16 +1,17 @@ import * as React from "react"; import Box from "@mui/material/Box"; +import { useTranslation } from "react-i18next"; import fileDocument from "../img/file-document.svg"; import fileImage from "../img/file-image.svg"; import fileVideo from "../img/file-video.svg"; import fileAudio from "../img/file-audio.svg"; import fileApp from "../img/file-app.svg"; -import { useTranslation } from "react-i18next"; const AttachmentIcon = (props) => { const { t } = useTranslation(); - const type = props.type; - let imageFile, imageLabel; + const { type } = props; + let imageFile; + let imageLabel; if (!type) { imageFile = fileDocument; imageLabel = t("notifications_attachment_file_image"); diff --git a/web/src/components/AvatarBox.jsx b/web/src/components/AvatarBox.jsx index 506ae630..470fcae8 100644 --- a/web/src/components/AvatarBox.jsx +++ b/web/src/components/AvatarBox.jsx @@ -3,23 +3,21 @@ import { Avatar } from "@mui/material"; import Box from "@mui/material/Box"; import logo from "../img/ntfy-filled.svg"; -const AvatarBox = (props) => { - return ( - - - {props.children} - - ); -}; +const AvatarBox = (props) => ( + + + {props.children} + +); export default AvatarBox; diff --git a/web/src/components/DialogFooter.jsx b/web/src/components/DialogFooter.jsx index 5a2bd7aa..2ddd7fb9 100644 --- a/web/src/components/DialogFooter.jsx +++ b/web/src/components/DialogFooter.jsx @@ -3,31 +3,29 @@ import Box from "@mui/material/Box"; import DialogContentText from "@mui/material/DialogContentText"; import DialogActions from "@mui/material/DialogActions"; -const DialogFooter = (props) => { - return ( - ( + + - - {props.status} - - {props.children} - - ); -}; + {props.status} + + {props.children} + +); export default DialogFooter; diff --git a/web/src/components/EmojiPicker.jsx b/web/src/components/EmojiPicker.jsx index 04cc5c72..6aa8e3c5 100644 --- a/web/src/components/EmojiPicker.jsx +++ b/web/src/components/EmojiPicker.jsx @@ -1,15 +1,15 @@ import * as React from "react"; import { useRef, useState } from "react"; import Typography from "@mui/material/Typography"; -import { rawEmojis } from "../app/emojis"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; import { ClickAwayListener, Fade, InputAdornment, styled } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import { Close } from "@mui/icons-material"; import Popper from "@mui/material/Popper"; -import { splitNoEmpty } from "../app/utils"; import { useTranslation } from "react-i18next"; +import { splitNoEmpty } from "../app/utils"; +import { rawEmojis } from "../app/emojis"; // Create emoji list by category and create a search base (string with all search words) // @@ -28,7 +28,7 @@ rawEmojis.forEach((emoji) => { const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; if (supportedEmoji) { const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; - const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; + const emojiWithSearchBase = { ...emoji, searchBase }; emojisByCategory[emoji.category].push(emojiWithSearchBase); } } catch (e) { @@ -133,7 +133,7 @@ const Category = (props) => { }; const Emoji = (props) => { - const emoji = props.emoji; + const { emoji } = props; const matches = emojiMatches(emoji, props.search); const title = `${emoji.description} (${emoji.aliases[0]})`; return ( diff --git a/web/src/components/ErrorBoundary.jsx b/web/src/components/ErrorBoundary.jsx index 21ee6a92..a8e67626 100644 --- a/web/src/components/ErrorBoundary.jsx +++ b/web/src/components/ErrorBoundary.jsx @@ -46,9 +46,9 @@ class ErrorBoundaryImpl extends React.Component { // Fetch additional info and a better stack trace StackTrace.fromError(error).then((stack) => { console.error("[ErrorBoundary] Stacktrace fetched", stack); - const niceStack = - `${error.toString()}\n` + - stack.map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); + const niceStack = `${error.toString()}\n${stack + .map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`) + .join("\n")}`; this.setState({ niceStack }); }); } @@ -73,9 +73,8 @@ class ErrorBoundaryImpl extends React.Component { if (this.state.error) { if (this.state.unsupportedIndexedDB) { return this.renderUnsupportedIndexedDB(); - } else { - return this.renderError(); } + return this.renderError(); } return this.props.children; } diff --git a/web/src/components/Login.jsx b/web/src/components/Login.jsx index ce4f3b50..57cf16ed 100644 --- a/web/src/components/Login.jsx +++ b/web/src/components/Login.jsx @@ -5,15 +5,15 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import TextField from "@mui/material/TextField"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; -import routes from "./routes"; -import session from "../app/Session"; import { NavLink } from "react-router-dom"; -import AvatarBox from "./AvatarBox"; import { useTranslation } from "react-i18next"; -import accountApi from "../app/AccountApi"; import IconButton from "@mui/material/IconButton"; import { InputAdornment } from "@mui/material"; import { Visibility, VisibilityOff } from "@mui/icons-material"; +import accountApi from "../app/AccountApi"; +import AvatarBox from "./AvatarBox"; +import session from "../app/Session"; +import routes from "./routes"; import { UnauthorizedError } from "../app/errors"; const Login = () => { diff --git a/web/src/components/Messaging.jsx b/web/src/components/Messaging.jsx index b6ed952b..cf91bbb1 100644 --- a/web/src/components/Messaging.jsx +++ b/web/src/components/Messaging.jsx @@ -1,21 +1,21 @@ import * as React from "react"; import { useState } from "react"; -import Navigation from "./Navigation"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; import TextField from "@mui/material/TextField"; import SendIcon from "@mui/icons-material/Send"; -import api from "../app/Api"; -import PublishDialog from "./PublishDialog"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import { Portal, Snackbar } from "@mui/material"; import { useTranslation } from "react-i18next"; +import PublishDialog from "./PublishDialog"; +import api from "../app/Api"; +import Navigation from "./Navigation"; const Messaging = (props) => { const [message, setMessage] = useState(""); const [dialogKey, setDialogKey] = useState(0); - const dialogOpenMode = props.dialogOpenMode; + const { dialogOpenMode } = props; const subscription = props.selected; const handleOpenDialogClick = () => { @@ -39,7 +39,7 @@ const Messaging = (props) => { topic={subscription?.topic ?? ""} message={message} onClose={handleDialogClose} - onDragEnter={() => props.onDialogOpenModeChange((prev) => (prev ? prev : PublishDialog.OPEN_MODE_DRAG))} // Only update if not already open + onDragEnter={() => props.onDialogOpenModeChange((prev) => prev || PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} /> @@ -48,7 +48,7 @@ const Messaging = (props) => { const MessageBar = (props) => { const { t } = useTranslation(); - const subscription = props.subscription; + const { subscription } = props; const [snackOpen, setSnackOpen] = useState(false); const handleSendClick = async () => { try { diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx index 1eeb3e83..81353627 100644 --- a/web/src/components/Navigation.jsx +++ b/web/src/components/Navigation.jsx @@ -11,28 +11,28 @@ import Divider from "@mui/material/Divider"; import List from "@mui/material/List"; import SettingsIcon from "@mui/icons-material/Settings"; import AddIcon from "@mui/icons-material/Add"; -import SubscribeDialog from "./SubscribeDialog"; import { Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip } from "@mui/material"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; +import { useLocation, useNavigate } from "react-router-dom"; +import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; +import Box from "@mui/material/Box"; +import ArticleIcon from "@mui/icons-material/Article"; +import { Trans, useTranslation } from "react-i18next"; +import CelebrationIcon from "@mui/icons-material/Celebration"; +import IconButton from "@mui/material/IconButton"; +import SubscribeDialog from "./SubscribeDialog"; import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; import routes from "./routes"; import { ConnectionState } from "../app/Connection"; -import { useLocation, useNavigate } from "react-router-dom"; import subscriptionManager from "../app/SubscriptionManager"; -import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; -import Box from "@mui/material/Box"; import notifier from "../app/Notifier"; import config from "../app/config"; -import ArticleIcon from "@mui/icons-material/Article"; -import { Trans, useTranslation } from "react-i18next"; import session from "../app/Session"; import accountApi, { Permission, Role } from "../app/AccountApi"; -import CelebrationIcon from "@mui/icons-material/Celebration"; import UpgradeDialog from "./UpgradeDialog"; import { AccountContext } from "./App"; import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; -import IconButton from "@mui/material/IconButton"; import { SubscriptionPopup } from "./SubscriptionPopup"; const navWidth = 280; @@ -237,9 +237,7 @@ const UpgradeBanner = () => { const SubscriptionList = (props) => { const sortedSubscriptions = props.subscriptions .filter((s) => !s.internal) - .sort((a, b) => { - return topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1; - }); + .sort((a, b) => (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1)); return ( <> {sortedSubscriptions.map((subscription) => ( @@ -258,7 +256,7 @@ const SubscriptionItem = (props) => { const navigate = useNavigate(); const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const subscription = props.subscription; + const { subscription } = props; const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; const displayName = topicDisplayName(subscription); const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index 35fd080b..5b611fb9 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -4,6 +4,15 @@ import Card from "@mui/material/Card"; import Typography from "@mui/material/Typography"; import * as React from "react"; import { useEffect, useState } from "react"; +import IconButton from "@mui/material/IconButton"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { useLiveQuery } from "dexie-react-hooks"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { Trans, useTranslation } from "react-i18next"; +import { useOutletContext } from "react-router-dom"; import { formatBytes, formatMessage, @@ -15,23 +24,14 @@ import { topicShortUrl, unmatchedTags, } from "../app/utils"; -import IconButton from "@mui/material/IconButton"; -import CheckIcon from "@mui/icons-material/Check"; -import CloseIcon from "@mui/icons-material/Close"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; -import { useLiveQuery } from "dexie-react-hooks"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; import subscriptionManager from "../app/SubscriptionManager"; -import InfiniteScroll from "react-infinite-scroll-component"; import priority1 from "../img/priority-1.svg"; import priority2 from "../img/priority-2.svg"; import priority4 from "../img/priority-4.svg"; import priority5 from "../img/priority-5.svg"; import logoOutline from "../img/ntfy-outline.svg"; import AttachmentIcon from "./AttachmentIcon"; -import { Trans, useTranslation } from "react-i18next"; -import { useOutletContext } from "react-router-dom"; import { useAutoSubscribe } from "./hooks"; export const AllSubscriptions = () => { @@ -52,46 +52,50 @@ export const SingleSubscription = () => { }; const AllSubscriptionsList = (props) => { - const subscriptions = props.subscriptions; + const { subscriptions } = props; const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); if (notifications === null || notifications === undefined) { return ; - } else if (subscriptions.length === 0) { + } + if (subscriptions.length === 0) { return ; - } else if (notifications.length === 0) { + } + if (notifications.length === 0) { return ; } return ; }; const SingleSubscriptionList = (props) => { - const subscription = props.subscription; + const { subscription } = props; const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); if (notifications === null || notifications === undefined) { return ; - } else if (notifications.length === 0) { + } + if (notifications.length === 0) { return ; } - return ; + return ; }; const NotificationList = (props) => { const { t } = useTranslation(); const pageSize = 20; - const notifications = props.notifications; + const { notifications } = props; const [snackOpen, setSnackOpen] = useState(false); const [maxCount, setMaxCount] = useState(pageSize); const count = Math.min(notifications.length, maxCount); - useEffect(() => { - return () => { + useEffect( + () => () => { setMaxCount(pageSize); const main = document.getElementById("main"); if (main) { main.scrollTo(0, 0); } - }; - }, [props.id]); + }, + [props.id] + ); return ( { const NotificationItem = (props) => { const { t } = useTranslation(); - const notification = props.notification; - const attachment = notification.attachment; + const { notification } = props; + const { attachment } = notification; const date = formatShortDateTime(notification.time); const otherTags = unmatchedTags(notification.tags); const tags = otherTags.length > 0 ? otherTags.join(", ") : null; @@ -272,7 +276,7 @@ const priorityFiles = { const Attachment = (props) => { const { t } = useTranslation(); - const attachment = props.attachment; + const { attachment } = props; const expired = attachment.expires && attachment.expires < Date.now() / 1000; const expires = attachment.expires && attachment.expires > Date.now() / 1000; const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); @@ -402,20 +406,18 @@ const Image = (props) => { ); }; -const UserActions = (props) => { - return ( - <> - {props.notification.actions.map((action) => ( - - ))} - - ); -}; +const UserActions = (props) => ( + <> + {props.notification.actions.map((action) => ( + + ))} + +); const UserAction = (props) => { const { t } = useTranslation(); - const notification = props.notification; - const action = props.action; + const { notification } = props; + const { action } = props; if (action.action === "broadcast") { return ( @@ -426,7 +428,8 @@ const UserAction = (props) => { ); - } else if (action.action === "view") { + } + if (action.action === "view") { return ( ); - } else if (action.action === "http") { + } + if (action.action === "http") { const method = action.method ?? "POST"; const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); return (