From 24064a65c80fe9f9e2acd714c7aaa989e3132c7f Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:20:09 +0530 Subject: [PATCH 01/68] bump to react 18 and install react-router-dom --- package-lock.json | 538 ++++++++++-------- package.json | 15 +- src/app/hooks/useCrossSigningStatus.js | 2 +- .../molecules/room-aliases/RoomAliases.jsx | 4 +- .../RoomHistoryVisibility.jsx | 4 +- .../room-notification/RoomNotification.jsx | 4 +- src/app/molecules/room-search/RoomSearch.jsx | 4 +- .../room-visibility/RoomVisibility.jsx | 4 +- .../emoji-verification/EmojiVerification.jsx | 2 +- .../organisms/space-manage/SpaceManage.jsx | 4 +- 10 files changed, 341 insertions(+), 240 deletions(-) diff --git a/package-lock.json b/package-lock.json index 677848231b..f7462f852d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,17 +45,18 @@ "pdfjs-dist": "3.10.111", "prismjs": "1.29.0", "prop-types": "15.8.1", - "react": "17.0.2", + "react": "18.2.0", "react-aria": "3.29.1", "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", - "react-dnd": "15.1.2", - "react-dnd-html5-backend": "15.1.3", - "react-dom": "17.0.2", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", + "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", "react-modal": "3.16.1", "react-range": "1.8.14", + "react-router-dom": "6.20.0", "sanitize-html": "2.8.0", "slate": "0.94.1", "slate-history": "0.93.0", @@ -71,13 +72,13 @@ "@types/file-saver": "2.0.5", "@types/node": "18.11.18", "@types/prismjs": "1.26.0", - "@types/react": "18.0.26", - "@types/react-dom": "18.0.9", + "@types/react": "18.2.39", + "@types/react-dom": "18.2.17", "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", - "@vitejs/plugin-react": "3.0.0", + "@vitejs/plugin-react": "4.2.0", "buffer": "6.0.3", "eslint": "8.29.0", "eslint-config-airbnb": "19.0.4", @@ -110,44 +111,45 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", + "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.20.10", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", - "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", - "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helpers": "^7.20.7", - "@babel/parser": "^7.20.7", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.12", - "@babel/types": "^7.20.7", - "convert-source-map": "^1.7.0", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", + "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.3", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.3", + "@babel/types": "^7.23.3", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -158,12 +160,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", - "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", + "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", "dependencies": { - "@babel/types": "^7.20.7", + "@babel/types": "^7.23.4", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -171,9 +174,9 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -184,21 +187,18 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { @@ -215,139 +215,139 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", - "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", + "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.13", - "@babel/types": "^7.20.7" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.4", + "@babel/types": "^7.23.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -355,9 +355,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", - "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", + "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -380,12 +380,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", - "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -395,12 +395,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz", - "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -445,31 +445,31 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", - "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.13", - "@babel/types": "^7.20.7", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", + "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", + "dependencies": { + "@babel/code-frame": "^7.23.4", + "@babel/generator": "^7.23.4", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.4", + "@babel/types": "^7.23.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -478,12 +478,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", + "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1893,19 +1893,19 @@ } }, "node_modules/@react-dnd/asap": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", - "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" }, "node_modules/@react-dnd/invariant": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-3.0.1.tgz", - "integrity": "sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" }, "node_modules/@react-dnd/shallowequal": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-3.0.1.tgz", - "integrity": "sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, "node_modules/@react-stately/calendar": { "version": "3.4.1", @@ -2583,6 +2583,14 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/@remix-run/router": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", + "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-inject": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.3.tgz", @@ -2705,6 +2713,47 @@ "react-dom": ">=16.8" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.7", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", + "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", + "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", @@ -2762,9 +2811,9 @@ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { - "version": "18.0.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", - "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", + "version": "18.2.39", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", + "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2772,9 +2821,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", - "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "dependencies": { "@types/react": "*" @@ -3208,22 +3257,22 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.0.0.tgz", - "integrity": "sha512-1mvyPc0xYW5G8CHQvJIJXLoMjl5Ct3q2g5Y2s6Ccfgwm45y48LBvsla7az+GkkAtYikWQ4Lxqcsq5RHLcZgtNQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.0.tgz", + "integrity": "sha512-+MHTH/e6H12kRp5HUkzOGqPMksezRMmW+TNzlh/QXfI8rRf6l2Z2yH/v12no1UvTwhZgEDMuQ7g7rrfMseU6FQ==", "dev": true, "dependencies": { - "@babel/core": "^7.20.5", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", + "@babel/core": "^7.23.3", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.4", "react-refresh": "^0.14.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.0.0" + "vite": "^4.2.0 || ^5.0.0" } }, "node_modules/abbrev": { @@ -3550,9 +3599,9 @@ "integrity": "sha512-L7siI766UCH6+arP9yT5wpA5AFxnmGbKiGSsxEVACl1tE0pvDJeQvMmbY2UmJiuffrr0ZJ2+U6Om46wQBqh1Lw==" }, "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "funding": [ { "type": "opencollective", @@ -3561,13 +3610,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -3631,9 +3684,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001446", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001446.tgz", - "integrity": "sha512-fEoga4PrImGcwUUGEol/PoFCSBnSkA9drgdkxXkJLsUBOnJ8rs3zDv6ApqYXGQFOyMPsjh79naWhF4DAxbF8rw==", + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "funding": [ { "type": "opencollective", @@ -3642,6 +3695,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3806,9 +3863,9 @@ } }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/core-js-pure": { "version": "3.26.1", @@ -3992,13 +4049,13 @@ } }, "node_modules/dnd-core": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.1.2.tgz", - "integrity": "sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", "dependencies": { - "@react-dnd/asap": "4.0.1", - "@react-dnd/invariant": "3.0.1", - "redux": "^4.1.2" + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" } }, "node_modules/doctrine": { @@ -4065,9 +4122,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==" + "version": "1.4.596", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.596.tgz", + "integrity": "sha512-zW3zbZ40Icb2BCWjm47nxwcFGYlIgdXkAx85XDO7cyky9J4QQfq8t0W19/TLZqq3JPQXtlv8BPIGmfa9Jb4scg==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -6317,9 +6374,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/nopt": { "version": "5.0.0", @@ -6764,12 +6821,11 @@ ] }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" @@ -6858,13 +6914,13 @@ } }, "node_modules/react-dnd": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.1.2.tgz", - "integrity": "sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", "dependencies": { - "@react-dnd/invariant": "3.0.1", - "@react-dnd/shallowequal": "3.0.1", - "dnd-core": "15.1.2", + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", "fast-deep-equal": "^3.1.3", "hoist-non-react-statics": "^3.3.2" }, @@ -6887,24 +6943,23 @@ } }, "node_modules/react-dnd-html5-backend": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.3.tgz", - "integrity": "sha512-HH/8nOEmrrcRGHMqJR91FOwhnLlx5SRLXmsQwZT3IPcBjx88WT+0pWC5A4tDOYDdoooh9k+KMPvWfxooR5TcOA==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", "dependencies": { - "dnd-core": "15.1.2" + "dnd-core": "^16.0.1" } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.2.0" } }, "node_modules/react-error-boundary": { @@ -6986,6 +7041,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", + "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", + "dependencies": { + "@remix-run/router": "1.13.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", + "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", + "dependencies": { + "@remix-run/router": "1.13.0", + "react-router": "6.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7013,9 +7098,9 @@ } }, "node_modules/redux": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", - "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "dependencies": { "@babel/runtime": "^7.9.2" } @@ -7247,12 +7332,11 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/scroll-into-view-if-needed": { @@ -7272,9 +7356,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -7786,9 +7870,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "funding": [ { "type": "opencollective", @@ -7797,6 +7881,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -7804,7 +7892,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" diff --git a/package.json b/package.json index 69bcd8f0e1..075bb3ed4e 100644 --- a/package.json +++ b/package.json @@ -55,17 +55,18 @@ "pdfjs-dist": "3.10.111", "prismjs": "1.29.0", "prop-types": "15.8.1", - "react": "17.0.2", + "react": "18.2.0", "react-aria": "3.29.1", "react-autosize-textarea": "7.1.0", "react-blurhash": "0.2.0", - "react-dnd": "15.1.2", - "react-dnd-html5-backend": "15.1.3", - "react-dom": "17.0.2", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", + "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", "react-modal": "3.16.1", "react-range": "1.8.14", + "react-router-dom": "6.20.0", "sanitize-html": "2.8.0", "slate": "0.94.1", "slate-history": "0.93.0", @@ -81,13 +82,13 @@ "@types/file-saver": "2.0.5", "@types/node": "18.11.18", "@types/prismjs": "1.26.0", - "@types/react": "18.0.26", - "@types/react-dom": "18.0.9", + "@types/react": "18.2.39", + "@types/react-dom": "18.2.17", "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", - "@vitejs/plugin-react": "3.0.0", + "@vitejs/plugin-react": "4.2.0", "buffer": "6.0.3", "eslint": "8.29.0", "eslint-config-airbnb": "19.0.4", diff --git a/src/app/hooks/useCrossSigningStatus.js b/src/app/hooks/useCrossSigningStatus.js index 61b69d1dc9..845c54629e 100644 --- a/src/app/hooks/useCrossSigningStatus.js +++ b/src/app/hooks/useCrossSigningStatus.js @@ -9,7 +9,7 @@ export function useCrossSigningStatus() { const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData()); useEffect(() => { - if (isCSEnabled) return null; + if (isCSEnabled) return undefined; const handleAccountData = (event) => { if (event.getType() === 'm.cross_signing.master') { setIsCSEnabled(true); diff --git a/src/app/molecules/room-aliases/RoomAliases.jsx b/src/app/molecules/room-aliases/RoomAliases.jsx index 201c523ae0..d573f7d6a4 100644 --- a/src/app/molecules/room-aliases/RoomAliases.jsx +++ b/src/app/molecules/room-aliases/RoomAliases.jsx @@ -110,7 +110,9 @@ function RoomAliases({ roomId }) { const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId); - useEffect(() => isMountedStore.setItem(true), []); + useEffect(() => { + isMountedStore.setItem(true) + }, []); useEffect(() => { let isUnmounted = false; diff --git a/src/app/molecules/room-history-visibility/RoomHistoryVisibility.jsx b/src/app/molecules/room-history-visibility/RoomHistoryVisibility.jsx index 6a72a99bb0..d9dd9540fa 100644 --- a/src/app/molecules/room-history-visibility/RoomHistoryVisibility.jsx +++ b/src/app/molecules/room-history-visibility/RoomHistoryVisibility.jsx @@ -49,7 +49,9 @@ function useVisibility(roomId) { const room = mx.getRoom(roomId); const [activeType, setActiveType] = useState(room.getHistoryVisibility()); - useEffect(() => setActiveType(room.getHistoryVisibility()), [roomId]); + useEffect(() => { + setActiveType(room.getHistoryVisibility()); + }, [roomId]); const setVisibility = useCallback((item) => { if (item.type === activeType.type) return; diff --git a/src/app/molecules/room-notification/RoomNotification.jsx b/src/app/molecules/room-notification/RoomNotification.jsx index 1c088e5f00..4adb1169e5 100644 --- a/src/app/molecules/room-notification/RoomNotification.jsx +++ b/src/app/molecules/room-notification/RoomNotification.jsx @@ -103,7 +103,9 @@ function setRoomNotifType(roomId, newType) { function useNotifications(roomId) { const { notifications } = initMatrix; const [activeType, setActiveType] = useState(notifications.getNotiType(roomId)); - useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]); + useEffect(() => { + setActiveType(notifications.getNotiType(roomId)); + }, [roomId]); const setNotification = useCallback((item) => { if (item.type === activeType.type) return; diff --git a/src/app/molecules/room-search/RoomSearch.jsx b/src/app/molecules/room-search/RoomSearch.jsx index 2612aed134..6009649f1a 100644 --- a/src/app/molecules/room-search/RoomSearch.jsx +++ b/src/app/molecules/room-search/RoomSearch.jsx @@ -29,7 +29,9 @@ function useRoomSearch(roomId) { const mountStore = useStore(roomId); const mx = initMatrix.matrixClient; - useEffect(() => mountStore.setItem(true), [roomId]); + useEffect(() => { + mountStore.setItem(true) + }, [roomId]); useEffect(() => { if (searchData?.results?.length > 0) { diff --git a/src/app/molecules/room-visibility/RoomVisibility.jsx b/src/app/molecules/room-visibility/RoomVisibility.jsx index 7a8528765d..a5e8e2d08e 100644 --- a/src/app/molecules/room-visibility/RoomVisibility.jsx +++ b/src/app/molecules/room-visibility/RoomVisibility.jsx @@ -50,7 +50,9 @@ function useVisibility(roomId) { const room = mx.getRoom(roomId); const [activeType, setActiveType] = useState(room.getJoinRule()); - useEffect(() => setActiveType(room.getJoinRule()), [roomId]); + useEffect(() => { + setActiveType(room.getJoinRule()); + }, [roomId]); const setNotification = useCallback((item) => { if (item.type === activeType.type) return; diff --git a/src/app/organisms/emoji-verification/EmojiVerification.jsx b/src/app/organisms/emoji-verification/EmojiVerification.jsx index 6fe81cddf5..3ae1f2948c 100644 --- a/src/app/organisms/emoji-verification/EmojiVerification.jsx +++ b/src/app/organisms/emoji-verification/EmojiVerification.jsx @@ -80,7 +80,7 @@ function EmojiVerificationContent({ data, requestClose }) { } }; - if (request === null) return null; + if (request === null) return undefined; const req = request; req.on('change', handleChange); return () => { diff --git a/src/app/organisms/space-manage/SpaceManage.jsx b/src/app/organisms/space-manage/SpaceManage.jsx index cf042da465..60f00ad31b 100644 --- a/src/app/organisms/space-manage/SpaceManage.jsx +++ b/src/app/organisms/space-manage/SpaceManage.jsx @@ -302,7 +302,9 @@ function SpaceManageContent({ roomId, requestClose }) { }; }, [roomId]); - useEffect(() => setSelected([]), [spacePath]); + useEffect(() => { + setSelected([]); + }, [spacePath]); const handleSelected = (selectedRoomId) => { const newSelected = [...selected]; From 15d512360442aeedfeaf842bde90b900a3e7b33c Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:52:04 +0530 Subject: [PATCH 02/68] Upgrade to react 18 root --- src/index.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index a8a7657039..0ec65e1699 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,6 +1,6 @@ /* eslint-disable import/first */ import React from 'react'; -import ReactDom from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { enableMapSet } from 'immer'; import '@fontsource/inter/variable.css'; import 'folds/dist/style.css'; @@ -18,4 +18,6 @@ document.body.classList.add(configClass, varsClass); settings.applyTheme(); -ReactDom.render(, document.getElementById('root')); +const rootContainer = document.getElementById('root'); +const root = createRoot(rootContainer); +root.render(); From 5bb47c2b26f3ebfbb68fef0ad07e6883afeed1bc Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:09:06 +0530 Subject: [PATCH 03/68] update vite --- package-lock.json | 419 +++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 305 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7462f852d..2df6fee212 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "prettier": "2.8.1", "sass": "1.56.2", "typescript": "4.9.4", - "vite": "4.3.9", + "vite": "5.0.8", "vite-plugin-static-copy": "0.13.0" }, "engines": { @@ -2664,6 +2664,175 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.8.0.tgz", + "integrity": "sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.8.0.tgz", + "integrity": "sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.8.0.tgz", + "integrity": "sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.8.0.tgz", + "integrity": "sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.8.0.tgz", + "integrity": "sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.8.0.tgz", + "integrity": "sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.8.0.tgz", + "integrity": "sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.8.0.tgz", + "integrity": "sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.8.0.tgz", + "integrity": "sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.8.0.tgz", + "integrity": "sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.8.0.tgz", + "integrity": "sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.8.0.tgz", + "integrity": "sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.8.0.tgz", + "integrity": "sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@swc/helpers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", @@ -5097,9 +5266,9 @@ "devOptional": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -6326,9 +6495,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -6687,9 +6856,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "funding": [ { "type": "opencollective", @@ -6705,7 +6874,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7210,18 +7379,31 @@ } }, "node_modules/rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.8.0.tgz", + "integrity": "sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==", "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.8.0", + "@rollup/rollup-android-arm64": "4.8.0", + "@rollup/rollup-darwin-arm64": "4.8.0", + "@rollup/rollup-darwin-x64": "4.8.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.8.0", + "@rollup/rollup-linux-arm64-gnu": "4.8.0", + "@rollup/rollup-linux-arm64-musl": "4.8.0", + "@rollup/rollup-linux-riscv64-gnu": "4.8.0", + "@rollup/rollup-linux-x64-gnu": "4.8.0", + "@rollup/rollup-linux-x64-musl": "4.8.0", + "@rollup/rollup-win32-arm64-msvc": "4.8.0", + "@rollup/rollup-win32-ia32-msvc": "4.8.0", + "@rollup/rollup-win32-x64-msvc": "4.8.0", "fsevents": "~2.3.2" } }, @@ -7922,27 +8104,31 @@ } }, "node_modules/vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.8.tgz", + "integrity": "sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.23", - "rollup": "^3.21.0" + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -7955,6 +8141,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -8023,9 +8212,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.9.tgz", + "integrity": "sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==", "cpu": [ "arm" ], @@ -8039,9 +8228,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz", + "integrity": "sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==", "cpu": [ "arm64" ], @@ -8055,9 +8244,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.9.tgz", + "integrity": "sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==", "cpu": [ "x64" ], @@ -8071,9 +8260,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz", + "integrity": "sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==", "cpu": [ "arm64" ], @@ -8087,9 +8276,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz", + "integrity": "sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==", "cpu": [ "x64" ], @@ -8103,9 +8292,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz", + "integrity": "sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==", "cpu": [ "arm64" ], @@ -8119,9 +8308,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz", + "integrity": "sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==", "cpu": [ "x64" ], @@ -8135,9 +8324,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz", + "integrity": "sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==", "cpu": [ "arm" ], @@ -8151,9 +8340,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz", + "integrity": "sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==", "cpu": [ "arm64" ], @@ -8167,9 +8356,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz", + "integrity": "sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==", "cpu": [ "ia32" ], @@ -8183,9 +8372,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz", + "integrity": "sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==", "cpu": [ "loong64" ], @@ -8199,9 +8388,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz", + "integrity": "sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==", "cpu": [ "mips64el" ], @@ -8215,9 +8404,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz", + "integrity": "sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==", "cpu": [ "ppc64" ], @@ -8231,9 +8420,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz", + "integrity": "sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==", "cpu": [ "riscv64" ], @@ -8247,9 +8436,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz", + "integrity": "sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==", "cpu": [ "s390x" ], @@ -8263,9 +8452,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz", + "integrity": "sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==", "cpu": [ "x64" ], @@ -8279,9 +8468,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz", + "integrity": "sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==", "cpu": [ "x64" ], @@ -8295,9 +8484,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz", + "integrity": "sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==", "cpu": [ "x64" ], @@ -8311,9 +8500,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz", + "integrity": "sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==", "cpu": [ "x64" ], @@ -8327,9 +8516,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz", + "integrity": "sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==", "cpu": [ "arm64" ], @@ -8343,9 +8532,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz", + "integrity": "sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==", "cpu": [ "ia32" ], @@ -8359,9 +8548,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz", + "integrity": "sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==", "cpu": [ "x64" ], @@ -8375,9 +8564,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz", + "integrity": "sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==", "dev": true, "hasInstallScript": true, "bin": { @@ -8387,28 +8576,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/android-arm": "0.19.9", + "@esbuild/android-arm64": "0.19.9", + "@esbuild/android-x64": "0.19.9", + "@esbuild/darwin-arm64": "0.19.9", + "@esbuild/darwin-x64": "0.19.9", + "@esbuild/freebsd-arm64": "0.19.9", + "@esbuild/freebsd-x64": "0.19.9", + "@esbuild/linux-arm": "0.19.9", + "@esbuild/linux-arm64": "0.19.9", + "@esbuild/linux-ia32": "0.19.9", + "@esbuild/linux-loong64": "0.19.9", + "@esbuild/linux-mips64el": "0.19.9", + "@esbuild/linux-ppc64": "0.19.9", + "@esbuild/linux-riscv64": "0.19.9", + "@esbuild/linux-s390x": "0.19.9", + "@esbuild/linux-x64": "0.19.9", + "@esbuild/netbsd-x64": "0.19.9", + "@esbuild/openbsd-x64": "0.19.9", + "@esbuild/sunos-x64": "0.19.9", + "@esbuild/win32-arm64": "0.19.9", + "@esbuild/win32-ia32": "0.19.9", + "@esbuild/win32-x64": "0.19.9" } }, "node_modules/warning": { diff --git a/package.json b/package.json index 075bb3ed4e..d494df84a5 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "prettier": "2.8.1", "sass": "1.56.2", "typescript": "4.9.4", - "vite": "4.3.9", + "vite": "5.0.8", "vite-plugin-static-copy": "0.13.0" } } From aa714a2824fb3743f083bff3abb78bb0696add8b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:09:29 +0530 Subject: [PATCH 04/68] add cs api's --- src/app/cs-api.ts | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/app/cs-api.ts diff --git a/src/app/cs-api.ts b/src/app/cs-api.ts new file mode 100644 index 0000000000..f0157206b2 --- /dev/null +++ b/src/app/cs-api.ts @@ -0,0 +1,104 @@ +import to from 'await-to-js'; + +export enum AutoDiscoveryAction { + PROMPT = 'PROMPT', + IGNORE = 'IGNORE', + FAIL_PROMPT = 'FAIL_PROMPT', + FAIL_ERROR = 'FAIL_ERROR', +} + +export type AutoDiscoveryError = { + host: string; + action: AutoDiscoveryAction; +}; + +export type AutoDiscoveryInfo = Record & { + 'm.homeserver': { + base_url: string; + }; + 'm.identity_server'?: { + base_url: string; + }; +}; + +export const autoDiscovery = async ( + request: typeof fetch, + server: string +): Promise<[AutoDiscoveryError, undefined] | [undefined, AutoDiscoveryInfo]> => { + const host = /^https?:\/\//.test(server) ? server : `https://${server}`; + const autoDiscoveryUrl = `${host}/.well-known/matrix/client`; + + const [err, response] = await to(request(autoDiscoveryUrl, { method: 'GET' })); + + if (err || response.status === 404) { + return [ + { + host, + action: AutoDiscoveryAction.IGNORE, + }, + undefined, + ]; + } + if (response.status !== 200) { + return [ + { + host, + action: AutoDiscoveryAction.FAIL_PROMPT, + }, + undefined, + ]; + } + + const [contentErr, content] = await to(response.json()); + + if (contentErr || typeof content !== 'object') { + return [ + { + host, + action: AutoDiscoveryAction.FAIL_PROMPT, + }, + undefined, + ]; + } + + const baseUrl = content['m.homeserver']?.base_url; + if (typeof baseUrl !== 'string') { + return [ + { + host, + action: AutoDiscoveryAction.FAIL_PROMPT, + }, + undefined, + ]; + } + + if (/^https?:\/\//.test(baseUrl) === false) { + return [ + { + host, + action: AutoDiscoveryAction.FAIL_ERROR, + }, + undefined, + ]; + } + + return [undefined, content]; +}; + +export type SpecVersions = { + versions: string[]; + unstable_features?: Record; +}; +export const specVersions = async ( + request: typeof fetch, + baseUrl: string +): Promise => { + const res = await request(`${baseUrl}/_matrix/client/versions`); + + const data = (await res.json()) as unknown; + + if (data && typeof data === 'object' && 'versions' in data && Array.isArray(data.versions)) { + return data as SpecVersions; + } + throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver'); +}; From 6300ef86581e57a1e0dfb52a33308af178bbb41c Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:09:57 +0530 Subject: [PATCH 05/68] convert state/auth to ts --- src/client/state/auth.js | 19 ------------------- src/client/state/auth.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 19 deletions(-) delete mode 100644 src/client/state/auth.js create mode 100644 src/client/state/auth.ts diff --git a/src/client/state/auth.js b/src/client/state/auth.js deleted file mode 100644 index fbc23f6f4f..0000000000 --- a/src/client/state/auth.js +++ /dev/null @@ -1,19 +0,0 @@ -import cons from './cons'; - -function getSecret(key) { - return localStorage.getItem(key); -} - -const isAuthenticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null; - -const secret = { - accessToken: getSecret(cons.secretKey.ACCESS_TOKEN), - deviceId: getSecret(cons.secretKey.DEVICE_ID), - userId: getSecret(cons.secretKey.USER_ID), - baseUrl: getSecret(cons.secretKey.BASE_URL), -}; - -export { - isAuthenticated, - secret, -}; diff --git a/src/client/state/auth.ts b/src/client/state/auth.ts new file mode 100644 index 0000000000..f9e1c29786 --- /dev/null +++ b/src/client/state/auth.ts @@ -0,0 +1,12 @@ +import cons from './cons'; + +const isAuthenticated = () => localStorage.getItem(cons.secretKey.ACCESS_TOKEN) !== null; + +const secret = { + accessToken: localStorage.getItem(cons.secretKey.ACCESS_TOKEN), + deviceId: localStorage.getItem(cons.secretKey.DEVICE_ID), + userId: localStorage.getItem(cons.secretKey.USER_ID), + baseUrl: localStorage.getItem(cons.secretKey.BASE_URL), +}; + +export { isAuthenticated, secret }; From ab4aabbffc0df37ded47a7e7dac4efeccf185e9e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:10:52 +0530 Subject: [PATCH 06/68] add client config context --- src/app/components/ClientConfigLoader.tsx | 26 +++++++++++++++++++++++ src/app/hooks/useClientConfig.ts | 19 +++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/app/components/ClientConfigLoader.tsx create mode 100644 src/app/hooks/useClientConfig.ts diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx new file mode 100644 index 0000000000..15a3c97107 --- /dev/null +++ b/src/app/components/ClientConfigLoader.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from 'react'; +import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; +import { ClientConfig } from '../hooks/useClientConfig'; + +const getClientConfig = async (): Promise => { + const config = await fetch('/config.json', { method: 'GET' }); + return config.json(); +}; + +type ClientConfigLoaderProps = { + fallback?: () => ReactNode; + children: (config: ClientConfig) => ReactNode; +}; +export function ClientConfigLoader({ fallback, children }: ClientConfigLoaderProps) { + const [state, load] = useAsyncCallback(getClientConfig); + + if (state.status === AsyncStatus.Idle) load(); + + if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { + return fallback?.(); + } + + const config: ClientConfig = state.status === AsyncStatus.Success ? state.data : {}; + + return children(config); +} diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts new file mode 100644 index 0000000000..854aecd100 --- /dev/null +++ b/src/app/hooks/useClientConfig.ts @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; + +export type ClientConfig = { + appVersion?: string; + basename?: string; + defaultHomeserver?: number; + homeserverList?: string[]; + allowCustomHomeservers?: boolean; +}; + +const ClientConfigContext = createContext(null); + +export const ClientConfigProvider = ClientConfigContext.Provider; + +export function useClientConfig(): ClientConfig { + const config = useContext(ClientConfigContext); + if (!config) throw new Error('Client config are not provided!'); + return config; +} From fed69167a65bac8c561f835a8531a4b56c6cc9de Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:11:28 +0530 Subject: [PATCH 07/68] add auto discovery context --- src/app/hooks/useAutoDiscoveryInfo.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/app/hooks/useAutoDiscoveryInfo.ts diff --git a/src/app/hooks/useAutoDiscoveryInfo.ts b/src/app/hooks/useAutoDiscoveryInfo.ts new file mode 100644 index 0000000000..b2f8bcb565 --- /dev/null +++ b/src/app/hooks/useAutoDiscoveryInfo.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; +import { AutoDiscoveryInfo } from '../cs-api'; + +const AutoDiscoverInfoContext = createContext(null); + +export const AutoDiscoveryInfoProvider = AutoDiscoverInfoContext.Provider; + +export const useAutoDiscoveryInfo = (): AutoDiscoveryInfo => { + const autoDiscoveryInfo = useContext(AutoDiscoverInfoContext); + if (!autoDiscoveryInfo) { + throw new Error('Auto Discovery Info not loaded'); + } + + return autoDiscoveryInfo; +}; From 04c8bfdfa95c8a49b0fdbf6890f848c907c7aa0a Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:11:44 +0530 Subject: [PATCH 08/68] add spec version context --- src/app/components/SpecVersionsLoader.tsx | 30 +++++++++++++++++++++++ src/app/hooks/useSpecVersions.ts | 12 +++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/app/components/SpecVersionsLoader.tsx create mode 100644 src/app/hooks/useSpecVersions.ts diff --git a/src/app/components/SpecVersionsLoader.tsx b/src/app/components/SpecVersionsLoader.tsx new file mode 100644 index 0000000000..449a1a33ba --- /dev/null +++ b/src/app/components/SpecVersionsLoader.tsx @@ -0,0 +1,30 @@ +import { ReactNode, useCallback } from 'react'; +import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; +import { SpecVersions, specVersions } from '../cs-api'; +import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo'; + +type SpecVersionsLoaderProps = { + fallback?: () => ReactNode; + error?: (err: unknown) => ReactNode; + children: (versions: SpecVersions) => ReactNode; +}; +export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) { + const autoDiscoveryInfo = useAutoDiscoveryInfo(); + const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url; + + const [state, load] = useAsyncCallback( + useCallback(() => specVersions(fetch, baseUrl), [baseUrl]) + ); + + if (state.status === AsyncStatus.Idle) load(); + + if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { + return fallback?.(); + } + + if (state.status === AsyncStatus.Error) { + return error?.(state.error); + } + + return children(state.data); +} diff --git a/src/app/hooks/useSpecVersions.ts b/src/app/hooks/useSpecVersions.ts new file mode 100644 index 0000000000..42403c61c6 --- /dev/null +++ b/src/app/hooks/useSpecVersions.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; +import { SpecVersions } from '../cs-api'; + +const SpecVersionsContext = createContext(null); + +export const SpecVersionsProvider = SpecVersionsContext.Provider; + +export function useSpecVersions(): SpecVersions { + const versions = useContext(SpecVersionsContext); + if (!versions) throw new Error('Server versions are not provided!'); + return versions; +} From 18757f6aeadc26505e231ea318a4673e37e3573b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:11:56 +0530 Subject: [PATCH 09/68] add auth flow context --- src/app/components/AuthFlowsLoader.tsx | 80 ++++++++++++++++++++++++++ src/app/hooks/useAuthFlows.ts | 36 ++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/app/components/AuthFlowsLoader.tsx create mode 100644 src/app/hooks/useAuthFlows.ts diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx new file mode 100644 index 0000000000..9dbd06c496 --- /dev/null +++ b/src/app/components/AuthFlowsLoader.tsx @@ -0,0 +1,80 @@ +import { ReactNode, useCallback, useMemo } from 'react'; +import { IAuthData, MatrixError, createClient } from 'matrix-js-sdk'; +import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; +import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo'; +import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common'; +import { AuthFlows, RegisterFlowStatus, RegisterFlowsResponse } from '../hooks/useAuthFlows'; + +type AuthFlowsLoaderProps = { + fallback?: () => ReactNode; + error?: (err: unknown) => ReactNode; + children: (versions: AuthFlows) => ReactNode; +}; +export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderProps) { + const autoDiscoveryInfo = useAutoDiscoveryInfo(); + const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url; + + const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); + + const [state, load] = useAsyncCallback( + useCallback(async () => { + const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]); + const loginFlows = promiseFulfilledResult(result[0]); + const registerReason = promiseRejectedResult(result[1]) as MatrixError | undefined; + let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest }; + + if (typeof registerReason === 'object' && registerReason.httpStatus) { + switch (registerReason.httpStatus) { + case RegisterFlowStatus.InvalidRequest: { + registerFlows = { status: RegisterFlowStatus.InvalidRequest }; + break; + } + case RegisterFlowStatus.RateLimited: { + registerFlows = { status: RegisterFlowStatus.RateLimited }; + break; + } + case RegisterFlowStatus.RegistrationDisabled: { + registerFlows = { status: RegisterFlowStatus.RegistrationDisabled }; + break; + } + case RegisterFlowStatus.FlowRequired: { + registerFlows = { + status: RegisterFlowStatus.FlowRequired, + data: registerReason.data as IAuthData, + }; + break; + } + default: { + registerFlows = { status: RegisterFlowStatus.InvalidRequest }; + } + } + } + + if (!loginFlows) { + throw new Error('Missing auth flow!'); + } + if ('errcode' in loginFlows) { + throw new Error('Failed to load auth flow!'); + } + + const authFlows: AuthFlows = { + loginFlows, + registerFlows, + }; + + return authFlows; + }, [mx]) + ); + + if (state.status === AsyncStatus.Idle) load(); + + if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { + return fallback?.(); + } + + if (state.status === AsyncStatus.Error) { + return error?.(state.error); + } + + return children(state.data); +} diff --git a/src/app/hooks/useAuthFlows.ts b/src/app/hooks/useAuthFlows.ts new file mode 100644 index 0000000000..2801368159 --- /dev/null +++ b/src/app/hooks/useAuthFlows.ts @@ -0,0 +1,36 @@ +import { createContext, useContext } from 'react'; +import { IAuthData } from 'matrix-js-sdk'; +import { ILoginFlowsResponse } from 'matrix-js-sdk/lib/@types/auth'; + +export enum RegisterFlowStatus { + FlowRequired = 401, + InvalidRequest = 400, + RegistrationDisabled = 403, + RateLimited = 429, +} + +export type RegisterFlowsResponse = + | { + status: RegisterFlowStatus.FlowRequired; + data: IAuthData; + } + | { + status: Exclude; + }; + +export type AuthFlows = { + loginFlows: ILoginFlowsResponse; + registerFlows: RegisterFlowsResponse; +}; + +const AuthFlowsContext = createContext(null); + +export const AuthFlowsProvider = AuthFlowsContext.Provider; + +export const useAuthFlows = (): AuthFlows => { + const authFlows = useContext(AuthFlowsContext); + if (!authFlows) { + throw new Error('Auth Flow info is not loaded!'); + } + return authFlows; +}; From 55cbff088c4ae06ac53608bd1b705c22497e2b4e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:12:21 +0530 Subject: [PATCH 10/68] add background dot pattern css --- src/app/styles/Patterns.css.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/app/styles/Patterns.css.ts diff --git a/src/app/styles/Patterns.css.ts b/src/app/styles/Patterns.css.ts new file mode 100644 index 0000000000..e455941673 --- /dev/null +++ b/src/app/styles/Patterns.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css'; +import { color, toRem } from 'folds'; + +export const BackgroundDotPattern = style({ + backgroundImage: `radial-gradient(${color.Background.ContainerActive} ${toRem(2)}, ${ + color.Background.Container + } ${toRem(2)})`, + backgroundSize: `${toRem(40)} ${toRem(40)}`, +}); From b51b2cc4e22b90d75fcb9aed8c4bb46152e4899b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:12:52 +0530 Subject: [PATCH 11/68] add promise utils --- src/app/utils/common.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index e007f222f9..0ce07dff7b 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -44,6 +44,17 @@ export const fulfilledPromiseSettledResult = (prs: PromiseSettledResult[]) return values; }, []); +export const promiseFulfilledResult = ( + settledResult: PromiseSettledResult +): T | undefined => { + if (settledResult.status === 'fulfilled') return settledResult.value; + return undefined; +}; +export const promiseRejectedResult = (settledResult: PromiseSettledResult): any => { + if (settledResult.status === 'rejected') return settledResult.reason; + return undefined; +}; + export const binarySearch = (items: T[], match: (item: T) => -1 | 0 | 1): T | undefined => { const search = (start: number, end: number): T | undefined => { if (start > end) return undefined; From 7f2d09eca76763deafcea13262ec8747b6b84e2f Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:13:39 +0530 Subject: [PATCH 12/68] init url based routing --- config.json | 2 + index.html | 2 +- src/app/pages/App.jsx | 17 --- src/app/pages/App.tsx | 71 ++++++++++ src/app/pages/auth/AuthFooter.tsx | 31 +++++ src/app/pages/auth/AuthLayout.tsx | 209 ++++++++++++++++++++++++++++ src/app/pages/auth/Login.tsx | 55 ++++++++ src/app/pages/auth/Register.tsx | 75 ++++++++++ src/app/pages/auth/ServerPicker.tsx | 136 ++++++++++++++++++ src/app/pages/auth/index.ts | 3 + src/app/pages/auth/styles.css.ts | 51 +++++++ src/app/pages/paths.ts | 2 + src/ext.d.ts | 5 + src/{index.jsx => index.tsx} | 17 ++- 14 files changed, 654 insertions(+), 22 deletions(-) delete mode 100644 src/app/pages/App.jsx create mode 100644 src/app/pages/App.tsx create mode 100644 src/app/pages/auth/AuthFooter.tsx create mode 100644 src/app/pages/auth/AuthLayout.tsx create mode 100644 src/app/pages/auth/Login.tsx create mode 100644 src/app/pages/auth/Register.tsx create mode 100644 src/app/pages/auth/ServerPicker.tsx create mode 100644 src/app/pages/auth/index.ts create mode 100644 src/app/pages/auth/styles.css.ts create mode 100644 src/app/pages/paths.ts rename src/{index.jsx => index.tsx} (63%) diff --git a/config.json b/config.json index ac7b1f3231..5218aa4d6e 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,6 @@ { + "appVersion": "3.2.0", + "basename": "/", "defaultHomeserver": 3, "homeserverList": [ "0wnz.at", diff --git a/index.html b/index.html index 6bc955c146..48f8e69ebf 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,6 @@ - + diff --git a/src/app/pages/App.jsx b/src/app/pages/App.jsx deleted file mode 100644 index 2828d7be80..0000000000 --- a/src/app/pages/App.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { StrictMode } from 'react'; -import { Provider } from 'jotai'; - -import { isAuthenticated } from '../../client/state/auth'; - -import Auth from '../templates/auth/Auth'; -import Client from '../templates/client/Client'; - -function App() { - return ( - - {isAuthenticated() ? : } - - ); -} - -export default App; diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx new file mode 100644 index 0000000000..abe6b0bb39 --- /dev/null +++ b/src/app/pages/App.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Provider as JotaiProvider } from 'jotai'; +import { + Outlet, + Route, + RouterProvider, + createBrowserRouter, + createRoutesFromElements, + redirect, +} from 'react-router-dom'; + +import { ClientConfigLoader } from '../components/ClientConfigLoader'; +import { ClientConfig, ClientConfigProvider } from '../hooks/useClientConfig'; +import { AuthLayout, Login, Register, authLayoutLoader } from './auth'; +import { LOGIN_PATH, REGISTER_PATH } from './paths'; +import { isAuthenticated } from '../../client/state/auth'; + +const createRouter = (clientConfig: ClientConfig) => { + const { basename } = clientConfig; + const router = createBrowserRouter( + createRoutesFromElements( + }> + { + if (isAuthenticated()) return redirect('/home'); + return redirect('/login'); + }} + /> + + }> + } /> + } /> + + + + + + } + > + home

} /> + direct

} /> + :spaceIdOrAlias

} /> + explore

} /> +
+ Page not found

} /> +
+ ), + { basename } + ); + return router; +}; + +// TODO: app crash boundary +function App() { + return ( +

loading

}> + {(clientConfig) => ( + + + + + + )} +
+ ); +} + +export default App; diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx new file mode 100644 index 0000000000..f2367dfd8d --- /dev/null +++ b/src/app/pages/auth/AuthFooter.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box, Text } from 'folds'; +import * as css from './styles.css'; +import { useClientConfig } from '../../hooks/useClientConfig'; + +export function AuthFooter() { + const { appVersion } = useClientConfig(); + + return ( + + + About + + + {`v${appVersion ?? '0.0.0'}`} + + + Twitter + + + Powered by Matrix + + + ); +} diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx new file mode 100644 index 0000000000..ba6b11d245 --- /dev/null +++ b/src/app/pages/auth/AuthLayout.tsx @@ -0,0 +1,209 @@ +import React, { useCallback, useEffect } from 'react'; +import { Box, Scroll, Spinner, Text, color } from 'folds'; +import { + LoaderFunction, + Outlet, + generatePath, + matchPath, + redirect, + useLocation, + useNavigate, + useParams, +} from 'react-router-dom'; +import classNames from 'classnames'; + +import { AuthFooter } from './AuthFooter'; +import * as css from './styles.css'; +import * as PatternsCss from '../../styles/Patterns.css'; +import { isAuthenticated } from '../../../client/state/auth'; +import { useClientConfig } from '../../hooks/useClientConfig'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { LOGIN_PATH, REGISTER_PATH } from '../paths'; +import CinnySVG from '../../../../public/res/svg/cinny.svg'; +import { ServerPicker } from './ServerPicker'; +import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api'; +import { SpecVersionsLoader } from '../../components/SpecVersionsLoader'; +import { SpecVersionsProvider } from '../../hooks/useSpecVersions'; +import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; +import { AuthFlowsLoader } from '../../components/AuthFlowsLoader'; +import { AuthFlowsProvider } from '../../hooks/useAuthFlows'; + +export const authLayoutLoader: LoaderFunction = () => { + // TODO: remove false case + const isAuth = false && isAuthenticated(); + if (isAuth) { + return redirect('/'); + } + + return null; +}; + +const currentAuthPath = (pathname: string): string => { + if (matchPath(LOGIN_PATH, pathname)) { + return LOGIN_PATH; + } + if (matchPath(REGISTER_PATH, pathname)) { + return REGISTER_PATH; + } + return LOGIN_PATH; +}; + +function AuthLayoutLoading({ message }: { message: string }) { + return ( + + + + {message} + + + ); +} + +function AuthLayoutError({ message }: { message: string }) { + return ( + + + {message} + + + ); +} + +export function AuthLayout() { + const navigate = useNavigate(); + const location = useLocation(); + const { server: urlEncodedServer } = useParams(); + + const { homeserverList, defaultHomeserver, allowCustomHomeservers } = useClientConfig(); + + const defaultServer = homeserverList?.[defaultHomeserver ?? 0] ?? ''; + let server = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; + if (!urlEncodedServer) { + navigate( + generatePath(currentAuthPath(location.pathname), { + server: encodeURIComponent(defaultServer), + }), + { replace: true } + ); + } + if (!allowCustomHomeservers && !homeserverList?.includes(server)) { + server = defaultServer; + } + + const [discoveryState, discoverServer] = useAsyncCallback( + useCallback((serverDomain: string) => autoDiscovery(fetch, serverDomain), []) + ); + + useEffect(() => { + if (server) discoverServer(server); + }, [discoverServer, server]); + + const selectServer = useCallback( + (newServer: string) => { + navigate( + generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) }) + ); + }, + [navigate, location] + ); + + const [autoDiscoveryError, autoDiscoveryInfo] = + discoveryState.status === AsyncStatus.Success ? discoveryState.data : []; + + let usableAutoDiscoveryInfo = autoDiscoveryInfo; + if (autoDiscoveryError?.action === AutoDiscoveryAction.IGNORE) { + usableAutoDiscoveryInfo = { + 'm.homeserver': { + base_url: autoDiscoveryError.host, + }, + }; + } + + return ( + + + + + Cinny Logo + + + Cinny + + + + + Homeserver + + + + {discoveryState.status === AsyncStatus.Loading && ( + + )} + {discoveryState.status === AsyncStatus.Error && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && ( + + )} + {usableAutoDiscoveryInfo && ( + + ( + + )} + error={() => ( + + )} + > + {(specVersions) => ( + + ( + + )} + error={() => ( + + )} + > + {(authFlows) => ( + + + + )} + + + )} + + + )} + + + + + + ); +} diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx new file mode 100644 index 0000000000..918b3ffb33 --- /dev/null +++ b/src/app/pages/auth/Login.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; +import { Link, generatePath, useParams } from 'react-router-dom'; +import { REGISTER_PATH } from '../paths'; +import { useAuthFlows } from '../../hooks/useAuthFlows'; + +export function Login() { + const { server } = useParams(); + const { loginFlows } = useAuthFlows(); + console.log(loginFlows); + + return ( + + + Login + + + + + Username + + + + + + Password + + + + + } + /> + + + + + + + Do not have an account?{' '} + Register + + + ); +} diff --git a/src/app/pages/auth/Register.tsx b/src/app/pages/auth/Register.tsx new file mode 100644 index 0000000000..8f88e64f5b --- /dev/null +++ b/src/app/pages/auth/Register.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; +import { Link, generatePath, useParams } from 'react-router-dom'; +import { LOGIN_PATH } from '../paths'; + +export function Register() { + const { server } = useParams(); + + return ( + + + Register + + + + + Username + + + + + + Password + + + + + } + /> + + + + Confirm Password + + + + + } + /> + + + + Email + + + + + + + + + Already have an account?{' '} + Login + + + ); +} diff --git a/src/app/pages/auth/ServerPicker.tsx b/src/app/pages/auth/ServerPicker.tsx new file mode 100644 index 0000000000..cc9ccec5e4 --- /dev/null +++ b/src/app/pages/auth/ServerPicker.tsx @@ -0,0 +1,136 @@ +import React, { + ChangeEventHandler, + KeyboardEventHandler, + MouseEventHandler, + useCallback, + useRef, + useState, +} from 'react'; +import { + Header, + Icon, + IconButton, + Icons, + Input, + Menu, + MenuItem, + PopOut, + Text, + config, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; + +import { useDebounce } from '../../hooks/useDebounce'; + +export function ServerPicker({ + defaultServer, + serverList, + allowCustomServer, + onServerChange, +}: { + defaultServer: string; + serverList: string[]; + allowCustomServer?: boolean; + onServerChange: (server: string) => void; +}) { + const [serverMenu, setServerMenu] = useState(false); + const serverInputRef = useRef(null); + + const handleServerChange: ChangeEventHandler = useDebounce( + useCallback( + (evt) => { + const inputServer = evt.target.value.trim(); + if (inputServer) onServerChange(inputServer); + }, + [onServerChange] + ), + { wait: 700 } + ); + + const handleKeyDown: KeyboardEventHandler = (evt) => { + if (evt.key === 'ArrowDown') { + evt.preventDefault(); + setServerMenu(true); + } + }; + + const handleServerSelect: MouseEventHandler = (evt) => { + const selectedServer = evt.currentTarget.getAttribute('data-server'); + if (selectedServer) { + const serverInput = serverInputRef.current; + if (serverInput) { + serverInput.value = selectedServer; + } + onServerChange(selectedServer); + } + setServerMenu(false); + }; + + return ( + setServerMenu(true)} + after={ + serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : ( + setServerMenu(false), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + }} + > + +
+ Homeserver List +
+
+ {serverList?.map((server) => ( + + {server} + + ))} +
+
+ + } + > + {(anchorRef) => ( + setServerMenu(true)} + variant={allowCustomServer ? 'Background' : 'Surface'} + size="300" + aria-pressed={serverMenu} + radii="300" + > + + + )} +
+ ) + } + /> + ); +} diff --git a/src/app/pages/auth/index.ts b/src/app/pages/auth/index.ts new file mode 100644 index 0000000000..ebba1f804c --- /dev/null +++ b/src/app/pages/auth/index.ts @@ -0,0 +1,3 @@ +export * from './AuthLayout'; +export * from './Login'; +export * from './Register'; diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts new file mode 100644 index 0000000000..30064d83d1 --- /dev/null +++ b/src/app/pages/auth/styles.css.ts @@ -0,0 +1,51 @@ +import { style } from '@vanilla-extract/css'; +import { DefaultReset, color, config, toRem } from 'folds'; + +export const AuthLayout = style({ + minHeight: '100%', + backgroundColor: color.Background.Container, + color: color.Background.OnContainer, + padding: config.space.S400, + paddingRight: config.space.S200, + paddingBottom: 0, + position: 'relative', +}); + +export const AuthCard = style({ + marginTop: '10vh', + maxWidth: config.size.ModalWidth300, + width: '100%', + backgroundColor: color.Surface.Container, + color: color.Surface.OnContainer, + borderRadius: config.radii.R400, + boxShadow: config.shadow.E100, + border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, + overflow: 'hidden', +}); + +export const AuthLogo = style([ + DefaultReset, + { + position: 'absolute', + transform: 'translateY(-50%)', + border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, + borderRadius: '50%', + + width: toRem(64), + height: toRem(64), + }, +]); + +export const AuthHeader = style({ + padding: `0 ${config.space.S400}`, + paddingTop: toRem(40), +}); + +export const AuthCardContent = style({ + padding: toRem(44), + gap: toRem(44), +}); + +export const AuthFooter = style({ + padding: config.space.S200, +}); diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts new file mode 100644 index 0000000000..90fcae8afe --- /dev/null +++ b/src/app/pages/paths.ts @@ -0,0 +1,2 @@ +export const LOGIN_PATH = '/login/:server?/'; +export const REGISTER_PATH = '/register/:server?/'; diff --git a/src/ext.d.ts b/src/ext.d.ts index 5593b6e7be..27ff11cfb1 100644 --- a/src/ext.d.ts +++ b/src/ext.d.ts @@ -26,3 +26,8 @@ declare module 'browser-encrypt-attachment' { info: EncryptedAttachmentInfo ): Promise; } + +declare module '*.svg' { + const content: string; + export default content; +} diff --git a/src/index.jsx b/src/index.tsx similarity index 63% rename from src/index.jsx rename to src/index.tsx index 0ec65e1699..1d86420371 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -15,9 +15,18 @@ import settings from './client/state/settings'; import App from './app/pages/App'; document.body.classList.add(configClass, varsClass); - settings.applyTheme(); -const rootContainer = document.getElementById('root'); -const root = createRoot(rootContainer); -root.render(); +const mountApp = () => { + const rootContainer = document.getElementById('root'); + + if (rootContainer === null) { + console.error('Root container element not found!'); + return; + } + + const root = createRoot(rootContainer); + root.render(); +}; + +mountApp(); From 12c42699360259fd4b24d474575a0bafb7274b18 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:31:34 +0530 Subject: [PATCH 13/68] update auth route server path as effect --- src/app/pages/App.tsx | 1 + src/app/pages/auth/AuthLayout.tsx | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index abe6b0bb39..8dd81b3974 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -56,6 +56,7 @@ const createRouter = (clientConfig: ClientConfig) => { // TODO: app crash boundary function App() { return ( + // TODO: initial loading screen

loading

}> {(clientConfig) => ( diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index ba6b11d245..206c550250 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -76,16 +76,8 @@ export function AuthLayout() { const { homeserverList, defaultHomeserver, allowCustomHomeservers } = useClientConfig(); - const defaultServer = homeserverList?.[defaultHomeserver ?? 0] ?? ''; + const defaultServer = homeserverList?.[defaultHomeserver ?? 0] ?? 'matrix.org'; let server = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; - if (!urlEncodedServer) { - navigate( - generatePath(currentAuthPath(location.pathname), { - server: encodeURIComponent(defaultServer), - }), - { replace: true } - ); - } if (!allowCustomHomeservers && !homeserverList?.includes(server)) { server = defaultServer; } @@ -98,6 +90,18 @@ export function AuthLayout() { if (server) discoverServer(server); }, [discoverServer, server]); + // if server is mismatches with path server, update path + useEffect(() => { + if (!urlEncodedServer || decodeURIComponent(urlEncodedServer) !== server) { + navigate( + generatePath(currentAuthPath(location.pathname), { + server: encodeURIComponent(server), + }), + { replace: true } + ); + } + }, [urlEncodedServer, navigate, location, server]); + const selectServer = useCallback( (newServer: string) => { navigate( From c7a1600327e94ac0bc7bdeed7fc5e3e3025d8c36 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:38:00 +0530 Subject: [PATCH 14/68] add auth server hook --- src/app/hooks/useAuthServer.ts | 14 +++++++ src/app/pages/auth/AuthLayout.tsx | 69 ++++++++++++++++--------------- 2 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 src/app/hooks/useAuthServer.ts diff --git a/src/app/hooks/useAuthServer.ts b/src/app/hooks/useAuthServer.ts new file mode 100644 index 0000000000..f77566f5f3 --- /dev/null +++ b/src/app/hooks/useAuthServer.ts @@ -0,0 +1,14 @@ +import { createContext, useContext } from 'react'; + +const AuthServerContext = createContext(null); + +export const AuthServerProvider = AuthServerContext.Provider; + +export const useAuthServer = (): string => { + const server = useContext(AuthServerContext); + if (server === null) { + throw new Error('Auth server is not provided!'); + } + + return server; +}; diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 206c550250..311a5ee30a 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -27,6 +27,7 @@ import { SpecVersionsProvider } from '../../hooks/useSpecVersions'; import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; import { AuthFlowsLoader } from '../../components/AuthFlowsLoader'; import { AuthFlowsProvider } from '../../hooks/useAuthFlows'; +import { AuthServerProvider } from '../../hooks/useAuthServer'; export const authLayoutLoader: LoaderFunction = () => { // TODO: remove false case @@ -76,8 +77,8 @@ export function AuthLayout() { const { homeserverList, defaultHomeserver, allowCustomHomeservers } = useClientConfig(); - const defaultServer = homeserverList?.[defaultHomeserver ?? 0] ?? 'matrix.org'; - let server = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; + const defaultServer: string = homeserverList?.[defaultHomeserver ?? 0] ?? 'matrix.org'; + let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; if (!allowCustomHomeservers && !homeserverList?.includes(server)) { server = defaultServer; } @@ -172,37 +173,39 @@ export function AuthLayout() { )} {usableAutoDiscoveryInfo && ( - - ( - - )} - error={() => ( - - )} - > - {(specVersions) => ( - - ( - - )} - error={() => ( - - )} - > - {(authFlows) => ( - - - - )} - - - )} - - + + + ( + + )} + error={() => ( + + )} + > + {(specVersions) => ( + + ( + + )} + error={() => ( + + )} + > + {(authFlows) => ( + + + + )} + + + )} + + + )} From dab1747bc9ad9516ca846dde7ce932d7a31f8fe1 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 20 Dec 2023 21:21:26 +0530 Subject: [PATCH 15/68] always use server from discovery info in context --- src/app/pages/auth/AuthLayout.tsx | 73 ++++++++++++++++++++++++------- src/app/pages/auth/Login.tsx | 10 ++--- src/app/pages/auth/Register.tsx | 8 ++-- 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 311a5ee30a..2f24b98c0c 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Box, Scroll, Spinner, Text, color } from 'folds'; import { LoaderFunction, @@ -21,7 +21,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { LOGIN_PATH, REGISTER_PATH } from '../paths'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; import { ServerPicker } from './ServerPicker'; -import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api'; +import { + AutoDiscoveryAction, + AutoDiscoveryError, + AutoDiscoveryInfo, + autoDiscovery, +} from '../../cs-api'; import { SpecVersionsLoader } from '../../components/SpecVersionsLoader'; import { SpecVersionsProvider } from '../../hooks/useSpecVersions'; import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; @@ -70,6 +75,36 @@ function AuthLayoutError({ message }: { message: string }) { ); } +const createDiscoveryInfo = ( + serverName: string, + autoDiscoveryError?: AutoDiscoveryError, + autoDiscoveryInfo?: AutoDiscoveryInfo +): + | undefined + | { + serverName: string; + info: AutoDiscoveryInfo; + } => { + if (autoDiscoveryInfo) { + return { + serverName, + info: autoDiscoveryInfo, + }; + } + if (autoDiscoveryError?.action === AutoDiscoveryAction.IGNORE) { + const tempAutoDiscoveryInfo = { + 'm.homeserver': { + base_url: autoDiscoveryError?.host, + }, + }; + return { + serverName, + info: tempAutoDiscoveryInfo, + }; + } + return undefined; +}; + export function AuthLayout() { const navigate = useNavigate(); const location = useLocation(); @@ -84,7 +119,13 @@ export function AuthLayout() { } const [discoveryState, discoverServer] = useAsyncCallback( - useCallback((serverDomain: string) => autoDiscovery(fetch, serverDomain), []) + useCallback(async (serverName: string) => { + const response = await autoDiscovery(fetch, serverName); + return { + serverName, + response, + }; + }, []) ); useEffect(() => { @@ -113,16 +154,16 @@ export function AuthLayout() { ); const [autoDiscoveryError, autoDiscoveryInfo] = - discoveryState.status === AsyncStatus.Success ? discoveryState.data : []; + discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : []; - let usableAutoDiscoveryInfo = autoDiscoveryInfo; - if (autoDiscoveryError?.action === AutoDiscoveryAction.IGNORE) { - usableAutoDiscoveryInfo = { - 'm.homeserver': { - base_url: autoDiscoveryError.host, - }, - }; - } + const serverDiscovery = useMemo(() => { + if (discoveryState.status !== AsyncStatus.Success) return undefined; + return createDiscoveryInfo( + discoveryState.data.serverName, + autoDiscoveryError, + autoDiscoveryInfo + ); + }, [discoveryState, autoDiscoveryError, autoDiscoveryInfo]); return ( @@ -172,13 +213,13 @@ export function AuthLayout() { {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && ( )} - {usableAutoDiscoveryInfo && ( - - + {serverDiscovery && ( + + ( )} error={() => ( diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 918b3ffb33..a732ce678d 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; -import { Link, generatePath, useParams } from 'react-router-dom'; +import { Link, generatePath } from 'react-router-dom'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; +import { useAuthServer } from '../../hooks/useAuthServer'; export function Login() { - const { server } = useParams(); + const server = useAuthServer(); const { loginFlows } = useAuthFlows(); - console.log(loginFlows); + console.log(server, loginFlows); return ( @@ -47,8 +48,7 @@ export function Login() { - Do not have an account?{' '} - Register + Do not have an account? Register ); diff --git a/src/app/pages/auth/Register.tsx b/src/app/pages/auth/Register.tsx index 8f88e64f5b..e8d2944bc1 100644 --- a/src/app/pages/auth/Register.tsx +++ b/src/app/pages/auth/Register.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; -import { Link, generatePath, useParams } from 'react-router-dom'; +import { Link, generatePath } from 'react-router-dom'; import { LOGIN_PATH } from '../paths'; +import { useAuthServer } from '../../hooks/useAuthServer'; export function Register() { - const { server } = useParams(); + const server = useAuthServer(); return ( @@ -67,8 +68,7 @@ export function Register() { - Already have an account?{' '} - Login + Already have an account? Login ); From 1d7f02741094d2c304ab3edf3ffaa9b7caf33e71 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:30:21 +0530 Subject: [PATCH 16/68] login - WIP --- config.json | 2 +- src/app/hooks/useParsedLoginFlows.ts | 38 +++ src/app/pages/App.tsx | 11 +- src/app/pages/auth/Login.tsx | 346 ++++++++++++++++++++++++--- src/app/utils/regex.ts | 3 + src/types/utils.ts | 3 + 6 files changed, 359 insertions(+), 44 deletions(-) create mode 100644 src/app/hooks/useParsedLoginFlows.ts create mode 100644 src/types/utils.ts diff --git a/config.json b/config.json index f417d7b3b3..2386a1f690 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,7 @@ { "appVersion": "3.2.0", "basename": "/", - "defaultHomeserver": 3, + "defaultHomeserver": 2, "homeserverList": [ "converser.eu", "envs.net", diff --git a/src/app/hooks/useParsedLoginFlows.ts b/src/app/hooks/useParsedLoginFlows.ts new file mode 100644 index 0000000000..14ecfb9dda --- /dev/null +++ b/src/app/hooks/useParsedLoginFlows.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth'; +import { WithRequiredProp } from '../../types/utils'; + +export type Required_SSOFlow = WithRequiredProp; +export const getSSOFlow = (loginFlows: LoginFlow[]): Required_SSOFlow | undefined => + loginFlows.find( + (flow) => + (flow.type === 'm.login.sso' || flow.type === 'm.login.cas') && + 'identity_providers' in flow && + Array.isArray(flow.identity_providers) && + flow.identity_providers.length > 0 + ) as Required_SSOFlow | undefined; + +export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined => + loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow; +export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined => + loginFlows.find((flow) => flow.type === 'm.login.token') as ILoginFlow & { + type: 'm.login.token'; + }; + +export type ParsedLoginFlows = { + password?: LoginFlow; + token?: LoginFlow; + sso?: Required_SSOFlow; +}; +export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => { + const parsedFlow: ParsedLoginFlows = useMemo( + () => ({ + password: getPasswordFlow(loginFlows), + token: getTokenFlow(loginFlows), + sso: getSSOFlow(loginFlows), + }), + [loginFlows] + ); + + return parsedFlow; +}; diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 8dd81b3974..691fc07a03 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -14,6 +14,7 @@ import { ClientConfig, ClientConfigProvider } from '../hooks/useClientConfig'; import { AuthLayout, Login, Register, authLayoutLoader } from './auth'; import { LOGIN_PATH, REGISTER_PATH } from './paths'; import { isAuthenticated } from '../../client/state/auth'; +import Client from '../templates/client/Client'; const createRouter = (clientConfig: ClientConfig) => { const { basename } = clientConfig; @@ -33,14 +34,8 @@ const createRouter = (clientConfig: ClientConfig) => { } /> - - - - } - > - home

} /> + + } /> direct

} /> :spaceIdOrAlias

} /> explore

} /> diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index a732ce678d..85ff26387d 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -1,52 +1,328 @@ -import React from 'react'; -import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; -import { Link, generatePath } from 'react-router-dom'; +import React, { FormEventHandler, useCallback, useState } from 'react'; +import { + Box, + Button, + Header, + Icon, + IconButton, + Icons, + Input, + Menu, + PopOut, + Text, + color, + config, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; +import { Link, generatePath, useSearchParams } from 'react-router-dom'; +import { createClient } from 'matrix-js-sdk'; +import to from 'await-to-js'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; import { useAuthServer } from '../../hooks/useAuthServer'; +import { UseStateProvider } from '../../components/UseStateProvider'; +import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { EMAIL_REGEX } from '../../utils/regex'; +import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; +import { useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { AutoDiscoveryAction, autoDiscovery, specVersions } from '../../cs-api'; + +function UsernameHint({ server }: { server: string }) { + const [open, setOpen] = useState(false); + return ( + setOpen(false), + clickOutsideDeactivates: true, + }} + > + +
+ Hint +
+ + + + Username: + {' '} + johndoe + + + + Matrix ID: + + {` @johndoe:${server}`} + + + + Email: + + {` johndoe@${server}`} + + +
+ + } + > + {(targetRef) => ( + setOpen(true)} + ref={targetRef} + type="button" + variant="Background" + size="400" + radii="300" + > + + + )} +
+ ); +} + +type PasswordLoginFormProps = { + defaultUsername?: string; + defaultEmail?: string; +}; +export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { + const server = useAuthServer(); + + const serverDiscovery = useAutoDiscoveryInfo(); + const baseUrl = serverDiscovery['m.homeserver'].base_url; + + const [loginState, startLogin] = useAsyncCallback( + useCallback(async (serverBaseUrl: string, data: object) => { + const mx = createClient({ baseUrl: serverBaseUrl }); + // const [err, res] = await to<{ + // access_token: string, + // device_id: string, + // user_id: string, + // expires_in_ms?: number, + // refresh_token: string, + // }>(mx.login('m.login.password', data)); + // console.log(res?.status); + // console.log(err, res); + return undefined; + }, []) + ); + + const handleUsernameLogin = (username: string, password: string) => { + startLogin(baseUrl, { + identifier: { + type: 'm.id.user', + user: username, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + const handleMxIdLogin = async (mxId: string, password: string) => { + const mxIdServer = getMxIdServer(mxId); + const mxIdUsername = getMxIdLocalPart(mxId); + if (!mxIdServer || !mxIdUsername) return; + + const [, discovery] = await to(autoDiscovery(fetch, mxIdServer)); + + let mxIdBaseUrl: string | undefined; + const [discoverError, discoveryInfo] = discovery ?? []; + if (discoverError?.action === AutoDiscoveryAction.IGNORE) { + mxIdBaseUrl = discoverError.host; + } + if (discoveryInfo) { + mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; + } + if (!mxIdBaseUrl) { + alert( + 'Failed to find MXID homeserver! Please enter server in Homeserver input for more details.' + ); + return; + } + const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); + if (!versions) { + alert('Homeserver URL does not appear to be a valid Matrix homeserver.'); + return; + } + + startLogin(mxIdBaseUrl, { + identifier: { + type: 'm.id.user', + user: mxIdUsername, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + const handleEmailLogin = (email: string, password: string) => { + startLogin(baseUrl, { + identifier: { + type: 'm.id.thirdparty', + medium: 'email', + address: email, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { usernameInput, passwordInput } = evt.target as HTMLFormElement & { + usernameInput: HTMLInputElement; + passwordInput: HTMLInputElement; + }; + + const username = usernameInput.value.trim(); + const password = passwordInput.value; + if (!username) { + usernameInput.focus(); + return; + } + if (!password) { + passwordInput.focus(); + return; + } + + if (isUserId(username)) { + handleMxIdLogin(username, password); + return; + } + if (EMAIL_REGEX.test(username)) { + handleEmailLogin(username, password); + return; + } + handleUsernameLogin(username, password); + }; + + return ( + + + + Username + + } + /> + + + + Password + + + {(visible, setVisible) => ( + setVisible(!visible)} + type="button" + variant="Background" + size="400" + radii="300" + > + + + } + /> + )} + + + + {/* TODO: make reset password path */} + Forget Password? + + + + + + + ); +} + +const getLoginSearchParams = ( + searchParams: URLSearchParams +): { + username?: string; + email?: string; + loginToken?: string; +} => ({ + username: searchParams.get('username') ?? undefined, + email: searchParams.get('email') ?? undefined, + loginToken: searchParams.get('loginToken') ?? undefined, +}); export function Login() { const server = useAuthServer(); - const { loginFlows } = useAuthFlows(); - console.log(server, loginFlows); + const { loginFlows, registerFlows } = useAuthFlows(); + const [searchParams] = useSearchParams(); + const loginSearchParams = getLoginSearchParams(searchParams); + + const parsedFlows = useParsedLoginFlows(loginFlows.flows); + console.log(parsedFlows); + console.log(server, loginFlows, registerFlows); return ( Login - - - - Username - - - - - - Password - - - - - } + {parsedFlows.token && false &&

Token login

} + {parsedFlows.password && ( + <> + -
- - -
- + + + )} Do not have an account? Register diff --git a/src/app/utils/regex.ts b/src/app/utils/regex.ts index 5188bef0fb..281f12006e 100644 --- a/src/app/utils/regex.ts +++ b/src/app/utils/regex.ts @@ -1,5 +1,8 @@ export const HTTP_URL_PATTERN = `https?:\\/\\/(?:www\\.)?(?:[^\\s)]*)(?()[\]\\.,;:\s@\\"]+(\.[^<>()[\]\\.,;:\s@\\"]+)*)|(\\".+\\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + export const URL_NEG_LB = '(? = Type & { + [Property in Key]-?: Type[Property]; +}; From bd7564a7f17666b733581e490f92772ea86d1097 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 24 Dec 2023 10:42:59 +0530 Subject: [PATCH 17/68] upgrade jotai to v2 --- package-lock.json | 49 +++++-------------------------- package.json | 2 +- src/app/pages/auth/Login.tsx | 4 +-- src/app/state/hooks/inviteList.ts | 37 ++++++++++------------- src/app/state/hooks/roomList.ts | 34 ++++++++++----------- src/app/state/hooks/settings.ts | 20 ++++++------- src/app/state/inviteList.ts | 4 +-- src/app/state/list.ts | 2 +- src/app/state/mDirectList.ts | 9 ++---- src/app/state/mutedRoomList.ts | 9 ++---- src/app/state/roomList.ts | 9 ++---- src/app/state/roomToParents.ts | 6 ++-- src/app/state/roomToUnread.ts | 6 ++-- src/app/state/sessions.ts | 15 ++++++++++ src/app/state/settings.ts | 2 +- src/app/state/tabToRoom.ts | 2 +- src/app/state/typingMembers.ts | 6 +++- src/app/state/upload.ts | 2 +- src/app/state/utils.ts | 2 +- 19 files changed, 95 insertions(+), 125 deletions(-) create mode 100644 src/app/state/sessions.ts diff --git a/package-lock.json b/package-lock.json index 2df6fee212..15ddc576ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "html-react-parser": "4.2.0", "immer": "9.0.16", "is-hotkey": "0.2.0", - "jotai": "1.12.0", + "jotai": "2.6.0", "katex": "0.16.4", "linkify-html": "4.0.2", "linkify-react": "4.1.1", @@ -5983,54 +5983,21 @@ "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==" }, "node_modules/jotai": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.12.0.tgz", - "integrity": "sha512-IhyBmjxU1sE2Ni/MUK7gQAb8QvCM6yd1/K5jtQzgQBmmjCjgfXZkkk1rYlQAIRp2KoQk0Y+yzhm1f5cZ7kegnw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.6.0.tgz", + "integrity": "sha512-Vt6hsc04Km4j03l+Ax+Sc+FVft5cRJhqgxt6GTz6GM2eM3DyX3CdBdzcG0z2FrlZToL1/0OAkqDghIyARWnSuQ==", "engines": { "node": ">=12.20.0" }, "peerDependencies": { - "@babel/core": "*", - "@babel/template": "*", - "jotai-immer": "*", - "jotai-optics": "*", - "jotai-redux": "*", - "jotai-tanstack-query": "*", - "jotai-urql": "*", - "jotai-valtio": "*", - "jotai-xstate": "*", - "jotai-zustand": "*", - "react": ">=16.8" + "@types/react": ">=17.0.0", + "react": ">=17.0.0" }, "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@babel/template": { - "optional": true - }, - "jotai-immer": { - "optional": true - }, - "jotai-optics": { - "optional": true - }, - "jotai-redux": { - "optional": true - }, - "jotai-tanstack-query": { - "optional": true - }, - "jotai-urql": { - "optional": true - }, - "jotai-valtio": { - "optional": true - }, - "jotai-xstate": { + "@types/react": { "optional": true }, - "jotai-zustand": { + "react": { "optional": true } } diff --git a/package.json b/package.json index d494df84a5..31eb95a0e4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "html-react-parser": "4.2.0", "immer": "9.0.16", "is-hotkey": "0.2.0", - "jotai": "1.12.0", + "jotai": "2.6.0", "katex": "0.16.4", "linkify-html": "4.0.2", "linkify-react": "4.1.1", diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 85ff26387d..88133acf3c 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -233,7 +233,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog style={{ paddingRight: config.space.S200 }} name="passwordInput" type={visible ? 'text' : 'password'} - variant="Background" + variant={visible ? 'Warning' : 'Background'} size="500" outlined required @@ -241,7 +241,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog setVisible(!visible)} type="button" - variant="Background" + variant={visible ? 'Warning' : 'Background'} size="400" radii="300" > diff --git a/src/app/state/hooks/inviteList.ts b/src/app/state/hooks/inviteList.ts index f8b7e057c9..ffe44445d9 100644 --- a/src/app/state/hooks/inviteList.ts +++ b/src/app/state/hooks/inviteList.ts @@ -1,28 +1,26 @@ -import { useAtomValue, WritableAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import { selectAtom } from 'jotai/utils'; import { MatrixClient } from 'matrix-js-sdk'; import { useCallback } from 'react'; import { isDirectInvite, isRoom, isSpace, isUnsupportedRoom } from '../../utils/room'; -import { compareRoomsEqual, RoomsAction } from '../utils'; -import { MDirectAction } from '../mDirectList'; +import { compareRoomsEqual } from '../utils'; +import { mDirectAtom } from '../mDirectList'; +import { allInvitesAtom } from '../inviteList'; -export const useSpaceInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom -) => { +export const useSpaceInvites = (mx: MatrixClient, invitesAtom: typeof allInvitesAtom) => { const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))), [mx] ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual)); }; export const useRoomInvites = ( mx: MatrixClient, - allInvitesAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> + invitesAtom: typeof allInvitesAtom, + directAtom: typeof mDirectAtom ) => { - const mDirects = useAtomValue(mDirectAtom); + const mDirects = useAtomValue(directAtom); const selector = useCallback( (rooms: string[]) => rooms.filter( @@ -32,15 +30,15 @@ export const useRoomInvites = ( ), [mx, mDirects] ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual)); }; export const useDirectInvites = ( mx: MatrixClient, - allInvitesAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> + invitesAtom: typeof allInvitesAtom, + directAtom: typeof mDirectAtom ) => { - const mDirects = useAtomValue(mDirectAtom); + const mDirects = useAtomValue(directAtom); const selector = useCallback( (rooms: string[]) => rooms.filter( @@ -48,16 +46,13 @@ export const useDirectInvites = ( ), [mx, mDirects] ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual)); }; -export const useUnsupportedInvites = ( - mx: MatrixClient, - allInvitesAtom: WritableAtom -) => { +export const useUnsupportedInvites = (mx: MatrixClient, invitesAtom: typeof allInvitesAtom) => { const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))), [mx] ); - return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual)); }; diff --git a/src/app/state/hooks/roomList.ts b/src/app/state/hooks/roomList.ts index 5d0890bddb..c0a7bfb88e 100644 --- a/src/app/state/hooks/roomList.ts +++ b/src/app/state/hooks/roomList.ts @@ -1,54 +1,52 @@ -import { useAtomValue, WritableAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import { selectAtom } from 'jotai/utils'; import { MatrixClient } from 'matrix-js-sdk'; import { useCallback } from 'react'; import { isRoom, isSpace, isUnsupportedRoom } from '../../utils/room'; -import { compareRoomsEqual, RoomsAction } from '../utils'; -import { MDirectAction } from '../mDirectList'; +import { compareRoomsEqual } from '../utils'; +import { mDirectAtom } from '../mDirectList'; +import { allRoomsAtom } from '../roomList'; -export const useSpaces = (mx: MatrixClient, allRoomsAtom: WritableAtom) => { +export const useSpaces = (mx: MatrixClient, roomsAtom: typeof allRoomsAtom) => { const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))), [mx] ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual)); }; export const useRooms = ( mx: MatrixClient, - allRoomsAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> + roomsAtom: typeof allRoomsAtom, + directAtom: typeof mDirectAtom ) => { - const mDirects = useAtomValue(mDirectAtom); + const mDirects = useAtomValue(directAtom); const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && !mDirects.has(roomId)), [mx, mDirects] ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual)); }; export const useDirects = ( mx: MatrixClient, - allRoomsAtom: WritableAtom, - mDirectAtom: WritableAtom, MDirectAction> + roomsAtom: typeof allRoomsAtom, + directAtom: typeof mDirectAtom ) => { - const mDirects = useAtomValue(mDirectAtom); + const mDirects = useAtomValue(directAtom); const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && mDirects.has(roomId)), [mx, mDirects] ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual)); }; -export const useUnsupportedRooms = ( - mx: MatrixClient, - allRoomsAtom: WritableAtom -) => { +export const useUnsupportedRooms = (mx: MatrixClient, roomsAtom: typeof allRoomsAtom) => { const selector = useCallback( (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))), [mx] ); - return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual)); + return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual)); }; diff --git a/src/app/state/hooks/settings.ts b/src/app/state/hooks/settings.ts index 43b565534a..d90c766461 100644 --- a/src/app/state/hooks/settings.ts +++ b/src/app/state/hooks/settings.ts @@ -1,16 +1,16 @@ -import { atom, useAtomValue, useSetAtom, WritableAtom } from 'jotai'; -import { SetAtom } from 'jotai/core/atom'; +import { atom, useAtomValue, useSetAtom } from 'jotai'; import { selectAtom } from 'jotai/utils'; import { useMemo } from 'react'; -import { Settings } from '../settings'; +import { Settings, settingsAtom as sAtom } from '../settings'; -export const useSetSetting = ( - settingsAtom: WritableAtom, - key: K -) => { +export type SettingSetter = + | Settings[K] + | ((s: Settings[K]) => Settings[K]); + +export const useSetSetting = (settingsAtom: typeof sAtom, key: K) => { const setterAtom = useMemo( () => - atom Settings[K])>(null, (get, set, value) => { + atom], undefined>(null, (get, set, value) => { const s = { ...get(settingsAtom) }; s[key] = typeof value === 'function' ? value(s[key]) : value; set(settingsAtom, s); @@ -22,9 +22,9 @@ export const useSetSetting = ( }; export const useSetting = ( - settingsAtom: WritableAtom, + settingsAtom: typeof sAtom, key: K -): [Settings[K], SetAtom Settings[K]), void>] => { +): [Settings[K], ReturnType>] => { const selector = useMemo(() => (s: Settings) => s[key], [key]); const setting = useAtomValue(selectAtom(settingsAtom, selector)); diff --git a/src/app/state/inviteList.ts b/src/app/state/inviteList.ts index 463fd352d1..a6dc79668b 100644 --- a/src/app/state/inviteList.ts +++ b/src/app/state/inviteList.ts @@ -5,7 +5,7 @@ import { Membership } from '../../types/matrix/room'; import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils'; const baseRoomsAtom = atom([]); -export const allInvitesAtom = atom( +export const allInvitesAtom = atom( (get) => get(baseRoomsAtom), (get, set, action) => { if (action.type === 'INITIALIZE') { @@ -22,7 +22,7 @@ export const allInvitesAtom = atom( export const useBindAllInvitesAtom = ( mx: MatrixClient, - allRooms: WritableAtom + allRooms: WritableAtom ) => { useBindRoomsWithMembershipsAtom( mx, diff --git a/src/app/state/list.ts b/src/app/state/list.ts index 4f5a619148..670e6db18b 100644 --- a/src/app/state/list.ts +++ b/src/app/state/list.ts @@ -12,7 +12,7 @@ export type ListAction = export const createListAtom = () => { const baseListAtom = atom([]); - return atom>( + return atom], undefined>( (get) => get(baseListAtom), (get, set, action) => { const items = get(baseListAtom); diff --git a/src/app/state/mDirectList.ts b/src/app/state/mDirectList.ts index 96e2f0d03e..1fa8311f4a 100644 --- a/src/app/state/mDirectList.ts +++ b/src/app/state/mDirectList.ts @@ -1,4 +1,4 @@ -import { atom, useSetAtom, WritableAtom } from 'jotai'; +import { atom, useSetAtom } from 'jotai'; import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { AccountDataEvent } from '../../types/matrix/accountData'; @@ -10,17 +10,14 @@ export type MDirectAction = { }; const baseMDirectAtom = atom(new Set()); -export const mDirectAtom = atom, MDirectAction>( +export const mDirectAtom = atom, [MDirectAction], undefined>( (get) => get(baseMDirectAtom), (get, set, action) => { set(baseMDirectAtom, action.rooms); } ); -export const useBindMDirectAtom = ( - mx: MatrixClient, - mDirect: WritableAtom, MDirectAction> -) => { +export const useBindMDirectAtom = (mx: MatrixClient, mDirect: typeof mDirectAtom) => { const setMDirect = useSetAtom(mDirect); useEffect(() => { diff --git a/src/app/state/mutedRoomList.ts b/src/app/state/mutedRoomList.ts index d456f8533b..f818450bbe 100644 --- a/src/app/state/mutedRoomList.ts +++ b/src/app/state/mutedRoomList.ts @@ -1,4 +1,4 @@ -import { atom, WritableAtom, useSetAtom } from 'jotai'; +import { atom, useSetAtom } from 'jotai'; import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { MuteChanges } from '../../types/matrix/room'; @@ -21,7 +21,7 @@ export const muteChangesAtom = atom({ }); const baseMutedRoomsAtom = atom(new Set()); -export const mutedRoomsAtom = atom, MutedRoomsUpdate>( +export const mutedRoomsAtom = atom, [MutedRoomsUpdate], undefined>( (get) => get(baseMutedRoomsAtom), (get, set, action) => { const mutedRooms = new Set([...get(mutedRoomsAtom)]); @@ -45,10 +45,7 @@ export const mutedRoomsAtom = atom, MutedRoomsUpdate>( } ); -export const useBindMutedRoomsAtom = ( - mx: MatrixClient, - mutedAtom: WritableAtom, MutedRoomsUpdate> -) => { +export const useBindMutedRoomsAtom = (mx: MatrixClient, mutedAtom: typeof mutedRoomsAtom) => { const setMuted = useSetAtom(mutedAtom); useEffect(() => { diff --git a/src/app/state/roomList.ts b/src/app/state/roomList.ts index 7a793d8c31..e0fa170fb0 100644 --- a/src/app/state/roomList.ts +++ b/src/app/state/roomList.ts @@ -1,11 +1,11 @@ -import { atom, WritableAtom } from 'jotai'; +import { atom } from 'jotai'; import { MatrixClient } from 'matrix-js-sdk'; import { useMemo } from 'react'; import { Membership } from '../../types/matrix/room'; import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils'; const baseRoomsAtom = atom([]); -export const allRoomsAtom = atom( +export const allRoomsAtom = atom( (get) => get(baseRoomsAtom), (get, set, action) => { if (action.type === 'INITIALIZE') { @@ -19,10 +19,7 @@ export const allRoomsAtom = atom( }); } ); -export const useBindAllRoomsAtom = ( - mx: MatrixClient, - allRooms: WritableAtom -) => { +export const useBindAllRoomsAtom = (mx: MatrixClient, allRooms: typeof allRoomsAtom) => { useBindRoomsWithMembershipsAtom( mx, allRooms, diff --git a/src/app/state/roomToParents.ts b/src/app/state/roomToParents.ts index 374ddd5752..1e2ef18c4d 100644 --- a/src/app/state/roomToParents.ts +++ b/src/app/state/roomToParents.ts @@ -1,5 +1,5 @@ import produce from 'immer'; -import { atom, useSetAtom, WritableAtom } from 'jotai'; +import { atom, useSetAtom } from 'jotai'; import { ClientEvent, MatrixClient, @@ -34,7 +34,7 @@ export type RoomToParentsAction = }; const baseRoomToParents = atom(new Map()); -export const roomToParentsAtom = atom( +export const roomToParentsAtom = atom( (get) => get(baseRoomToParents), (get, set, action) => { if (action.type === 'INITIALIZE') { @@ -69,7 +69,7 @@ export const roomToParentsAtom = atom( export const useBindRoomToParentsAtom = ( mx: MatrixClient, - roomToParents: WritableAtom + roomToParents: typeof roomToParentsAtom ) => { const setRoomToParents = useSetAtom(roomToParents); diff --git a/src/app/state/roomToUnread.ts b/src/app/state/roomToUnread.ts index 0c7b6bd697..ad388763ed 100644 --- a/src/app/state/roomToUnread.ts +++ b/src/app/state/roomToUnread.ts @@ -1,5 +1,5 @@ import produce from 'immer'; -import { atom, useSetAtom, PrimitiveAtom, WritableAtom, useAtomValue } from 'jotai'; +import { atom, useSetAtom, PrimitiveAtom, useAtomValue } from 'jotai'; import { IRoomTimelineData, MatrixClient, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk'; import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts'; import { useEffect } from 'react'; @@ -82,7 +82,7 @@ const deleteUnreadInfo = (roomToUnread: RoomToUnread, allParents: Set, r }; const baseRoomToUnread = atom(new Map()); -export const roomToUnreadAtom = atom( +export const roomToUnreadAtom = atom( (get) => get(baseRoomToUnread), (get, set, action) => { if (action.type === 'RESET') { @@ -127,7 +127,7 @@ export const roomToUnreadAtom = atom( export const useBindRoomToUnreadAtom = ( mx: MatrixClient, - unreadAtom: WritableAtom, + unreadAtom: typeof roomToUnreadAtom, muteChangesAtom: PrimitiveAtom ) => { const setUnreadAtom = useSetAtom(unreadAtom); diff --git a/src/app/state/sessions.ts b/src/app/state/sessions.ts new file mode 100644 index 0000000000..aa38340a5b --- /dev/null +++ b/src/app/state/sessions.ts @@ -0,0 +1,15 @@ +import { atom } from 'jotai'; + +export type Session = { + baseUrl: string; + userId: string; + deviceId: string; + accessToken: string; + expiresInMs?: number; + refreshToken?: string; + fallbackSdkStores?: boolean; +}; + +export type Sessions = Session[]; + +export const sessionsAtom = atom([]); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 92d40ff8c6..061931ea82 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -64,7 +64,7 @@ export const setSettings = (settings: Settings) => { }; const baseSettings = atom(getSettings()); -export const settingsAtom = atom( +export const settingsAtom = atom( (get) => get(baseSettings), (get, set, update) => { set(baseSettings, update); diff --git a/src/app/state/tabToRoom.ts b/src/app/state/tabToRoom.ts index 2f4ee92a49..b9472d9f91 100644 --- a/src/app/state/tabToRoom.ts +++ b/src/app/state/tabToRoom.ts @@ -14,7 +14,7 @@ type TabToRoomAction = { }; const baseTabToRoom = atom(new Map()); -export const tabToRoomAtom = atom( +export const tabToRoomAtom = atom( (get) => get(baseTabToRoom), (get, set, action) => { if (action.type === 'PUT') { diff --git a/src/app/state/typingMembers.ts b/src/app/state/typingMembers.ts index b87817d190..c77c91be6b 100644 --- a/src/app/state/typingMembers.ts +++ b/src/app/state/typingMembers.ts @@ -23,7 +23,11 @@ export type IRoomIdToTypingMembersAction = }; const baseRoomIdToTypingMembersAtom = atom(new Map()); -export const roomIdToTypingMembersAtom = atom( +export const roomIdToTypingMembersAtom = atom< + IRoomIdToTypingMembers, + [IRoomIdToTypingMembersAction], + undefined +>( (get) => get(baseRoomIdToTypingMembersAtom), (get, set, action) => { const roomIdToTypingMembers = get(baseRoomIdToTypingMembersAtom); diff --git a/src/app/state/upload.ts b/src/app/state/upload.ts index d92b93d3d1..13869afb25 100644 --- a/src/app/state/upload.ts +++ b/src/app/state/upload.ts @@ -57,7 +57,7 @@ export const createUploadAtom = (file: TUploadContent) => { file, status: UploadStatus.Idle, }); - return atom( + return atom( (get) => get(baseUploadAtom), (get, set, update) => { const uploadState = get(baseUploadAtom); diff --git a/src/app/state/utils.ts b/src/app/state/utils.ts index 355c941106..4c4caa5cae 100644 --- a/src/app/state/utils.ts +++ b/src/app/state/utils.ts @@ -15,7 +15,7 @@ export type RoomsAction = export const useBindRoomsWithMembershipsAtom = ( mx: MatrixClient, - roomsAtom: WritableAtom, + roomsAtom: WritableAtom, memberships: Membership[] ) => { const setRoomsAtom = useSetAtom(roomsAtom); From 1e449c6d0837c843589981b090c4290d57493f17 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:35:50 +0530 Subject: [PATCH 18/68] add atom with localStorage util --- src/app/state/utils/atomWithLocalStorage.ts | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/app/state/utils/atomWithLocalStorage.ts diff --git a/src/app/state/utils/atomWithLocalStorage.ts b/src/app/state/utils/atomWithLocalStorage.ts new file mode 100644 index 0000000000..f17d3a3d08 --- /dev/null +++ b/src/app/state/utils/atomWithLocalStorage.ts @@ -0,0 +1,51 @@ +import { atom } from 'jotai'; + +export const getLocalStorageItem = (key: string, defaultValue: T): T => { + const item = localStorage.getItem(key); + if (item === null) return defaultValue; + if (item === 'undefined') return undefined as T; + try { + return JSON.parse(item) as T; + } catch { + return defaultValue; + } +}; + +export const setLocalStorageItem = (key: string, value: T) => { + localStorage.setItem(key, JSON.stringify(value)); +}; + +export type GetLocalStorageItem = (key: string) => T; +export type SetLocalStorageItem = (key: string, value: T) => void; + +export const atomWithLocalStorage = ( + key: string, + getItem: GetLocalStorageItem, + setItem: SetLocalStorageItem +) => { + const value = getItem(key); + + const baseAtom = atom(value); + + baseAtom.onMount = (setAtom) => { + const handleChange = (evt: StorageEvent) => { + if (evt.key !== key) return; + setAtom(getItem(key)); + }; + + window.addEventListener('storage', handleChange); + return () => { + window.removeEventListener('storage', handleChange); + }; + }; + + const localStorageAtom = atom( + (get) => get(baseAtom), + (get, set, newValue) => { + set(baseAtom, newValue); + setItem(key, newValue); + } + ); + + return localStorageAtom; +}; From b049e58d147d0d9b43db0ab5b189fe3de7871ab5 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 24 Dec 2023 14:36:16 +0530 Subject: [PATCH 19/68] add multi account sessions atom --- src/app/pages/auth/Login.tsx | 2 + src/app/state/sessions.ts | 116 ++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 88133acf3c..2d3d0a64fe 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -144,6 +144,8 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog if (discoveryInfo) { mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; } + // TODO: add displayAlertDialog func + // which will create another react root with portal if (!mxIdBaseUrl) { alert( 'Failed to find MXID homeserver! Please enter server in Homeserver input for more details.' diff --git a/src/app/state/sessions.ts b/src/app/state/sessions.ts index aa38340a5b..85bcd10e2e 100644 --- a/src/app/state/sessions.ts +++ b/src/app/state/sessions.ts @@ -1,4 +1,9 @@ import { atom } from 'jotai'; +import { + atomWithLocalStorage, + getLocalStorageItem, + setLocalStorageItem, +} from './utils/atomWithLocalStorage'; export type Session = { baseUrl: string; @@ -11,5 +16,114 @@ export type Session = { }; export type Sessions = Session[]; +export type SessionStoreName = { + sync: string; + crypto: string; +}; + +/** + * Migration code for old session + */ +const FALLBACK_STORE_NAME: SessionStoreName = { + sync: 'web-sync-store', + crypto: 'crypto-store', +} as const; + +const removeFallbackSession = () => { + localStorage.removeItem('cinny_hs_base_url'); + localStorage.removeItem('cinny_user_id'); + localStorage.removeItem('cinny_device_id'); + localStorage.removeItem('cinny_access_token'); +}; +const getFallbackSession = (): Session | undefined => { + const baseUrl = localStorage.getItem('cinny_hs_base_url'); + const userId = localStorage.getItem('cinny_user_id'); + const deviceId = localStorage.getItem('cinny_device_id'); + const accessToken = localStorage.getItem('cinny_access_token'); + + if (baseUrl && userId && deviceId && accessToken) { + const session: Session = { + baseUrl, + userId, + deviceId, + accessToken, + fallbackSdkStores: true, + }; + + return session; + } + + return undefined; +}; +/** + * End of migration code for old session + */ + +export const getSessionStoreName = (session: Session): SessionStoreName => { + if (session.fallbackSdkStores) { + return FALLBACK_STORE_NAME; + } + + return { + sync: `sync${session.userId}`, + crypto: `crypto${session.userId}`, + }; +}; + +export const MATRIX_SESSIONS_KEY = 'matrixSessions'; +const baseSessionsAtom = atomWithLocalStorage( + MATRIX_SESSIONS_KEY, + (key) => { + const defaultSessions: Sessions = []; + const sessions = getLocalStorageItem(key, defaultSessions); + + // Before multi account support session was stored + // as multiple item in local storage. + // So we need these migration code. + const fallbackSession = getFallbackSession(); + if (fallbackSession) { + removeFallbackSession(); + sessions.push(fallbackSession); + setLocalStorageItem(key, sessions); + } + return sessions; + }, + (key, value) => { + setLocalStorageItem(key, value); + } +); + +export type SessionsAction = + | { + type: 'PUT'; + session: Session; + } + | { + type: 'DELETE'; + session: Session; + }; -export const sessionsAtom = atom([]); +export const sessionsAtom = atom( + (get) => get(baseSessionsAtom), + (get, set, action) => { + if (action.type === 'PUT') { + const sessions = [...get(baseSessionsAtom)]; + const sessionIndex = sessions.findIndex( + (session) => session.userId === action.session.userId + ); + if (sessionIndex === -1) { + sessions.push(action.session); + } else { + sessions.splice(sessionIndex, 1, action.session); + } + set(baseSessionsAtom, sessions); + return; + } + if (action.type === 'DELETE') { + const sessions = get(baseSessionsAtom).filter( + (session) => session.userId !== action.session.userId + ); + set(baseSessionsAtom, sessions); + } + } +); From 88c50cc871bf6dceca173c2cb6332b4435bb1960 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Dec 2023 10:52:41 +0530 Subject: [PATCH 20/68] add default IGNORE res to auto discovery --- src/app/cs-api.ts | 19 ++++++++--- src/app/pages/auth/AuthLayout.tsx | 56 ++++--------------------------- src/app/pages/auth/Login.tsx | 24 ++++--------- src/app/utils/common.ts | 7 ++++ 4 files changed, 35 insertions(+), 71 deletions(-) diff --git a/src/app/cs-api.ts b/src/app/cs-api.ts index f0157206b2..b9c677192b 100644 --- a/src/app/cs-api.ts +++ b/src/app/cs-api.ts @@ -1,4 +1,5 @@ import to from 'await-to-js'; +import { trimTrailingSlash } from './utils/common'; export enum AutoDiscoveryAction { PROMPT = 'PROMPT', @@ -25,18 +26,21 @@ export const autoDiscovery = async ( request: typeof fetch, server: string ): Promise<[AutoDiscoveryError, undefined] | [undefined, AutoDiscoveryInfo]> => { - const host = /^https?:\/\//.test(server) ? server : `https://${server}`; + const host = /^https?:\/\//.test(server) ? trimTrailingSlash(server) : `https://${server}`; const autoDiscoveryUrl = `${host}/.well-known/matrix/client`; const [err, response] = await to(request(autoDiscoveryUrl, { method: 'GET' })); if (err || response.status === 404) { + // AutoDiscoveryAction.IGNORE + // We will use default value for IGNORE action return [ + undefined, { - host, - action: AutoDiscoveryAction.IGNORE, + 'm.homeserver': { + base_url: host, + }, }, - undefined, ]; } if (response.status !== 200) { @@ -82,6 +86,13 @@ export const autoDiscovery = async ( ]; } + content['m.homeserver'].base_url = trimTrailingSlash(baseUrl); + if (content['m.identity_server']) { + content['m.identity_server'].base_url = trimTrailingSlash( + content['m.identity_server'].base_url + ); + } + return [undefined, content]; }; diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 2f24b98c0c..67a2166f21 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Box, Scroll, Spinner, Text, color } from 'folds'; import { LoaderFunction, @@ -21,12 +21,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { LOGIN_PATH, REGISTER_PATH } from '../paths'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; import { ServerPicker } from './ServerPicker'; -import { - AutoDiscoveryAction, - AutoDiscoveryError, - AutoDiscoveryInfo, - autoDiscovery, -} from '../../cs-api'; +import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api'; import { SpecVersionsLoader } from '../../components/SpecVersionsLoader'; import { SpecVersionsProvider } from '../../hooks/useSpecVersions'; import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo'; @@ -75,36 +70,6 @@ function AuthLayoutError({ message }: { message: string }) { ); } -const createDiscoveryInfo = ( - serverName: string, - autoDiscoveryError?: AutoDiscoveryError, - autoDiscoveryInfo?: AutoDiscoveryInfo -): - | undefined - | { - serverName: string; - info: AutoDiscoveryInfo; - } => { - if (autoDiscoveryInfo) { - return { - serverName, - info: autoDiscoveryInfo, - }; - } - if (autoDiscoveryError?.action === AutoDiscoveryAction.IGNORE) { - const tempAutoDiscoveryInfo = { - 'm.homeserver': { - base_url: autoDiscoveryError?.host, - }, - }; - return { - serverName, - info: tempAutoDiscoveryInfo, - }; - } - return undefined; -}; - export function AuthLayout() { const navigate = useNavigate(); const location = useLocation(); @@ -156,15 +121,6 @@ export function AuthLayout() { const [autoDiscoveryError, autoDiscoveryInfo] = discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : []; - const serverDiscovery = useMemo(() => { - if (discoveryState.status !== AsyncStatus.Success) return undefined; - return createDiscoveryInfo( - discoveryState.data.serverName, - autoDiscoveryError, - autoDiscoveryInfo - ); - }, [discoveryState, autoDiscoveryError, autoDiscoveryInfo]); - return ( )} - {serverDiscovery && ( - - + {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && ( + + ( )} error={() => ( diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 2d3d0a64fe..b6e9e2901c 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -15,7 +15,7 @@ import { } from 'folds'; import FocusTrap from 'focus-trap-react'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; -import { createClient } from 'matrix-js-sdk'; +import { LoginRequest, createClient } from 'matrix-js-sdk'; import to from 'await-to-js'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; @@ -26,7 +26,7 @@ import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; import { EMAIL_REGEX } from '../../utils/regex'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { AutoDiscoveryAction, autoDiscovery, specVersions } from '../../cs-api'; +import { autoDiscovery, specVersions } from '../../cs-api'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); @@ -104,18 +104,10 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const baseUrl = serverDiscovery['m.homeserver'].base_url; const [loginState, startLogin] = useAsyncCallback( - useCallback(async (serverBaseUrl: string, data: object) => { + useCallback(async (serverBaseUrl: string, data: Omit) => { const mx = createClient({ baseUrl: serverBaseUrl }); - // const [err, res] = await to<{ - // access_token: string, - // device_id: string, - // user_id: string, - // expires_in_ms?: number, - // refresh_token: string, - // }>(mx.login('m.login.password', data)); - // console.log(res?.status); - // console.log(err, res); - return undefined; + // const [err, res] = await to(mx.login('m.login.password', data)); + // return res; }, []) ); @@ -137,10 +129,8 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const [, discovery] = await to(autoDiscovery(fetch, mxIdServer)); let mxIdBaseUrl: string | undefined; - const [discoverError, discoveryInfo] = discovery ?? []; - if (discoverError?.action === AutoDiscoveryAction.IGNORE) { - mxIdBaseUrl = discoverError.host; - } + const [, discoveryInfo] = discovery ?? []; + if (discoveryInfo) { mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; } diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index 0ce07dff7b..9bb597d60e 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -88,3 +88,10 @@ export const parseGeoUri = (location: string) => { longitude, }; }; + +export const trimTrailingSlash = (str: string) => { + if (str.endsWith('/')) { + return str.slice(0, str.length - 1); + } + return str; +}; From f397b4b1112c3147b79587d892144fdfc2239c3d Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Dec 2023 12:30:53 +0530 Subject: [PATCH 21/68] add error type in async callback hook --- src/app/hooks/useAsyncCallback.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/hooks/useAsyncCallback.ts b/src/app/hooks/useAsyncCallback.ts index 18b63ecc6a..936bd1e90e 100644 --- a/src/app/hooks/useAsyncCallback.ts +++ b/src/app/hooks/useAsyncCallback.ts @@ -16,24 +16,24 @@ export type AsyncLoading = { status: AsyncStatus.Loading; }; -export type AsyncSuccess = { +export type AsyncSuccess = { status: AsyncStatus.Success; - data: T; + data: D; }; -export type AsyncError = { +export type AsyncError = { status: AsyncStatus.Error; - error: unknown; + error: E; }; -export type AsyncState = AsyncIdle | AsyncLoading | AsyncSuccess | AsyncError; +export type AsyncState = AsyncIdle | AsyncLoading | AsyncSuccess | AsyncError; export type AsyncCallback = (...args: TArgs) => Promise; -export const useAsyncCallback = ( +export const useAsyncCallback = ( asyncCallback: AsyncCallback -): [AsyncState, AsyncCallback] => { - const [state, setState] = useState>({ +): [AsyncState, AsyncCallback] => { + const [state, setState] = useState>({ status: AsyncStatus.Idle, }); const alive = useAlive(); @@ -57,7 +57,7 @@ export const useAsyncCallback = ( if (alive()) { setState({ status: AsyncStatus.Error, - error: e, + error: e as TError, }); } throw e; From 041fa52b5433d8d1cfed445074c91c559a11daad Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Dec 2023 12:39:54 +0530 Subject: [PATCH 22/68] handle password login error --- src/app/pages/auth/Login.tsx | 271 +---------------- src/app/pages/auth/PasswordLoginForm.tsx | 364 +++++++++++++++++++++++ 2 files changed, 371 insertions(+), 264 deletions(-) create mode 100644 src/app/pages/auth/PasswordLoginForm.tsx diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index b6e9e2901c..c65bfbb7f4 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -1,276 +1,19 @@ -import React, { FormEventHandler, useCallback, useState } from 'react'; -import { - Box, - Button, - Header, - Icon, - IconButton, - Icons, - Input, - Menu, - PopOut, - Text, - color, - config, -} from 'folds'; -import FocusTrap from 'focus-trap-react'; +import React from 'react'; +import { Box, Text, color } from 'folds'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; -import { LoginRequest, createClient } from 'matrix-js-sdk'; -import to from 'await-to-js'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; import { useAuthServer } from '../../hooks/useAuthServer'; -import { UseStateProvider } from '../../components/UseStateProvider'; import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; -import { EMAIL_REGEX } from '../../utils/regex'; -import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; -import { useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { autoDiscovery, specVersions } from '../../cs-api'; +import { PasswordLoginForm } from './PasswordLoginForm'; -function UsernameHint({ server }: { server: string }) { - const [open, setOpen] = useState(false); - return ( - setOpen(false), - clickOutsideDeactivates: true, - }} - > - -
- Hint -
- - - - Username: - {' '} - johndoe - - - - Matrix ID: - - {` @johndoe:${server}`} - - - - Email: - - {` johndoe@${server}`} - - -
- - } - > - {(targetRef) => ( - setOpen(true)} - ref={targetRef} - type="button" - variant="Background" - size="400" - radii="300" - > - - - )} -
- ); -} - -type PasswordLoginFormProps = { - defaultUsername?: string; - defaultEmail?: string; -}; -export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { - const server = useAuthServer(); - - const serverDiscovery = useAutoDiscoveryInfo(); - const baseUrl = serverDiscovery['m.homeserver'].base_url; - - const [loginState, startLogin] = useAsyncCallback( - useCallback(async (serverBaseUrl: string, data: Omit) => { - const mx = createClient({ baseUrl: serverBaseUrl }); - // const [err, res] = await to(mx.login('m.login.password', data)); - // return res; - }, []) - ); - - const handleUsernameLogin = (username: string, password: string) => { - startLogin(baseUrl, { - identifier: { - type: 'm.id.user', - user: username, - }, - password, - initial_device_display_name: 'Cinny Web', - }); - }; - const handleMxIdLogin = async (mxId: string, password: string) => { - const mxIdServer = getMxIdServer(mxId); - const mxIdUsername = getMxIdLocalPart(mxId); - if (!mxIdServer || !mxIdUsername) return; - - const [, discovery] = await to(autoDiscovery(fetch, mxIdServer)); - - let mxIdBaseUrl: string | undefined; - const [, discoveryInfo] = discovery ?? []; - - if (discoveryInfo) { - mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; - } - // TODO: add displayAlertDialog func - // which will create another react root with portal - if (!mxIdBaseUrl) { - alert( - 'Failed to find MXID homeserver! Please enter server in Homeserver input for more details.' - ); - return; - } - const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); - if (!versions) { - alert('Homeserver URL does not appear to be a valid Matrix homeserver.'); - return; - } - - startLogin(mxIdBaseUrl, { - identifier: { - type: 'm.id.user', - user: mxIdUsername, - }, - password, - initial_device_display_name: 'Cinny Web', - }); - }; - const handleEmailLogin = (email: string, password: string) => { - startLogin(baseUrl, { - identifier: { - type: 'm.id.thirdparty', - medium: 'email', - address: email, - }, - password, - initial_device_display_name: 'Cinny Web', - }); - }; - - const handleSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - const { usernameInput, passwordInput } = evt.target as HTMLFormElement & { - usernameInput: HTMLInputElement; - passwordInput: HTMLInputElement; - }; - - const username = usernameInput.value.trim(); - const password = passwordInput.value; - if (!username) { - usernameInput.focus(); - return; - } - if (!password) { - passwordInput.focus(); - return; - } - - if (isUserId(username)) { - handleMxIdLogin(username, password); - return; - } - if (EMAIL_REGEX.test(username)) { - handleEmailLogin(username, password); - return; - } - handleUsernameLogin(username, password); - }; - - return ( - - - - Username - - } - /> - - - - Password - - - {(visible, setVisible) => ( - setVisible(!visible)} - type="button" - variant={visible ? 'Warning' : 'Background'} - size="400" - radii="300" - > - -
- } - /> - )} - - - - {/* TODO: make reset password path */} - Forget Password? - - -
- - - - ); -} - -const getLoginSearchParams = ( - searchParams: URLSearchParams -): { +export type LoginSearchParams = { username?: string; email?: string; loginToken?: string; -} => ({ +}; + +const getLoginSearchParams = (searchParams: URLSearchParams): LoginSearchParams => ({ username: searchParams.get('username') ?? undefined, email: searchParams.get('email') ?? undefined, loginToken: searchParams.get('loginToken') ?? undefined, diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/PasswordLoginForm.tsx new file mode 100644 index 0000000000..3258639020 --- /dev/null +++ b/src/app/pages/auth/PasswordLoginForm.tsx @@ -0,0 +1,364 @@ +import React, { FormEventHandler, useCallback, useEffect, useState } from 'react'; +import { + Box, + Button, + Header, + Icon, + IconButton, + Icons, + Input, + Menu, + PopOut, + Text, + color, + config, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; +import to from 'await-to-js'; +import { Link, generatePath } from 'react-router-dom'; +import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk'; +import { UseStateProvider } from '../../components/UseStateProvider'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { EMAIL_REGEX } from '../../utils/regex'; +import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { autoDiscovery, specVersions } from '../../cs-api'; +import { REGISTER_PATH } from '../paths'; +import { useAuthServer } from '../../hooks/useAuthServer'; + +function UsernameHint({ server }: { server: string }) { + const [open, setOpen] = useState(false); + return ( + setOpen(false), + clickOutsideDeactivates: true, + }} + > + +
+ Hint +
+ + + + Username: + {' '} + johndoe + + + + Matrix ID: + + {` @johndoe:${server}`} + + + + Email: + + {` johndoe@${server}`} + + +
+ + } + > + {(targetRef) => ( + setOpen(true)} + ref={targetRef} + type="button" + variant="Background" + size="400" + radii="300" + > + + + )} +
+ ); +} + +function LoginFieldError({ message }: { message: string }) { + return ( + + + + {message} + + + ); +} + +const factoryFetchBaseUrl = (server: string) => { + const fetchBaseUrl = async (): Promise => { + const [, discovery] = await to(autoDiscovery(fetch, server)); + + let mxIdBaseUrl: string | undefined; + const [, discoveryInfo] = discovery ?? []; + + if (discoveryInfo) { + mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; + } + + if (!mxIdBaseUrl) { + throw new Error( + 'Failed to find MXID homeserver! Please enter server in Homeserver input for more details.' + ); + } + const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); + if (!versions) { + throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver.'); + } + return mxIdBaseUrl; + }; + return fetchBaseUrl; +}; + +enum LoginError { + InvalidServer = 'InvalidServer', + Forbidden = 'Forbidden', + UserDeactivated = 'UserDeactivated', + InvalidRequest = 'InvalidRequest', + RateLimited = 'RateLimited', + Unknown = 'Unknown', +} + +const passwordLogin = async ( + serverBaseUrl: string | (() => Promise), + data: Omit +) => { + const [urlError, url] = + typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; + if (urlError) { + throw new MatrixError({ + errcode: LoginError.InvalidServer, + }); + } + + const mx = createClient({ baseUrl: url }); + const [err, res] = await to(mx.login('m.login.password', data)); + + if (err) { + if (err.httpStatus === 400) { + throw new MatrixError({ + errcode: LoginError.InvalidRequest, + }); + } + if (err.httpStatus === 429) { + throw new MatrixError({ + errcode: LoginError.RateLimited, + }); + } + if (err.errcode === 'M_USER_DEACTIVATED') { + throw new MatrixError({ + errcode: LoginError.UserDeactivated, + }); + } + + if (err.httpStatus === 403) { + throw new MatrixError({ + errcode: LoginError.Forbidden, + }); + } + + throw new MatrixError({ + errcode: LoginError.Unknown, + }); + } + return res; +}; + +type PasswordLoginFormProps = { + defaultUsername?: string; + defaultEmail?: string; +}; +export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { + const server = useAuthServer(); + + const serverDiscovery = useAutoDiscoveryInfo(); + const baseUrl = serverDiscovery['m.homeserver'].base_url; + + const [loginState, startLogin] = useAsyncCallback< + LoginResponse, + MatrixError, + Parameters + >(useCallback(passwordLogin, [])); + + useEffect(() => { + if (loginState.status === AsyncStatus.Success) { + // TODO: save response + // redirect to home + } + }, [loginState]); + + const handleUsernameLogin = (username: string, password: string) => { + startLogin(baseUrl, { + identifier: { + type: 'm.id.user', + user: username, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + + const handleMxIdLogin = async (mxId: string, password: string) => { + const mxIdServer = getMxIdServer(mxId); + const mxIdUsername = getMxIdLocalPart(mxId); + if (!mxIdServer || !mxIdUsername) return; + + const fetchBaseUrl = factoryFetchBaseUrl(mxIdServer); + + startLogin(fetchBaseUrl, { + identifier: { + type: 'm.id.user', + user: mxIdUsername, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + const handleEmailLogin = (email: string, password: string) => { + startLogin(baseUrl, { + identifier: { + type: 'm.id.thirdparty', + medium: 'email', + address: email, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { usernameInput, passwordInput } = evt.target as HTMLFormElement & { + usernameInput: HTMLInputElement; + passwordInput: HTMLInputElement; + }; + + const username = usernameInput.value.trim(); + const password = passwordInput.value; + if (!username) { + usernameInput.focus(); + return; + } + if (!password) { + passwordInput.focus(); + return; + } + + if (isUserId(username)) { + handleMxIdLogin(username, password); + return; + } + if (EMAIL_REGEX.test(username)) { + handleEmailLogin(username, password); + return; + } + handleUsernameLogin(username, password); + }; + + return ( + + + + Username + + } + /> + {loginState.status === AsyncStatus.Error && + loginState.error.errcode === LoginError.InvalidServer && ( + + )} + + + + Password + + + {(visible, setVisible) => ( + setVisible(!visible)} + type="button" + variant={visible ? 'Warning' : 'Background'} + size="400" + radii="300" + > + + + } + /> + )} + + + {loginState.status === AsyncStatus.Error && ( + <> + {loginState.error.errcode === LoginError.Forbidden && ( + + )} + {loginState.error.errcode === LoginError.UserDeactivated && ( + + )} + {loginState.error.errcode === LoginError.InvalidRequest && ( + + )} + {loginState.error.errcode === LoginError.RateLimited && ( + + )} + {loginState.error.errcode === LoginError.Unknown && ( + + )} + + )} + + + {/* TODO: make reset password path */} + Forget Password? + + + + + + + + ); +} From fbb690cb61e1b5792555b07206f0b31aa88e526b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Dec 2023 15:40:41 +0530 Subject: [PATCH 23/68] fix async callback hook --- src/app/hooks/useAsyncCallback.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/hooks/useAsyncCallback.ts b/src/app/hooks/useAsyncCallback.ts index 936bd1e90e..f70f322895 100644 --- a/src/app/hooks/useAsyncCallback.ts +++ b/src/app/hooks/useAsyncCallback.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useAlive } from './useAlive'; export enum AsyncStatus { @@ -38,14 +38,25 @@ export const useAsyncCallback = ( }); const alive = useAlive(); + // Tracks the request number. + // If two or more requests are made subsequently + // we will throw all old request's response after they resolved. + const reqNumberRef = useRef(0); + const callback: AsyncCallback = useCallback( async (...args) => { setState({ status: AsyncStatus.Loading, }); + reqNumberRef.current += 1; + + const currentReqNumber = reqNumberRef.current; try { const data = await asyncCallback(...args); + if (currentReqNumber !== reqNumberRef.current) { + throw new Error('AsyncCallbackHook: Request replaced!'); + } if (alive()) { setState({ status: AsyncStatus.Success, @@ -54,6 +65,9 @@ export const useAsyncCallback = ( } return data; } catch (e) { + if (currentReqNumber !== reqNumberRef.current) { + throw new Error('AsyncCallbackHook: Request replaced!'); + } if (alive()) { setState({ status: AsyncStatus.Error, From 70797a82c92855ae15a0eccb309e7354e0279dd2 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:30:27 +0530 Subject: [PATCH 24/68] allow password login --- src/app/pages/App.tsx | 10 ++++-- src/app/pages/auth/AuthLayout.tsx | 4 +-- src/app/pages/auth/PasswordLoginForm.tsx | 43 +++++++++++++++++++----- src/app/pages/paths.ts | 2 ++ src/client/action/auth.js | 2 +- src/client/initMatrix.js | 3 +- src/client/state/auth.ts | 6 ++-- 7 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 691fc07a03..2db917dd3d 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -20,7 +20,7 @@ const createRouter = (clientConfig: ClientConfig) => { const { basename } = clientConfig; const router = createBrowserRouter( createRoutesFromElements( - }> + { @@ -28,13 +28,17 @@ const createRouter = (clientConfig: ClientConfig) => { return redirect('/login'); }} /> - }> } /> } /> - + { + if (!isAuthenticated()) return redirect('/login'); + return null; + }} + > } /> direct

} /> :spaceIdOrAlias

} /> diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 67a2166f21..b5376cef64 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -30,9 +30,7 @@ import { AuthFlowsProvider } from '../../hooks/useAuthFlows'; import { AuthServerProvider } from '../../hooks/useAuthServer'; export const authLayoutLoader: LoaderFunction = () => { - // TODO: remove false case - const isAuth = false && isAuthenticated(); - if (isAuth) { + if (isAuthenticated()) { return redirect('/'); } diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/PasswordLoginForm.tsx index 3258639020..ca984cf5c3 100644 --- a/src/app/pages/auth/PasswordLoginForm.tsx +++ b/src/app/pages/auth/PasswordLoginForm.tsx @@ -8,14 +8,18 @@ import { Icons, Input, Menu, + Overlay, + OverlayBackdrop, + OverlayCenter, PopOut, + Spinner, Text, color, config, } from 'folds'; import FocusTrap from 'focus-trap-react'; import to from 'await-to-js'; -import { Link, generatePath } from 'react-router-dom'; +import { Link, generatePath, useNavigate } from 'react-router-dom'; import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk'; import { UseStateProvider } from '../../components/UseStateProvider'; import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; @@ -23,8 +27,9 @@ import { EMAIL_REGEX } from '../../utils/regex'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { autoDiscovery, specVersions } from '../../cs-api'; -import { REGISTER_PATH } from '../paths'; +import { REGISTER_PATH, ROOT_PATH } from '../paths'; import { useAuthServer } from '../../hooks/useAuthServer'; +import { updateLocalStore } from '../../../client/action/auth'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); @@ -136,10 +141,14 @@ enum LoginError { Unknown = 'Unknown', } +type PasswordLoginResponse = { + baseUrl: string; + response: LoginResponse; +}; const passwordLogin = async ( serverBaseUrl: string | (() => Promise), data: Omit -) => { +): Promise => { const [urlError, url] = typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; if (urlError) { @@ -178,7 +187,10 @@ const passwordLogin = async ( errcode: LoginError.Unknown, }); } - return res; + return { + baseUrl: url, + response: res, + }; }; type PasswordLoginFormProps = { @@ -186,23 +198,25 @@ type PasswordLoginFormProps = { defaultEmail?: string; }; export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { + const navigate = useNavigate(); const server = useAuthServer(); const serverDiscovery = useAutoDiscoveryInfo(); const baseUrl = serverDiscovery['m.homeserver'].base_url; const [loginState, startLogin] = useAsyncCallback< - LoginResponse, + PasswordLoginResponse, MatrixError, Parameters >(useCallback(passwordLogin, [])); useEffect(() => { if (loginState.status === AsyncStatus.Success) { - // TODO: save response - // redirect to home + const { response: loginRes, baseUrl: loginBaseUrl } = loginState.data; + updateLocalStore(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl); + navigate(ROOT_PATH, { replace: true }); } - }, [loginState]); + }, [loginState, navigate]); const handleUsernameLogin = (username: string, password: string) => { startLogin(baseUrl, { @@ -329,7 +343,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog {loginState.status === AsyncStatus.Error && ( <> {loginState.error.errcode === LoginError.Forbidden && ( - + )} {loginState.error.errcode === LoginError.UserDeactivated && ( @@ -359,6 +373,17 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog Login + + } + > + + + + ); } diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 90fcae8afe..928738eee9 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -1,2 +1,4 @@ +export const ROOT_PATH = '/'; + export const LOGIN_PATH = '/login/:server?/'; export const REGISTER_PATH = '/register/:server?/'; diff --git a/src/client/action/auth.js b/src/client/action/auth.js index f9be13bc0e..f04306b8fe 100644 --- a/src/client/action/auth.js +++ b/src/client/action/auth.js @@ -98,7 +98,7 @@ async function completeRegisterStage( } export { - createTemporaryClient, login, verifyEmail, + updateLocalStore, createTemporaryClient, login, verifyEmail, loginWithToken, startSsoLogin, completeRegisterStage, }; diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js index 9b8d1d82ee..211cf11425 100644 --- a/src/client/initMatrix.js +++ b/src/client/initMatrix.js @@ -3,7 +3,7 @@ import * as sdk from 'matrix-js-sdk'; import Olm from '@matrix-org/olm'; // import { logger } from 'matrix-js-sdk/lib/logger'; -import { secret } from './state/auth'; +import { getSecret } from './state/auth'; import RoomList from './state/RoomList'; import AccountData from './state/AccountData'; import RoomsInput from './state/RoomsInput'; @@ -40,6 +40,7 @@ class InitMatrix extends EventEmitter { dbName: 'web-sync-store', }); await indexedDBStore.startup(); + const secret = getSecret(); this.matrixClient = sdk.createClient({ baseUrl: secret.baseUrl, diff --git a/src/client/state/auth.ts b/src/client/state/auth.ts index f9e1c29786..9536a927ec 100644 --- a/src/client/state/auth.ts +++ b/src/client/state/auth.ts @@ -2,11 +2,11 @@ import cons from './cons'; const isAuthenticated = () => localStorage.getItem(cons.secretKey.ACCESS_TOKEN) !== null; -const secret = { +const getSecret = () => ({ accessToken: localStorage.getItem(cons.secretKey.ACCESS_TOKEN), deviceId: localStorage.getItem(cons.secretKey.DEVICE_ID), userId: localStorage.getItem(cons.secretKey.USER_ID), baseUrl: localStorage.getItem(cons.secretKey.BASE_URL), -}; +}); -export { isAuthenticated, secret }; +export { isAuthenticated, getSecret }; From f339cab1f293ce65efc5078a18a7eb80f319e76b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 26 Dec 2023 09:50:57 +0530 Subject: [PATCH 25/68] Show custom server not allowed error in mxId login --- src/app/hooks/useClientConfig.ts | 11 ++++++ src/app/pages/App.tsx | 1 - src/app/pages/auth/AuthLayout.tsx | 17 +++++---- src/app/pages/auth/PasswordLoginForm.tsx | 46 ++++++++++++++++-------- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index 854aecd100..ebaca07dd4 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -17,3 +17,14 @@ export function useClientConfig(): ClientConfig { if (!config) throw new Error('Client config are not provided!'); return config; } + +export const clientDefaultServer = (clientConfig: ClientConfig): string => + clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'matrix.org'; + +export const clientAllowedServer = (clientConfig: ClientConfig, server: string): boolean => { + const { homeserverList, allowCustomHomeservers } = clientConfig; + + if (allowCustomHomeservers) return true; + + return homeserverList?.includes(server) === true; +}; diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 2db917dd3d..6df824449c 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Provider as JotaiProvider } from 'jotai'; import { - Outlet, Route, RouterProvider, createBrowserRouter, diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index b5376cef64..ecc0fbee22 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -16,7 +16,11 @@ import { AuthFooter } from './AuthFooter'; import * as css from './styles.css'; import * as PatternsCss from '../../styles/Patterns.css'; import { isAuthenticated } from '../../../client/state/auth'; -import { useClientConfig } from '../../hooks/useClientConfig'; +import { + clientAllowedServer, + clientDefaultServer, + useClientConfig, +} from '../../hooks/useClientConfig'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { LOGIN_PATH, REGISTER_PATH } from '../paths'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; @@ -73,11 +77,12 @@ export function AuthLayout() { const location = useLocation(); const { server: urlEncodedServer } = useParams(); - const { homeserverList, defaultHomeserver, allowCustomHomeservers } = useClientConfig(); + const clientConfig = useClientConfig(); - const defaultServer: string = homeserverList?.[defaultHomeserver ?? 0] ?? 'matrix.org'; + const defaultServer = clientDefaultServer(clientConfig); let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; - if (!allowCustomHomeservers && !homeserverList?.includes(server)) { + + if (!clientAllowedServer(clientConfig, server)) { server = defaultServer; } @@ -148,8 +153,8 @@ export function AuthLayout() { diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/PasswordLoginForm.tsx index ca984cf5c3..d57707a3c6 100644 --- a/src/app/pages/auth/PasswordLoginForm.tsx +++ b/src/app/pages/auth/PasswordLoginForm.tsx @@ -30,6 +30,7 @@ import { autoDiscovery, specVersions } from '../../cs-api'; import { REGISTER_PATH, ROOT_PATH } from '../paths'; import { useAuthServer } from '../../hooks/useAuthServer'; import { updateLocalStore } from '../../../client/action/auth'; +import { ClientConfig, clientAllowedServer, useClientConfig } from '../../hooks/useClientConfig'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); @@ -107,8 +108,16 @@ function LoginFieldError({ message }: { message: string }) { ); } -const factoryFetchBaseUrl = (server: string) => { - const fetchBaseUrl = async (): Promise => { +enum GetBaseUrlError { + NotAllow = 'NotAllow', + NotFound = 'NotFound', +} +const factoryGetBaseUrl = (clientConfig: ClientConfig, server: string) => { + const getBaseUrl = async (): Promise => { + if (!clientAllowedServer(clientConfig, server)) { + throw new Error(GetBaseUrlError.NotAllow); + } + const [, discovery] = await to(autoDiscovery(fetch, server)); let mxIdBaseUrl: string | undefined; @@ -119,20 +128,19 @@ const factoryFetchBaseUrl = (server: string) => { } if (!mxIdBaseUrl) { - throw new Error( - 'Failed to find MXID homeserver! Please enter server in Homeserver input for more details.' - ); + throw new Error(GetBaseUrlError.NotFound); } const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); if (!versions) { - throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver.'); + throw new Error(GetBaseUrlError.NotFound); } return mxIdBaseUrl; }; - return fetchBaseUrl; + return getBaseUrl; }; enum LoginError { + ServerNotAllowed = 'ServerNotAllowed', InvalidServer = 'InvalidServer', Forbidden = 'Forbidden', UserDeactivated = 'UserDeactivated', @@ -153,7 +161,10 @@ const passwordLogin = async ( typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; if (urlError) { throw new MatrixError({ - errcode: LoginError.InvalidServer, + errcode: + urlError.message === GetBaseUrlError.NotAllow + ? LoginError.ServerNotAllowed + : LoginError.InvalidServer, }); } @@ -200,6 +211,7 @@ type PasswordLoginFormProps = { export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { const navigate = useNavigate(); const server = useAuthServer(); + const clientConfig = useClientConfig(); const serverDiscovery = useAutoDiscoveryInfo(); const baseUrl = serverDiscovery['m.homeserver'].base_url; @@ -234,9 +246,9 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const mxIdUsername = getMxIdLocalPart(mxId); if (!mxIdServer || !mxIdUsername) return; - const fetchBaseUrl = factoryFetchBaseUrl(mxIdServer); + const getBaseUrl = factoryGetBaseUrl(clientConfig, mxIdServer); - startLogin(fetchBaseUrl, { + startLogin(getBaseUrl, { identifier: { type: 'm.id.user', user: mxIdUsername, @@ -302,10 +314,16 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog outlined after={} /> - {loginState.status === AsyncStatus.Error && - loginState.error.errcode === LoginError.InvalidServer && ( - - )} + {loginState.status === AsyncStatus.Error && ( + <> + {loginState.error.errcode === LoginError.ServerNotAllowed && ( + + )} + {loginState.error.errcode === LoginError.InvalidServer && ( + + )} + + )} From 6bb9365c210e3aba3e420bbdf82b25e9e354a2ce Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 26 Dec 2023 11:18:11 +0530 Subject: [PATCH 26/68] add sso login component --- src/app/pages/auth/Login.tsx | 15 ++++++- src/app/pages/auth/SSOLogin.tsx | 72 ++++++++++++++++++++++++++++++++ src/app/pages/auth/styles.css.ts | 2 +- 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/app/pages/auth/SSOLogin.tsx diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index c65bfbb7f4..d0149f1c1a 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Box, Text, color } from 'folds'; +import { Box, Line, Text, color } from 'folds'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; import { useAuthServer } from '../../hooks/useAuthServer'; import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; import { PasswordLoginForm } from './PasswordLoginForm'; +import { SSOLogin } from './SSOLogin'; export type LoginSearchParams = { username?: string; @@ -42,11 +43,21 @@ export function Login() { defaultEmail={loginSearchParams.email} /> + {parsedFlows.sso && ( + + + Or + + + )} )} {parsedFlows.sso && ( <> - SSO login supported + )} diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx new file mode 100644 index 0000000000..76181b9cc3 --- /dev/null +++ b/src/app/pages/auth/SSOLogin.tsx @@ -0,0 +1,72 @@ +import { Avatar, AvatarImage, Box, Button, Text } from 'folds'; +import { IIdentityProvider, createClient } from 'matrix-js-sdk'; +import React, { useMemo } from 'react'; +import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; + +type SSOLoginProps = { + providers: IIdentityProvider[]; + canPasswordLogin?: boolean; +}; +export function SSOLogin({ providers, canPasswordLogin }: SSOLoginProps) { + const discovery = useAutoDiscoveryInfo(); + const baseUrl = discovery['m.homeserver'].base_url; + const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); + + const getSSOIdUrl = (ssoId: string): string => { + const redirectUrl = window.location.href; + return mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); + }; + + return ( + + {providers.map((provider) => { + const { id, name, icon } = provider; + const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false); + + const buttonTitle = `Login with ${name}`; + + // Only show SSO buttons as icons if we have + // password login UI and high number of SSO buttons + if (iconUrl && canPasswordLogin && providers.length > 2) { + return ( + + + + ); + } + + return ( + + ); + })} + + ); +} diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts index 30064d83d1..154008ec61 100644 --- a/src/app/pages/auth/styles.css.ts +++ b/src/app/pages/auth/styles.css.ts @@ -12,7 +12,7 @@ export const AuthLayout = style({ }); export const AuthCard = style({ - marginTop: '10vh', + marginTop: '6vh', maxWidth: config.size.ModalWidth300, width: '100%', backgroundColor: color.Surface.Container, From 920da5b7ce210c7faedf72615dd68478a6b64d9e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 26 Dec 2023 12:01:38 +0530 Subject: [PATCH 27/68] add token login --- src/app/pages/auth/Login.tsx | 9 +- src/app/pages/auth/PasswordLoginForm.tsx | 131 +++-------------------- src/app/pages/auth/ServerPicker.tsx | 2 + src/app/pages/auth/TokenLogin.tsx | 94 ++++++++++++++++ src/app/pages/auth/loginUtil.ts | 117 ++++++++++++++++++++ 5 files changed, 234 insertions(+), 119 deletions(-) create mode 100644 src/app/pages/auth/TokenLogin.tsx create mode 100644 src/app/pages/auth/loginUtil.ts diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index d0149f1c1a..d6b83c24a9 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -7,6 +7,7 @@ import { useAuthServer } from '../../hooks/useAuthServer'; import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; import { PasswordLoginForm } from './PasswordLoginForm'; import { SSOLogin } from './SSOLogin'; +import { TokenLogin } from './TokenLogin'; export type LoginSearchParams = { username?: string; @@ -22,20 +23,20 @@ const getLoginSearchParams = (searchParams: URLSearchParams): LoginSearchParams export function Login() { const server = useAuthServer(); - const { loginFlows, registerFlows } = useAuthFlows(); + const { loginFlows } = useAuthFlows(); const [searchParams] = useSearchParams(); const loginSearchParams = getLoginSearchParams(searchParams); const parsedFlows = useParsedLoginFlows(loginFlows.flows); - console.log(parsedFlows); - console.log(server, loginFlows, registerFlows); return ( Login - {parsedFlows.token && false &&

Token login

} + {parsedFlows.token && loginSearchParams.loginToken && ( + + )} {parsedFlows.password && ( <> { - const getBaseUrl = async (): Promise => { - if (!clientAllowedServer(clientConfig, server)) { - throw new Error(GetBaseUrlError.NotAllow); - } - - const [, discovery] = await to(autoDiscovery(fetch, server)); - - let mxIdBaseUrl: string | undefined; - const [, discoveryInfo] = discovery ?? []; - - if (discoveryInfo) { - mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; - } - - if (!mxIdBaseUrl) { - throw new Error(GetBaseUrlError.NotFound); - } - const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); - if (!versions) { - throw new Error(GetBaseUrlError.NotFound); - } - return mxIdBaseUrl; - }; - return getBaseUrl; -}; - -enum LoginError { - ServerNotAllowed = 'ServerNotAllowed', - InvalidServer = 'InvalidServer', - Forbidden = 'Forbidden', - UserDeactivated = 'UserDeactivated', - InvalidRequest = 'InvalidRequest', - RateLimited = 'RateLimited', - Unknown = 'Unknown', -} - -type PasswordLoginResponse = { - baseUrl: string; - response: LoginResponse; -}; -const passwordLogin = async ( - serverBaseUrl: string | (() => Promise), - data: Omit -): Promise => { - const [urlError, url] = - typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; - if (urlError) { - throw new MatrixError({ - errcode: - urlError.message === GetBaseUrlError.NotAllow - ? LoginError.ServerNotAllowed - : LoginError.InvalidServer, - }); - } - - const mx = createClient({ baseUrl: url }); - const [err, res] = await to(mx.login('m.login.password', data)); - - if (err) { - if (err.httpStatus === 400) { - throw new MatrixError({ - errcode: LoginError.InvalidRequest, - }); - } - if (err.httpStatus === 429) { - throw new MatrixError({ - errcode: LoginError.RateLimited, - }); - } - if (err.errcode === 'M_USER_DEACTIVATED') { - throw new MatrixError({ - errcode: LoginError.UserDeactivated, - }); - } - - if (err.httpStatus === 403) { - throw new MatrixError({ - errcode: LoginError.Forbidden, - }); - } - - throw new MatrixError({ - errcode: LoginError.Unknown, - }); - } - return { - baseUrl: url, - response: res, - }; -}; - type PasswordLoginFormProps = { defaultUsername?: string; defaultEmail?: string; }; export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { - const navigate = useNavigate(); const server = useAuthServer(); const clientConfig = useClientConfig(); @@ -217,18 +124,12 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const baseUrl = serverDiscovery['m.homeserver'].base_url; const [loginState, startLogin] = useAsyncCallback< - PasswordLoginResponse, + CustomLoginResponse, MatrixError, - Parameters - >(useCallback(passwordLogin, [])); + Parameters + >(useCallback(login, [])); - useEffect(() => { - if (loginState.status === AsyncStatus.Success) { - const { response: loginRes, baseUrl: loginBaseUrl } = loginState.data; - updateLocalStore(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl); - navigate(ROOT_PATH, { replace: true }); - } - }, [loginState, navigate]); + useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined); const handleUsernameLogin = (username: string, password: string) => { startLogin(baseUrl, { diff --git a/src/app/pages/auth/ServerPicker.tsx b/src/app/pages/auth/ServerPicker.tsx index cc9ccec5e4..e25f94c1ce 100644 --- a/src/app/pages/auth/ServerPicker.tsx +++ b/src/app/pages/auth/ServerPicker.tsx @@ -66,6 +66,8 @@ export function ServerPicker({ setServerMenu(false); }; + // TODO: input not update on url changes + return ( + + + Token Login + + {message} + + +
+ ); +} + +type TokenLoginProps = { + token: string; +}; +export function TokenLogin({ token }: TokenLoginProps) { + const discovery = useAutoDiscoveryInfo(); + const baseUrl = discovery['m.homeserver'].base_url; + + const [loginState, startLogin] = useAsyncCallback< + CustomLoginResponse, + MatrixError, + Parameters + >(useCallback(login, [])); + + useEffect(() => { + startLogin(baseUrl, { + type: 'm.login.token', + token, + initial_device_display_name: 'Cinny Web', + }); + }, [baseUrl, token, startLogin]); + + useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined); + + return ( + <> + {loginState.status === AsyncStatus.Error && ( + <> + {loginState.error.errcode === LoginError.Forbidden && ( + + )} + {loginState.error.errcode === LoginError.UserDeactivated && ( + + )} + {loginState.error.errcode === LoginError.InvalidRequest && ( + + )} + {loginState.error.errcode === LoginError.RateLimited && ( + + )} + {loginState.error.errcode === LoginError.Unknown && ( + + )} + + )} + }> + + + + + + ); +} diff --git a/src/app/pages/auth/loginUtil.ts b/src/app/pages/auth/loginUtil.ts new file mode 100644 index 0000000000..3a6ef1c415 --- /dev/null +++ b/src/app/pages/auth/loginUtil.ts @@ -0,0 +1,117 @@ +import to from 'await-to-js'; +import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ClientConfig, clientAllowedServer } from '../../hooks/useClientConfig'; +import { autoDiscovery, specVersions } from '../../cs-api'; +import { updateLocalStore } from '../../../client/action/auth'; +import { ROOT_PATH } from '../paths'; + +export enum GetBaseUrlError { + NotAllow = 'NotAllow', + NotFound = 'NotFound', +} +export const factoryGetBaseUrl = (clientConfig: ClientConfig, server: string) => { + const getBaseUrl = async (): Promise => { + if (!clientAllowedServer(clientConfig, server)) { + throw new Error(GetBaseUrlError.NotAllow); + } + + const [, discovery] = await to(autoDiscovery(fetch, server)); + + let mxIdBaseUrl: string | undefined; + const [, discoveryInfo] = discovery ?? []; + + if (discoveryInfo) { + mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url; + } + + if (!mxIdBaseUrl) { + throw new Error(GetBaseUrlError.NotFound); + } + const [, versions] = await to(specVersions(fetch, mxIdBaseUrl)); + if (!versions) { + throw new Error(GetBaseUrlError.NotFound); + } + return mxIdBaseUrl; + }; + return getBaseUrl; +}; + +export enum LoginError { + ServerNotAllowed = 'ServerNotAllowed', + InvalidServer = 'InvalidServer', + Forbidden = 'Forbidden', + UserDeactivated = 'UserDeactivated', + InvalidRequest = 'InvalidRequest', + RateLimited = 'RateLimited', + Unknown = 'Unknown', +} + +export type CustomLoginResponse = { + baseUrl: string; + response: LoginResponse; +}; +export const login = async ( + serverBaseUrl: string | (() => Promise), + data: Omit +): Promise => { + const [urlError, url] = + typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; + if (urlError) { + throw new MatrixError({ + errcode: + urlError.message === GetBaseUrlError.NotAllow + ? LoginError.ServerNotAllowed + : LoginError.InvalidServer, + }); + } + + const mx = createClient({ baseUrl: url }); + const [err, res] = await to(mx.login('m.login.password', data)); + + if (err) { + if (err.httpStatus === 400) { + throw new MatrixError({ + errcode: LoginError.InvalidRequest, + }); + } + if (err.httpStatus === 429) { + throw new MatrixError({ + errcode: LoginError.RateLimited, + }); + } + if (err.errcode === 'M_USER_DEACTIVATED') { + throw new MatrixError({ + errcode: LoginError.UserDeactivated, + }); + } + + if (err.httpStatus === 403) { + throw new MatrixError({ + errcode: LoginError.Forbidden, + }); + } + + throw new MatrixError({ + errcode: LoginError.Unknown, + }); + } + return { + baseUrl: url, + response: res, + }; +}; + +export const useLoginComplete = (data?: CustomLoginResponse) => { + const navigate = useNavigate(); + + useEffect(() => { + if (data) { + const { response: loginRes, baseUrl: loginBaseUrl } = data; + updateLocalStore(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl); + // TODO: add after login redirect url + navigate(ROOT_PATH, { replace: true }); + } + }, [data, navigate]); +}; From a1f6189579a1cd692a93bca5a53dbea12fbe8991 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 26 Dec 2023 12:16:50 +0530 Subject: [PATCH 28/68] fix hardcoded m.login.password in login func --- src/app/pages/auth/PasswordLoginForm.tsx | 3 +++ src/app/pages/auth/SSOLogin.tsx | 3 ++- src/app/pages/auth/TokenLogin.tsx | 4 ++-- src/app/pages/auth/loginUtil.ts | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/PasswordLoginForm.tsx index cc5b404157..e3dd9cefc8 100644 --- a/src/app/pages/auth/PasswordLoginForm.tsx +++ b/src/app/pages/auth/PasswordLoginForm.tsx @@ -133,6 +133,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const handleUsernameLogin = (username: string, password: string) => { startLogin(baseUrl, { + type: 'm.login.password', identifier: { type: 'm.id.user', user: username, @@ -150,6 +151,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog const getBaseUrl = factoryGetBaseUrl(clientConfig, mxIdServer); startLogin(getBaseUrl, { + type: 'm.login.password', identifier: { type: 'm.id.user', user: mxIdUsername, @@ -160,6 +162,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog }; const handleEmailLogin = (email: string, password: string) => { startLogin(baseUrl, { + type: 'm.login.password', identifier: { type: 'm.id.thirdparty', medium: 'email', diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx index 76181b9cc3..d63bff9b7d 100644 --- a/src/app/pages/auth/SSOLogin.tsx +++ b/src/app/pages/auth/SSOLogin.tsx @@ -13,7 +13,8 @@ export function SSOLogin({ providers, canPasswordLogin }: SSOLoginProps) { const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); const getSSOIdUrl = (ssoId: string): string => { - const redirectUrl = window.location.href; + // remove query params and use current url as redirect + const [redirectUrl] = window.location.href.split('?'); return mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); }; diff --git a/src/app/pages/auth/TokenLogin.tsx b/src/app/pages/auth/TokenLogin.tsx index 302543bbbb..d2e6d90a60 100644 --- a/src/app/pages/auth/TokenLogin.tsx +++ b/src/app/pages/auth/TokenLogin.tsx @@ -25,7 +25,7 @@ function LoginTokenError({ message }: { message: string }) { padding: config.space.S300, borderRadius: config.radii.R400, }} - justifyContent="Center" + justifyContent="Start" alignItems="Start" gap="300" > @@ -68,7 +68,7 @@ export function TokenLogin({ token }: TokenLoginProps) { {loginState.status === AsyncStatus.Error && ( <> {loginState.error.errcode === LoginError.Forbidden && ( - + )} {loginState.error.errcode === LoginError.UserDeactivated && ( diff --git a/src/app/pages/auth/loginUtil.ts b/src/app/pages/auth/loginUtil.ts index 3a6ef1c415..dff2690761 100644 --- a/src/app/pages/auth/loginUtil.ts +++ b/src/app/pages/auth/loginUtil.ts @@ -54,7 +54,7 @@ export type CustomLoginResponse = { }; export const login = async ( serverBaseUrl: string | (() => Promise), - data: Omit + data: LoginRequest ): Promise => { const [urlError, url] = typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl]; @@ -68,7 +68,7 @@ export const login = async ( } const mx = createClient({ baseUrl: url }); - const [err, res] = await to(mx.login('m.login.password', data)); + const [err, res] = await to(mx.login(data.type, data)); if (err) { if (err.httpStatus === 400) { From a5f2cb69b1842c4ffe4a8c07631ca1f5c1ac8c4f Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:38:41 +0530 Subject: [PATCH 29/68] update server input on url change --- src/app/pages/auth/AuthLayout.tsx | 2 +- src/app/pages/auth/Login.tsx | 2 +- src/app/pages/auth/ServerPicker.tsx | 26 ++++++++++++-------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index ecc0fbee22..8cb9f6bc35 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -152,7 +152,7 @@ export function AuthLayout() { Homeserver
- Or + OR
)} diff --git a/src/app/pages/auth/ServerPicker.tsx b/src/app/pages/auth/ServerPicker.tsx index e25f94c1ce..57fc907d9d 100644 --- a/src/app/pages/auth/ServerPicker.tsx +++ b/src/app/pages/auth/ServerPicker.tsx @@ -23,12 +23,12 @@ import FocusTrap from 'focus-trap-react'; import { useDebounce } from '../../hooks/useDebounce'; export function ServerPicker({ - defaultServer, + server, serverList, allowCustomServer, onServerChange, }: { - defaultServer: string; + server: string; serverList: string[]; allowCustomServer?: boolean; onServerChange: (server: string) => void; @@ -36,6 +36,10 @@ export function ServerPicker({ const [serverMenu, setServerMenu] = useState(false); const serverInputRef = useRef(null); + if (serverInputRef.current && serverInputRef.current.value !== server) { + serverInputRef.current.value = server; + } + const handleServerChange: ChangeEventHandler = useDebounce( useCallback( (evt) => { @@ -57,24 +61,18 @@ export function ServerPicker({ const handleServerSelect: MouseEventHandler = (evt) => { const selectedServer = evt.currentTarget.getAttribute('data-server'); if (selectedServer) { - const serverInput = serverInputRef.current; - if (serverInput) { - serverInput.value = selectedServer; - } onServerChange(selectedServer); } setServerMenu(false); }; - // TODO: input not update on url changes - return ( Homeserver List
- {serverList?.map((server) => ( + {serverList?.map((serverName) => ( - {server} + {serverName} ))}
From 9c039eb460681df3cee2db0bdad5cea02e41bafd Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:17:53 +0530 Subject: [PATCH 30/68] Improve sso login labels --- src/app/pages/auth/Login.tsx | 13 ++++--------- src/app/pages/auth/OrDivider.tsx | 12 ++++++++++++ src/app/pages/auth/SSOLogin.tsx | 10 +++++----- 3 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 src/app/pages/auth/OrDivider.tsx diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 861aeabc36..58537dcf26 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Line, Text, color } from 'folds'; +import { Box, Text, color } from 'folds'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; import { REGISTER_PATH } from '../paths'; import { useAuthFlows } from '../../hooks/useAuthFlows'; @@ -8,6 +8,7 @@ import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; import { PasswordLoginForm } from './PasswordLoginForm'; import { SSOLogin } from './SSOLogin'; import { TokenLogin } from './TokenLogin'; +import { OrDivider } from './OrDivider'; export type LoginSearchParams = { username?: string; @@ -44,20 +45,14 @@ export function Login() { defaultEmail={loginSearchParams.email} /> - {parsedFlows.sso && ( - - - OR - - - )} + {parsedFlows.sso && } )} {parsedFlows.sso && ( <> diff --git a/src/app/pages/auth/OrDivider.tsx b/src/app/pages/auth/OrDivider.tsx new file mode 100644 index 0000000000..629d3f5209 --- /dev/null +++ b/src/app/pages/auth/OrDivider.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Box, Line, Text } from 'folds'; + +export function OrDivider() { + return ( + + + OR + + + ); +} diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx index d63bff9b7d..a468fcd0c3 100644 --- a/src/app/pages/auth/SSOLogin.tsx +++ b/src/app/pages/auth/SSOLogin.tsx @@ -5,9 +5,9 @@ import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; type SSOLoginProps = { providers: IIdentityProvider[]; - canPasswordLogin?: boolean; + asIcons?: boolean; }; -export function SSOLogin({ providers, canPasswordLogin }: SSOLoginProps) { +export function SSOLogin({ providers, asIcons }: SSOLoginProps) { const discovery = useAutoDiscoveryInfo(); const baseUrl = discovery['m.homeserver'].base_url; const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); @@ -24,11 +24,11 @@ export function SSOLogin({ providers, canPasswordLogin }: SSOLoginProps) { const { id, name, icon } = provider; const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false); - const buttonTitle = `Login with ${name}`; + const buttonTitle = `Continue with ${name}`; // Only show SSO buttons as icons if we have - // password login UI and high number of SSO buttons - if (iconUrl && canPasswordLogin && providers.length > 2) { + // high number of SSO buttons to display + if (iconUrl && asIcons && providers.length > 2) { return ( Date: Sat, 30 Dec 2023 14:19:44 +0530 Subject: [PATCH 31/68] update folds --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f1ef881c9..033a5465fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "file-saver": "2.0.5", "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "1.5.0", + "folds": "1.5.1", "formik": "2.2.9", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", @@ -5185,9 +5185,9 @@ } }, "node_modules/folds": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/folds/-/folds-1.5.0.tgz", - "integrity": "sha512-1QNHzD57OxFZT5SOe0nWcrKQvWmfMRv1f5sTF8xhGtwx9rajjv36T9SwCcj9Fh58PbERqOdBiwvpdhu+BQTVjg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/folds/-/folds-1.5.1.tgz", + "integrity": "sha512-2QxyA+FRKjPKXDTMDoD7NmOUiReWrKYO0Msg44QqlzTkTrRVEzJgyPIfC/Ia4/u0ByQpk6dbq8UQxomKmneJ/g==", "peerDependencies": { "@vanilla-extract/css": "^1.9.2", "@vanilla-extract/recipes": "^0.3.0", diff --git a/package.json b/package.json index ffed200b4c..dea1bd9408 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "file-saver": "2.0.5", "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "1.5.0", + "folds": "1.5.1", "formik": "2.2.9", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", From 5dcf45096aadf53c148197ced773d88775816dc1 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 30 Dec 2023 14:21:23 +0530 Subject: [PATCH 32/68] fix async callback batching state update in safari --- src/app/components/AuthFlowsLoader.tsx | 8 +++++--- src/app/components/ClientConfigLoader.tsx | 6 ++++-- src/app/components/SpecVersionsLoader.tsx | 6 ++++-- src/app/hooks/useAsyncCallback.ts | 9 +++++++-- src/app/organisms/room/message/UrlPreviewCard.tsx | 5 ++++- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx index 9dbd06c496..5d3269c23b 100644 --- a/src/app/components/AuthFlowsLoader.tsx +++ b/src/app/components/AuthFlowsLoader.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useMemo } from 'react'; +import { ReactNode, useCallback, useEffect, useMemo } from 'react'; import { IAuthData, MatrixError, createClient } from 'matrix-js-sdk'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo'; @@ -8,7 +8,7 @@ import { AuthFlows, RegisterFlowStatus, RegisterFlowsResponse } from '../hooks/u type AuthFlowsLoaderProps = { fallback?: () => ReactNode; error?: (err: unknown) => ReactNode; - children: (versions: AuthFlows) => ReactNode; + children: (authFlows: AuthFlows) => ReactNode; }; export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderProps) { const autoDiscoveryInfo = useAutoDiscoveryInfo(); @@ -66,7 +66,9 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr }, [mx]) ); - if (state.status === AsyncStatus.Idle) load(); + useEffect(() => { + load(); + }, [load]); if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { return fallback?.(); diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx index 15a3c97107..43b7812f71 100644 --- a/src/app/components/ClientConfigLoader.tsx +++ b/src/app/components/ClientConfigLoader.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { ClientConfig } from '../hooks/useClientConfig'; @@ -14,7 +14,9 @@ type ClientConfigLoaderProps = { export function ClientConfigLoader({ fallback, children }: ClientConfigLoaderProps) { const [state, load] = useAsyncCallback(getClientConfig); - if (state.status === AsyncStatus.Idle) load(); + useEffect(() => { + load(); + }, [load]); if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { return fallback?.(); diff --git a/src/app/components/SpecVersionsLoader.tsx b/src/app/components/SpecVersionsLoader.tsx index 449a1a33ba..56d7f8b055 100644 --- a/src/app/components/SpecVersionsLoader.tsx +++ b/src/app/components/SpecVersionsLoader.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback } from 'react'; +import { ReactNode, useCallback, useEffect } from 'react'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { SpecVersions, specVersions } from '../cs-api'; import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo'; @@ -16,7 +16,9 @@ export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLo useCallback(() => specVersions(fetch, baseUrl), [baseUrl]) ); - if (state.status === AsyncStatus.Idle) load(); + useEffect(() => { + load(); + }, [load]); if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { return fallback?.(); diff --git a/src/app/hooks/useAsyncCallback.ts b/src/app/hooks/useAsyncCallback.ts index f70f322895..3cefa6be7e 100644 --- a/src/app/hooks/useAsyncCallback.ts +++ b/src/app/hooks/useAsyncCallback.ts @@ -1,4 +1,5 @@ import { useCallback, useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { useAlive } from './useAlive'; export enum AsyncStatus { @@ -45,8 +46,12 @@ export const useAsyncCallback = ( const callback: AsyncCallback = useCallback( async (...args) => { - setState({ - status: AsyncStatus.Loading, + flushSync(() => { + // flushSync because + // https://github.com/facebook/react/issues/26713#issuecomment-1872085134 + setState({ + status: AsyncStatus.Loading, + }); }); reqNumberRef.current += 1; diff --git a/src/app/organisms/room/message/UrlPreviewCard.tsx b/src/app/organisms/room/message/UrlPreviewCard.tsx index 9ae4d298b3..b085e184aa 100644 --- a/src/app/organisms/room/message/UrlPreviewCard.tsx +++ b/src/app/organisms/room/message/UrlPreviewCard.tsx @@ -23,7 +23,10 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>( const [previewStatus, loadPreview] = useAsyncCallback( useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) ); - if (previewStatus.status === AsyncStatus.Idle) loadPreview(); + + useEffect(() => { + loadPreview(); + }, [loadPreview]); if (previewStatus.status === AsyncStatus.Error) return null; From 7facccaae2b8e7e5e721223d74e1dd85482dbefd Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 31 Dec 2023 10:45:36 +0530 Subject: [PATCH 33/68] wrap async callback set state in queueMicrotask --- src/app/hooks/useAsyncCallback.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/hooks/useAsyncCallback.ts b/src/app/hooks/useAsyncCallback.ts index 3cefa6be7e..fc7dca63fe 100644 --- a/src/app/hooks/useAsyncCallback.ts +++ b/src/app/hooks/useAsyncCallback.ts @@ -46,11 +46,16 @@ export const useAsyncCallback = ( const callback: AsyncCallback = useCallback( async (...args) => { - flushSync(() => { - // flushSync because - // https://github.com/facebook/react/issues/26713#issuecomment-1872085134 - setState({ - status: AsyncStatus.Loading, + queueMicrotask(() => { + // Warning: flushSync was called from inside a lifecycle method. + // React cannot flush when React is already rendering. + // Consider moving this call to a scheduler task or micro task. + flushSync(() => { + // flushSync because + // https://github.com/facebook/react/issues/26713#issuecomment-1872085134 + setState({ + status: AsyncStatus.Loading, + }); }); }); From 974197594944e13d5d6f4a513c274436eaff98b5 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 14 Jan 2024 16:54:53 +0530 Subject: [PATCH 34/68] wip --- src/app/components/AuthFlowsLoader.tsx | 38 +-- .../components/SupportedUIAFlowsLoader.tsx | 17 ++ .../password-input/PasswordInput.tsx | 45 +++ src/app/hooks/useAuthFlows.ts | 25 +- src/app/hooks/useUIAFlows.ts | 98 +++++++ src/app/pages/auth/AuthLayout.tsx | 11 +- src/app/pages/auth/Login.tsx | 6 +- src/app/pages/auth/PasswordLoginForm.tsx | 32 +-- src/app/pages/auth/PasswordRegisterForm.tsx | 271 ++++++++++++++++++ src/app/pages/auth/Register.tsx | 133 +++++---- src/app/pages/auth/SSOLogin.tsx | 13 +- src/app/pages/auth/ServerPicker.tsx | 22 +- src/app/utils/matrix-uia.ts | 77 +++++ 13 files changed, 651 insertions(+), 137 deletions(-) create mode 100644 src/app/components/SupportedUIAFlowsLoader.tsx create mode 100644 src/app/components/password-input/PasswordInput.tsx create mode 100644 src/app/hooks/useUIAFlows.ts create mode 100644 src/app/pages/auth/PasswordRegisterForm.tsx create mode 100644 src/app/utils/matrix-uia.ts diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx index 5d3269c23b..f21bad0440 100644 --- a/src/app/components/AuthFlowsLoader.tsx +++ b/src/app/components/AuthFlowsLoader.tsx @@ -1,9 +1,14 @@ import { ReactNode, useCallback, useEffect, useMemo } from 'react'; -import { IAuthData, MatrixError, createClient } from 'matrix-js-sdk'; +import { MatrixError, createClient } from 'matrix-js-sdk'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo'; import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common'; -import { AuthFlows, RegisterFlowStatus, RegisterFlowsResponse } from '../hooks/useAuthFlows'; +import { + AuthFlows, + RegisterFlowStatus, + RegisterFlowsResponse, + parseRegisterErrResp, +} from '../hooks/useAuthFlows'; type AuthFlowsLoaderProps = { fallback?: () => ReactNode; @@ -20,34 +25,11 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr useCallback(async () => { const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]); const loginFlows = promiseFulfilledResult(result[0]); - const registerReason = promiseRejectedResult(result[1]) as MatrixError | undefined; + const registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined; let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest }; - if (typeof registerReason === 'object' && registerReason.httpStatus) { - switch (registerReason.httpStatus) { - case RegisterFlowStatus.InvalidRequest: { - registerFlows = { status: RegisterFlowStatus.InvalidRequest }; - break; - } - case RegisterFlowStatus.RateLimited: { - registerFlows = { status: RegisterFlowStatus.RateLimited }; - break; - } - case RegisterFlowStatus.RegistrationDisabled: { - registerFlows = { status: RegisterFlowStatus.RegistrationDisabled }; - break; - } - case RegisterFlowStatus.FlowRequired: { - registerFlows = { - status: RegisterFlowStatus.FlowRequired, - data: registerReason.data as IAuthData, - }; - break; - } - default: { - registerFlows = { status: RegisterFlowStatus.InvalidRequest }; - } - } + if (typeof registerResp === 'object' && registerResp.httpStatus) { + registerFlows = parseRegisterErrResp(registerResp); } if (!loginFlows) { diff --git a/src/app/components/SupportedUIAFlowsLoader.tsx b/src/app/components/SupportedUIAFlowsLoader.tsx new file mode 100644 index 0000000000..442eb5729b --- /dev/null +++ b/src/app/components/SupportedUIAFlowsLoader.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import { UIAFlow } from 'matrix-js-sdk'; +import { useSupportedUIAFlows } from '../hooks/useUIAFlows'; + +export function SupportedUIAFlowsLoader({ + flows, + supportedStages, + children, +}: { + supportedStages: string[]; + flows: UIAFlow[]; + children: (supportedFlows: UIAFlow[]) => ReactNode; +}) { + const supportedFlows = useSupportedUIAFlows(flows, supportedStages); + + return children(supportedFlows); +} diff --git a/src/app/components/password-input/PasswordInput.tsx b/src/app/components/password-input/PasswordInput.tsx new file mode 100644 index 0000000000..ca4a645a27 --- /dev/null +++ b/src/app/components/password-input/PasswordInput.tsx @@ -0,0 +1,45 @@ +import React, { ComponentProps, forwardRef } from 'react'; +import { Icon, IconButton, Input, config, Icons } from 'folds'; +import { UseStateProvider } from '../UseStateProvider'; + +type PasswordInputProps = Omit, 'type' | 'size'> & { + size: '400' | '500'; +}; +export const PasswordInput = forwardRef( + ({ variant, size, style, after, ...props }, ref) => { + const btnSize: ComponentProps['size'] = size === '500' ? '400' : '300'; + + return ( + + {(visible, setVisible) => ( + + {after} + setVisible(!visible)} + type="button" + variant={visible ? 'Warning' : variant} + size={btnSize} + radii="300" + > + + + + } + /> + )} + + ); + } +); diff --git a/src/app/hooks/useAuthFlows.ts b/src/app/hooks/useAuthFlows.ts index 2801368159..7bb7ddc551 100644 --- a/src/app/hooks/useAuthFlows.ts +++ b/src/app/hooks/useAuthFlows.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react'; -import { IAuthData } from 'matrix-js-sdk'; +import { IAuthData, MatrixError } from 'matrix-js-sdk'; import { ILoginFlowsResponse } from 'matrix-js-sdk/lib/@types/auth'; export enum RegisterFlowStatus { @@ -18,6 +18,29 @@ export type RegisterFlowsResponse = status: Exclude; }; +export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsResponse => { + switch (matrixError.httpStatus) { + case RegisterFlowStatus.InvalidRequest: { + return { status: RegisterFlowStatus.InvalidRequest }; + } + case RegisterFlowStatus.RateLimited: { + return { status: RegisterFlowStatus.RateLimited }; + } + case RegisterFlowStatus.RegistrationDisabled: { + return { status: RegisterFlowStatus.RegistrationDisabled }; + } + case RegisterFlowStatus.FlowRequired: { + return { + status: RegisterFlowStatus.FlowRequired, + data: matrixError.data as IAuthData, + }; + } + default: { + return { status: RegisterFlowStatus.InvalidRequest }; + } + } +}; + export type AuthFlows = { loginFlows: ILoginFlowsResponse; registerFlows: RegisterFlowsResponse; diff --git a/src/app/hooks/useUIAFlows.ts b/src/app/hooks/useUIAFlows.ts new file mode 100644 index 0000000000..67915ff9db --- /dev/null +++ b/src/app/hooks/useUIAFlows.ts @@ -0,0 +1,98 @@ +import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk'; +import { useCallback, useMemo } from 'react'; +import { + getSupportedUIAFlows, + getUIACompleted, + getUIAErrorCode, + getUIAParams, + getUIASession, +} from '../utils/matrix-uia'; + +export const SUPPORTED_FLOW_TYPES = [ + AuthType.Dummy, + AuthType.Password, + AuthType.Email, + AuthType.Terms, + AuthType.Recaptcha, + AuthType.RegistrationToken, +] as const; + +export const useSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] => + useMemo(() => getSupportedUIAFlows(uiaFlows, supportedStages), [uiaFlows, supportedStages]); + +export const useUIACompleted = (authData: IAuthData): string[] => + useMemo(() => getUIACompleted(authData), [authData]); + +export const useUIAParams = (authData: IAuthData) => + useMemo(() => getUIAParams(authData), [authData]); + +export const useUIASession = (authData: IAuthData) => + useMemo(() => getUIASession(authData), [authData]); + +export const useUIAErrorCode = (authData: IAuthData) => + useMemo(() => getUIAErrorCode(authData), [authData]); + +export type StageInfo = Record; +export type AuthStageData = { + type: string; + info?: StageInfo; + session?: string; + errorCode?: string; +}; +export type AuthStageDataGetter = () => AuthStageData | undefined; + +export type UIAFlowInterface = { + getStageToComplete: AuthStageDataGetter; + hasStage: (stageType: string) => boolean; + getStageInfo: (stageType: string) => StageInfo | undefined; +}; +export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterface => { + const completed = useUIACompleted(authData); + const params = useUIAParams(authData); + const session = useUIASession(authData); + const errorCode = useUIAErrorCode(authData); + + const getPrevCompletedStage = useCallback(() => { + const prevCompletedI = completed.length - 1; + const prevCompletedStage = prevCompletedI !== -1 ? completed[prevCompletedI] : undefined; + return prevCompletedStage; + }, [completed]); + + const getStageToComplete: AuthStageDataGetter = useCallback(() => { + const { stages } = uiaFlow; + const prevCompletedStage = getPrevCompletedStage(); + + const nextStageIndex = stages.findIndex((stage) => stage === prevCompletedStage) + 1; + const nextStage = nextStageIndex < stages.length ? stages[nextStageIndex] : undefined; + if (!nextStage) return undefined; + + const info = params[nextStage]; + + return { + type: nextStage, + info, + session, + errorCode, + }; + }, [uiaFlow, getPrevCompletedStage, params, errorCode, session]); + + const hasStage = useCallback( + (stageType: string): boolean => uiaFlow.stages.includes(stageType), + [uiaFlow] + ); + + const getStageInfo = useCallback( + (stageType: string): StageInfo | undefined => { + if (!hasStage(stageType)) return undefined; + + return params[stageType]; + }, + [hasStage, params] + ); + + return { + getStageToComplete, + hasStage, + getStageInfo, + }; +}; diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 8cb9f6bc35..ba41c79632 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -114,11 +114,16 @@ export function AuthLayout() { const selectServer = useCallback( (newServer: string) => { + if (newServer === server) { + if (discoveryState.status === AsyncStatus.Loading) return; + discoverServer(server); + return; + } navigate( generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) }) ); }, - [navigate, location] + [navigate, location, discoveryState, server, discoverServer] ); const [autoDiscoveryError, autoDiscoveryInfo] = @@ -131,7 +136,7 @@ export function AuthLayout() { direction="Column" alignItems="Center" justifyContent="SpaceBetween" - gap="700" + gap="400" > @@ -182,7 +187,7 @@ export function AuthLayout() { /> )} error={() => ( - + )} > {(specVersions) => ( diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/Login.tsx index 58537dcf26..d2dadaf932 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/Login.tsx @@ -27,6 +27,7 @@ export function Login() { const { loginFlows } = useAuthFlows(); const [searchParams] = useSearchParams(); const loginSearchParams = getLoginSearchParams(searchParams); + const [ssoRedirectUrl] = window.location.href.split('?'); const parsedFlows = useParsedLoginFlows(loginFlows.flows); @@ -52,7 +53,10 @@ export function Login() { <> 2 + } /> diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/PasswordLoginForm.tsx index e3dd9cefc8..8b76fc57ef 100644 --- a/src/app/pages/auth/PasswordLoginForm.tsx +++ b/src/app/pages/auth/PasswordLoginForm.tsx @@ -20,7 +20,6 @@ import { import FocusTrap from 'focus-trap-react'; import { Link, generatePath } from 'react-router-dom'; import { MatrixError } from 'matrix-js-sdk'; -import { UseStateProvider } from '../../components/UseStateProvider'; import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; import { EMAIL_REGEX } from '../../utils/regex'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; @@ -35,6 +34,7 @@ import { login, useLoginComplete, } from './loginUtil'; +import { PasswordInput } from '../../components/password-input/PasswordInput'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); @@ -233,34 +233,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog Password - - {(visible, setVisible) => ( - setVisible(!visible)} - type="button" - variant={visible ? 'Warning' : 'Background'} - size="400" - radii="300" - > - - - } - /> - )} - + {loginState.status === AsyncStatus.Error && ( <> @@ -289,7 +262,6 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog - + {flowData && ( + + )} + + ); +} diff --git a/src/app/pages/auth/Register.tsx b/src/app/pages/auth/Register.tsx index e8d2944bc1..e6137c46a3 100644 --- a/src/app/pages/auth/Register.tsx +++ b/src/app/pages/auth/Register.tsx @@ -1,72 +1,97 @@ import React from 'react'; -import { Box, Button, Icon, IconButton, Icons, Input, Text, config } from 'folds'; -import { Link, generatePath } from 'react-router-dom'; +import { Box, Text, color } from 'folds'; +import { Link, generatePath, useSearchParams } from 'react-router-dom'; import { LOGIN_PATH } from '../paths'; import { useAuthServer } from '../../hooks/useAuthServer'; +import { RegisterFlowStatus, useAuthFlows } from '../../hooks/useAuthFlows'; +import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; +import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from './PasswordRegisterForm'; +import { OrDivider } from './OrDivider'; +import { SSOLogin } from './SSOLogin'; +import { SupportedUIAFlowsLoader } from '../../components/SupportedUIAFlowsLoader'; + +export type RegisterSearchParams = { + username?: string; + email?: string; + registerToken?: string; +}; + +const getRegisterSearchParams = (searchParams: URLSearchParams): RegisterSearchParams => ({ + username: searchParams.get('username') ?? undefined, + email: searchParams.get('email') ?? undefined, + registerToken: searchParams.get('registerToken') ?? undefined, +}); export function Register() { const server = useAuthServer(); + const { loginFlows, registerFlows } = useAuthFlows(); + const [searchParams] = useSearchParams(); + const registerSearchParams = getRegisterSearchParams(searchParams); + const { sso } = useParsedLoginFlows(loginFlows.flows); + + // redirect to /login because only that path handle m.login.token + const [ssoRedirectUrl] = window.location.href.split('?'); + ssoRedirectUrl.replace('/register/', '/login/'); return ( Register - - - - Username - - - - - - Password - - - - + {registerFlows.status === RegisterFlowStatus.RegistrationDisabled && !sso && ( + + Registration has been disabled on this homeserver. + + )} + {registerFlows.status === RegisterFlowStatus.RateLimited && !sso && ( + + You have been rate-limited! Please try after some time. + + )} + {registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && ( + + Invalid Request! + + )} + {registerFlows.status === RegisterFlowStatus.FlowRequired && ( + <> + + {(supportedFlows) => + supportedFlows.length === 0 ? ( + + This application does not support registration on this homeserver. + + ) : ( + + ) } - /> - - - - Confirm Password - - - - + + + {sso && } + + )} + {sso && ( + <> + 2 } /> - - - - Email - - - - - - - + + + )} Already have an account? Login diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx index a468fcd0c3..a9c1c54bc1 100644 --- a/src/app/pages/auth/SSOLogin.tsx +++ b/src/app/pages/auth/SSOLogin.tsx @@ -6,17 +6,14 @@ import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; type SSOLoginProps = { providers: IIdentityProvider[]; asIcons?: boolean; + redirectUrl: string; }; -export function SSOLogin({ providers, asIcons }: SSOLoginProps) { +export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) { const discovery = useAutoDiscoveryInfo(); const baseUrl = discovery['m.homeserver'].base_url; const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); - const getSSOIdUrl = (ssoId: string): string => { - // remove query params and use current url as redirect - const [redirectUrl] = window.location.href.split('?'); - return mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); - }; + const getSSOIdUrl = (ssoId: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); return ( @@ -26,9 +23,7 @@ export function SSOLogin({ providers, asIcons }: SSOLoginProps) { const buttonTitle = `Continue with ${name}`; - // Only show SSO buttons as icons if we have - // high number of SSO buttons to display - if (iconUrl && asIcons && providers.length > 2) { + if (iconUrl && asIcons) { return ( = useDebounce( - useCallback( - (evt) => { - const inputServer = evt.target.value.trim(); - if (inputServer) onServerChange(inputServer); - }, - [onServerChange] - ), - { wait: 700 } - ); + const debounceServerSelect = useDebounce(onServerChange, { wait: 700 }); + + const handleServerChange: ChangeEventHandler = (evt) => { + const inputServer = evt.target.value.trim(); + if (inputServer) debounceServerSelect(inputServer); + }; const handleKeyDown: KeyboardEventHandler = (evt) => { if (evt.key === 'ArrowDown') { evt.preventDefault(); setServerMenu(true); } + if (evt.key === 'Enter') { + evt.preventDefault(); + const inputServer = evt.currentTarget.value.trim(); + if (inputServer) debounceServerSelect(inputServer); + } }; const handleServerSelect: MouseEventHandler = (evt) => { diff --git a/src/app/utils/matrix-uia.ts b/src/app/utils/matrix-uia.ts new file mode 100644 index 0000000000..91ebf614a2 --- /dev/null +++ b/src/app/utils/matrix-uia.ts @@ -0,0 +1,77 @@ +import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk'; + +export const getSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] => { + const supportedUIAFlows = uiaFlows.filter((flow) => + flow.stages.every((stage) => supportedStages.includes(stage)) + ); + + return supportedUIAFlows; +}; + +export const getUIACompleted = (authData: IAuthData): string[] => { + const completed = authData.completed ?? []; + return completed; +}; + +export type UIAParams = Record>; +export const getUIAParams = (authData: IAuthData): UIAParams => { + const params = authData.params ?? {}; + return params; +}; + +export const getUIASession = (authData: IAuthData): string | undefined => { + const session = authData.session ?? undefined; + return session; +}; + +export const getUIAErrorCode = (authData: IAuthData): string | undefined => { + const errorCode = + 'errcode' in authData && typeof authData.errcode === 'string' ? authData.errcode : undefined; + + return errorCode; +}; + +export const getUIAFlowForStages = (uiaFlows: UIAFlow[], stages: string[]): UIAFlow | undefined => { + const matchedFlows = uiaFlows + .filter((flow) => { + if (flow.stages.length < stages.length) return false; + if (flow.stages.length > stages.length) { + // As a valid flow can also have m.login.dummy type, + // we will pick one extra length flow only if it has dummy + if (flow.stages.length > stages.length + 1) return false; + if (stages.includes(AuthType.Dummy)) return false; + if (flow.stages.includes(AuthType.Dummy)) return true; + return false; + } + return true; + }) + .filter((flow) => stages.every((stage) => flow.stages.includes(stage))); + + if (matchedFlows.length === 0) return undefined; + + matchedFlows.sort((a, b) => a.stages.length - b.stages.length); + return matchedFlows[0]; +}; + +export const hasStageInFlows = (uiaFlows: UIAFlow[], stage: string) => + uiaFlows.some((flow) => flow.stages.includes(stage)); + +export const requiredStageInFlows = (uiaFlows: UIAFlow[], stage: string) => + uiaFlows.every((flow) => flow.stages.includes(stage)); + +export const getLoginTermUrl = (params: UIAParams): string | undefined => { + const terms = params[AuthType.Terms]; + if (terms && 'policies' in terms && typeof terms.policies === 'object') { + if (terms.policies === null) return undefined; + if ('privacy_policy' in terms.policies && typeof terms.policies.privacy_policy === 'object') { + if (terms.policies.privacy_policy === null) return undefined; + const langToPolicy = terms.policies.privacy_policy as Record; + const url = langToPolicy.en?.url; + if (typeof url === 'string') return url; + + const firstKey = Object.keys(langToPolicy)[0]; + return langToPolicy[firstKey]?.url; + } + } + return undefined; +}; From 6782bf08903e7f35d5098aceb06139cf5fc89ace Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:34:25 +0530 Subject: [PATCH 35/68] wip - register --- src/app/pages/auth/PasswordRegisterForm.tsx | 128 ++++++++++---------- src/app/pages/auth/registerUtil.ts | 0 2 files changed, 63 insertions(+), 65 deletions(-) create mode 100644 src/app/pages/auth/registerUtil.ts diff --git a/src/app/pages/auth/PasswordRegisterForm.tsx b/src/app/pages/auth/PasswordRegisterForm.tsx index 3f87eaa7f1..e9e4f27c19 100644 --- a/src/app/pages/auth/PasswordRegisterForm.tsx +++ b/src/app/pages/auth/PasswordRegisterForm.tsx @@ -14,10 +14,12 @@ import { AuthType, IAuthData, MatrixError, + RegisterRequest, RegisterResponse, UIAFlow, createClient, } from 'matrix-js-sdk'; +import to from 'await-to-js'; import { PasswordInput } from '../../components/password-input/PasswordInput'; import { getLoginTermUrl, @@ -28,8 +30,7 @@ import { import { useUIAFlow, useUIAParams } from '../../hooks/useUIAFlows'; import { useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; -import to from 'await-to-js'; -import { parseRegisterErrResp } from '../../hooks/useAuthFlows'; +import { RegisterFlowStatus, parseRegisterErrResp } from '../../hooks/useAuthFlows'; export const SUPPORTED_REGISTER_STAGES = [ AuthType.RegistrationToken, @@ -38,13 +39,40 @@ export const SUPPORTED_REGISTER_STAGES = [ AuthType.Email, AuthType.Dummy, ]; +type RegisterFormInputs = { + usernameInput: HTMLInputElement; + passwordInput: HTMLInputElement; + confirmPasswordInput: HTMLInputElement; + tokenInput?: HTMLInputElement; + emailInput?: HTMLInputElement; + termsInput?: HTMLInputElement; +}; + +type FormData = { + username: string; + password: string; + token?: string; + email?: string; + terms?: boolean; +}; + +const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => { + const pickedStages: string[] = []; + if (formData.token) pickedStages.push(AuthType.RegistrationToken); + if (formData.email) pickedStages.push(AuthType.Email); + if (formData.terms) pickedStages.push(AuthType.Terms); + if (hasStageInFlows(uiaFlows, AuthType.Recaptcha)) { + pickedStages.push(AuthType.Recaptcha); + } + + return pickedStages; +}; type RegisterUIAFlowProps = { flow: UIAFlow; authData: IAuthData; - onAuthDataChange: (authData: IAuthData) => void; }; -function RegisterUIAFlow({ flow, authData, onAuthDataChange }: RegisterUIAFlowProps) { +function RegisterUIAFlow({ flow, authData }: RegisterUIAFlowProps) { const { getStageToComplete } = useUIAFlow(authData, flow); const stageToComplete = getStageToComplete(); @@ -68,15 +96,6 @@ function RegisterUIAFlow({ flow, authData, onAuthDataChange }: RegisterUIAFlowPr ); } -type RegisterFormInputs = { - usernameInput: HTMLInputElement; - passwordInput: HTMLInputElement; - confirmPasswordInput: HTMLInputElement; - tokenInput?: HTMLInputElement; - emailInput?: HTMLInputElement; - termsInput?: HTMLInputElement; -}; - type PasswordRegisterFormProps = { authData: IAuthData; uiaFlows: UIAFlow[]; @@ -96,34 +115,33 @@ export function PasswordRegisterForm({ const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); const params = useUIAParams(authData); const termUrl = getLoginTermUrl(params); + const [formData, setFormData] = useState(); - const [flowData, setFlowData] = useState< - | { - flow: UIAFlow; - authData: IAuthData; - } - | undefined - >(); + const [flowData, setFlowData] = useState<{ flow: UIAFlow; authData: IAuthData } | undefined>(); const [registerState, register] = useAsyncCallback( - useCallback(async () => { - const [err, res] = await to(mx.register()); - if (err) { - const errRes = parseRegisterErrResp(err); - } - // TODO: registered => Redirect maybe? - }, [mx]) - ); + useCallback( + async (registerReqData: RegisterRequest) => { + const [err, res] = await to( + mx.registerRequest(registerReqData) + ); + if (err) { + const errRes = parseRegisterErrResp(err); + if (errRes.status === RegisterFlowStatus.FlowRequired) { + setFlowData((d) => d && { ...d, authData: errRes.data }); + } - const handleAuthDataChange = (authD: IAuthData) => { - setFlowData( - (d) => - d && { - flow: d.flow, - authData: authD, + return; } - ); - }; + const userId = res.user_id; + const accessToken = res.access_token; + const deviceId = res.device_id; + console.log(userId, accessToken, deviceId); + // TODO: registered => Redirect maybe? + }, + [mx] + ) + ); const handleSubmit: ChangeEventHandler = (evt) => { evt.preventDefault(); @@ -143,35 +161,21 @@ export function PasswordRegisterForm({ // TODO: display password doesn't match error if (password !== confirmPassword) return; const email = emailInput?.value.trim(); + // TODO: verify email const terms = termsInput?.value === 'on'; if (!username) { usernameInput.focus(); return; } - // TODO: match password here or in async callback - - const pickedStages: string[] = []; - if (token) pickedStages.push(AuthType.RegistrationToken); - if (email) pickedStages.push(AuthType.Email); - if (terms) pickedStages.push(AuthType.Terms); - if (hasStageInFlows(uiaFlows, AuthType.Recaptcha)) { - pickedStages.push(AuthType.Recaptcha); - } - const targetFlow = getUIAFlowForStages(uiaFlows, pickedStages); - - // TODO: send register request - // receive response - // check if done - // update auth data - // render uiaFlow component - // component will complete stages - // update the authData - // if completed it will log you in - - console.log(username, password, confirmPassword, email, token, terms); - console.log(targetFlow); + setFormData({ + username, + password, + token, + email, + terms, + }); }; return ( @@ -259,13 +263,7 @@ export function PasswordRegisterForm({ Register - {flowData && ( - - )} + {flowData && } ); } diff --git a/src/app/pages/auth/registerUtil.ts b/src/app/pages/auth/registerUtil.ts new file mode 100644 index 0000000000..e69de29bb2 From bdb7fbca9769172f8a01111307e640e2603b08dd Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:43:20 +0530 Subject: [PATCH 36/68] arrange auth file structure --- src/app/pages/auth/index.ts | 4 ++-- src/app/pages/auth/{ => login}/Login.tsx | 12 ++++++------ .../pages/auth/{ => login}/PasswordLoginForm.tsx | 16 ++++++++-------- src/app/pages/auth/{ => login}/TokenLogin.tsx | 4 ++-- src/app/pages/auth/login/index.ts | 1 + src/app/pages/auth/{ => login}/loginUtil.ts | 8 ++++---- .../auth/{ => register}/PasswordRegisterForm.tsx | 12 ++++++------ src/app/pages/auth/{ => register}/Register.tsx | 16 ++++++++-------- src/app/pages/auth/register/index.ts | 1 + .../pages/auth/{ => register}/registerUtil.ts | 0 10 files changed, 38 insertions(+), 36 deletions(-) rename src/app/pages/auth/{ => login}/Login.tsx (87%) rename src/app/pages/auth/{ => login}/PasswordLoginForm.tsx (94%) rename src/app/pages/auth/{ => login}/TokenLogin.tsx (94%) create mode 100644 src/app/pages/auth/login/index.ts rename src/app/pages/auth/{ => login}/loginUtil.ts (92%) rename src/app/pages/auth/{ => register}/PasswordRegisterForm.tsx (95%) rename src/app/pages/auth/{ => register}/Register.tsx (87%) create mode 100644 src/app/pages/auth/register/index.ts rename src/app/pages/auth/{ => register}/registerUtil.ts (100%) diff --git a/src/app/pages/auth/index.ts b/src/app/pages/auth/index.ts index ebba1f804c..3d52a7910e 100644 --- a/src/app/pages/auth/index.ts +++ b/src/app/pages/auth/index.ts @@ -1,3 +1,3 @@ export * from './AuthLayout'; -export * from './Login'; -export * from './Register'; +export * from './login'; +export * from './register'; diff --git a/src/app/pages/auth/Login.tsx b/src/app/pages/auth/login/Login.tsx similarity index 87% rename from src/app/pages/auth/Login.tsx rename to src/app/pages/auth/login/Login.tsx index d2dadaf932..72508428c4 100644 --- a/src/app/pages/auth/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { Box, Text, color } from 'folds'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; -import { REGISTER_PATH } from '../paths'; -import { useAuthFlows } from '../../hooks/useAuthFlows'; -import { useAuthServer } from '../../hooks/useAuthServer'; -import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; +import { REGISTER_PATH } from '../../paths'; +import { useAuthFlows } from '../../../hooks/useAuthFlows'; +import { useAuthServer } from '../../../hooks/useAuthServer'; +import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; import { PasswordLoginForm } from './PasswordLoginForm'; -import { SSOLogin } from './SSOLogin'; +import { SSOLogin } from '../SSOLogin'; import { TokenLogin } from './TokenLogin'; -import { OrDivider } from './OrDivider'; +import { OrDivider } from '../OrDivider'; export type LoginSearchParams = { username?: string; diff --git a/src/app/pages/auth/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx similarity index 94% rename from src/app/pages/auth/PasswordLoginForm.tsx rename to src/app/pages/auth/login/PasswordLoginForm.tsx index 8b76fc57ef..a6a7fe1a28 100644 --- a/src/app/pages/auth/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -20,13 +20,13 @@ import { import FocusTrap from 'focus-trap-react'; import { Link, generatePath } from 'react-router-dom'; import { MatrixError } from 'matrix-js-sdk'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; -import { EMAIL_REGEX } from '../../utils/regex'; -import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { REGISTER_PATH } from '../paths'; -import { useAuthServer } from '../../hooks/useAuthServer'; -import { useClientConfig } from '../../hooks/useClientConfig'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; +import { EMAIL_REGEX } from '../../../utils/regex'; +import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { REGISTER_PATH } from '../../paths'; +import { useAuthServer } from '../../../hooks/useAuthServer'; +import { useClientConfig } from '../../../hooks/useClientConfig'; import { CustomLoginResponse, LoginError, @@ -34,7 +34,7 @@ import { login, useLoginComplete, } from './loginUtil'; -import { PasswordInput } from '../../components/password-input/PasswordInput'; +import { PasswordInput } from '../../../components/password-input/PasswordInput'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); diff --git a/src/app/pages/auth/TokenLogin.tsx b/src/app/pages/auth/login/TokenLogin.tsx similarity index 94% rename from src/app/pages/auth/TokenLogin.tsx rename to src/app/pages/auth/login/TokenLogin.tsx index d2e6d90a60..761d5dc53c 100644 --- a/src/app/pages/auth/TokenLogin.tsx +++ b/src/app/pages/auth/login/TokenLogin.tsx @@ -12,8 +12,8 @@ import { } from 'folds'; import React, { useCallback, useEffect } from 'react'; import { MatrixError } from 'matrix-js-sdk'; -import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { CustomLoginResponse, LoginError, login, useLoginComplete } from './loginUtil'; function LoginTokenError({ message }: { message: string }) { diff --git a/src/app/pages/auth/login/index.ts b/src/app/pages/auth/login/index.ts new file mode 100644 index 0000000000..a10c3a83ac --- /dev/null +++ b/src/app/pages/auth/login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/src/app/pages/auth/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts similarity index 92% rename from src/app/pages/auth/loginUtil.ts rename to src/app/pages/auth/login/loginUtil.ts index dff2690761..738989fa7a 100644 --- a/src/app/pages/auth/loginUtil.ts +++ b/src/app/pages/auth/login/loginUtil.ts @@ -2,10 +2,10 @@ import to from 'await-to-js'; import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ClientConfig, clientAllowedServer } from '../../hooks/useClientConfig'; -import { autoDiscovery, specVersions } from '../../cs-api'; -import { updateLocalStore } from '../../../client/action/auth'; -import { ROOT_PATH } from '../paths'; +import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfig'; +import { autoDiscovery, specVersions } from '../../../cs-api'; +import { updateLocalStore } from '../../../../client/action/auth'; +import { ROOT_PATH } from '../../paths'; export enum GetBaseUrlError { NotAllow = 'NotAllow', diff --git a/src/app/pages/auth/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx similarity index 95% rename from src/app/pages/auth/PasswordRegisterForm.tsx rename to src/app/pages/auth/register/PasswordRegisterForm.tsx index e9e4f27c19..0476e835b3 100644 --- a/src/app/pages/auth/PasswordRegisterForm.tsx +++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx @@ -20,17 +20,17 @@ import { createClient, } from 'matrix-js-sdk'; import to from 'await-to-js'; -import { PasswordInput } from '../../components/password-input/PasswordInput'; +import { PasswordInput } from '../../../components/password-input/PasswordInput'; import { getLoginTermUrl, getUIAFlowForStages, hasStageInFlows, requiredStageInFlows, -} from '../../utils/matrix-uia'; -import { useUIAFlow, useUIAParams } from '../../hooks/useUIAFlows'; -import { useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; -import { RegisterFlowStatus, parseRegisterErrResp } from '../../hooks/useAuthFlows'; +} from '../../../utils/matrix-uia'; +import { useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows'; +import { useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; +import { RegisterFlowStatus, parseRegisterErrResp } from '../../../hooks/useAuthFlows'; export const SUPPORTED_REGISTER_STAGES = [ AuthType.RegistrationToken, diff --git a/src/app/pages/auth/Register.tsx b/src/app/pages/auth/register/Register.tsx similarity index 87% rename from src/app/pages/auth/Register.tsx rename to src/app/pages/auth/register/Register.tsx index e6137c46a3..697677a013 100644 --- a/src/app/pages/auth/Register.tsx +++ b/src/app/pages/auth/register/Register.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { Box, Text, color } from 'folds'; import { Link, generatePath, useSearchParams } from 'react-router-dom'; -import { LOGIN_PATH } from '../paths'; -import { useAuthServer } from '../../hooks/useAuthServer'; -import { RegisterFlowStatus, useAuthFlows } from '../../hooks/useAuthFlows'; -import { useParsedLoginFlows } from '../../hooks/useParsedLoginFlows'; -import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from './PasswordRegisterForm'; -import { OrDivider } from './OrDivider'; -import { SSOLogin } from './SSOLogin'; -import { SupportedUIAFlowsLoader } from '../../components/SupportedUIAFlowsLoader'; +import { LOGIN_PATH } from '../../paths'; +import { useAuthServer } from '../../../hooks/useAuthServer'; +import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows'; +import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; +import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from '../register/PasswordRegisterForm'; +import { OrDivider } from '../OrDivider'; +import { SSOLogin } from '../SSOLogin'; +import { SupportedUIAFlowsLoader } from '../../../components/SupportedUIAFlowsLoader'; export type RegisterSearchParams = { username?: string; diff --git a/src/app/pages/auth/register/index.ts b/src/app/pages/auth/register/index.ts new file mode 100644 index 0000000000..7eb55fd55e --- /dev/null +++ b/src/app/pages/auth/register/index.ts @@ -0,0 +1 @@ +export * from './Register'; diff --git a/src/app/pages/auth/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts similarity index 100% rename from src/app/pages/auth/registerUtil.ts rename to src/app/pages/auth/register/registerUtil.ts From 77b62dbe994404f7dfa17634a2dec948d4d7e449 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:26:49 +0530 Subject: [PATCH 37/68] add error codes --- src/app/cs-errorcode.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/app/cs-errorcode.ts diff --git a/src/app/cs-errorcode.ts b/src/app/cs-errorcode.ts new file mode 100644 index 0000000000..02f3ad40fb --- /dev/null +++ b/src/app/cs-errorcode.ts @@ -0,0 +1,36 @@ +export enum ErrorCode { + M_FORBIDDEN = 'M_FORBIDDEN', + M_UNKNOWN_TOKEN = 'M_UNKNOWN_TOKEN', + M_MISSING_TOKEN = 'M_MISSING_TOKEN', + M_BAD_JSON = 'M_BAD_JSON', + M_NOT_JSON = 'M_NOT_JSON', + M_NOT_FOUND = 'M_NOT_FOUND', + M_LIMIT_EXCEEDED = 'M_LIMIT_EXCEEDED', + M_UNRECOGNIZED = 'M_UNRECOGNIZED', + M_UNKNOWN = 'M_UNKNOWN', + + M_UNAUTHORIZED = 'M_UNAUTHORIZED', + M_USER_DEACTIVATED = 'M_USER_DEACTIVATED', + M_USER_IN_USE = 'M_USER_IN_USE', + M_INVALID_USERNAME = 'M_INVALID_USERNAME', + M_WEAK_PASSWORD = 'M_WEAK_PASSWORD', + M_ROOM_IN_USE = 'M_ROOM_IN_USE', + M_INVALID_ROOM_STATE = 'M_INVALID_ROOM_STATE', + M_THREEPID_IN_USE = 'M_THREEPID_IN_USE', + M_THREEPID_NOT_FOUND = 'M_THREEPID_NOT_FOUND', + M_THREEPID_AUTH_FAILED = 'M_THREEPID_AUTH_FAILED', + M_THREEPID_DENIED = 'M_THREEPID_DENIED', + M_SERVER_NOT_TRUSTED = 'M_SERVER_NOT_TRUSTED', + M_UNSUPPORTED_ROOM_VERSION = 'M_UNSUPPORTED_ROOM_VERSION', + M_INCOMPATIBLE_ROOM_VERSION = 'M_INCOMPATIBLE_ROOM_VERSION', + M_BAD_STATE = 'M_BAD_STATE', + M_GUEST_ACCESS_FORBIDDEN = 'M_GUEST_ACCESS_FORBIDDEN', + M_CAPTCHA_NEEDED = 'M_CAPTCHA_NEEDED', + M_CAPTCHA_INVALID = 'M_CAPTCHA_INVALID', + M_MISSING_PARAM = 'M_MISSING_PARAM', + M_INVALID_PARAM = 'M_INVALID_PARAM', + M_TOO_LARGE = 'M_TOO_LARGE', + M_EXCLUSIVE = 'M_EXCLUSIVE', + M_RESOURCE_LIMIT_EXCEEDED = 'M_RESOURCE_LIMIT_EXCEEDED', + M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM', +} From 39d65bc89990ebeed117ce1db2264cd93416529b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:27:17 +0530 Subject: [PATCH 38/68] extract filed error component form password login --- src/app/pages/auth/FiledError.tsx | 13 +++++++++ .../pages/auth/login/PasswordLoginForm.tsx | 27 ++++++------------- 2 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 src/app/pages/auth/FiledError.tsx diff --git a/src/app/pages/auth/FiledError.tsx b/src/app/pages/auth/FiledError.tsx new file mode 100644 index 0000000000..d96fc87259 --- /dev/null +++ b/src/app/pages/auth/FiledError.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Box, Icon, Icons, color, Text } from 'folds'; + +export function FieldError({ message }: { message: string }) { + return ( + + + + {message} + + + ); +} diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx index a6a7fe1a28..6d4a60525a 100644 --- a/src/app/pages/auth/login/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -14,7 +14,6 @@ import { PopOut, Spinner, Text, - color, config, } from 'folds'; import FocusTrap from 'focus-trap-react'; @@ -35,6 +34,7 @@ import { useLoginComplete, } from './loginUtil'; import { PasswordInput } from '../../../components/password-input/PasswordInput'; +import { FieldError } from '../FiledError'; function UsernameHint({ server }: { server: string }) { const [open, setOpen] = useState(false); @@ -101,17 +101,6 @@ function UsernameHint({ server }: { server: string }) { ); } -function LoginFieldError({ message }: { message: string }) { - return ( - - - - {message} - - - ); -} - type PasswordLoginFormProps = { defaultUsername?: string; defaultEmail?: string; @@ -221,10 +210,10 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog {loginState.status === AsyncStatus.Error && ( <> {loginState.error.errcode === LoginError.ServerNotAllowed && ( - + )} {loginState.error.errcode === LoginError.InvalidServer && ( - + )} )} @@ -238,19 +227,19 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog {loginState.status === AsyncStatus.Error && ( <> {loginState.error.errcode === LoginError.Forbidden && ( - + )} {loginState.error.errcode === LoginError.UserDeactivated && ( - + )} {loginState.error.errcode === LoginError.InvalidRequest && ( - + )} {loginState.error.errcode === LoginError.RateLimited && ( - + )} {loginState.error.errcode === LoginError.Unknown && ( - + )} )} From 2265c47d48c996dfadb1230203b346caf784605d Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:27:31 +0530 Subject: [PATCH 39/68] add register util function --- src/app/pages/auth/login/loginUtil.ts | 3 +- src/app/pages/auth/register/registerUtil.ts | 116 ++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts index 738989fa7a..b2fd387094 100644 --- a/src/app/pages/auth/login/loginUtil.ts +++ b/src/app/pages/auth/login/loginUtil.ts @@ -6,6 +6,7 @@ import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfi import { autoDiscovery, specVersions } from '../../../cs-api'; import { updateLocalStore } from '../../../../client/action/auth'; import { ROOT_PATH } from '../../paths'; +import { ErrorCode } from '../../../cs-errorcode'; export enum GetBaseUrlError { NotAllow = 'NotAllow', @@ -81,7 +82,7 @@ export const login = async ( errcode: LoginError.RateLimited, }); } - if (err.errcode === 'M_USER_DEACTIVATED') { + if (err.errcode === ErrorCode.M_USER_DEACTIVATED) { throw new MatrixError({ errcode: LoginError.UserDeactivated, }); diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts index e69de29bb2..dd7d2f8987 100644 --- a/src/app/pages/auth/register/registerUtil.ts +++ b/src/app/pages/auth/register/registerUtil.ts @@ -0,0 +1,116 @@ +import to from 'await-to-js'; +import { + IAuthData, + MatrixClient, + MatrixError, + RegisterRequest, + RegisterResponse, +} from 'matrix-js-sdk'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { updateLocalStore } from '../../../../client/action/auth'; +import { ROOT_PATH } from '../../paths'; +import { ErrorCode } from '../../../cs-errorcode'; + +export enum RegisterError { + UserTaken = 'UserTaken', + UserInvalid = 'UserInvalid', + UserExclusive = 'UserExclusive', + PasswordWeak = 'PasswordWeak', + InvalidRequest = 'InvalidRequest', + Forbidden = 'Forbidden', + RateLimited = 'RateLimited', + Unknown = 'Unknown', +} + +export type CustomRegisterResponse = { + baseUrl: string; + response: RegisterResponse; +}; +export type RegisterResult = [IAuthData, undefined] | [undefined, CustomRegisterResponse]; +export const register = async ( + mx: MatrixClient, + requestData: RegisterRequest +): Promise => { + const [err, res] = await to(mx.registerRequest(requestData)); + + if (err) { + if (err.httpStatus === 401) { + const authData = err.data as IAuthData; + return [authData, undefined]; + } + + if (err.errcode === ErrorCode.M_USER_IN_USE) { + throw new MatrixError({ + errcode: RegisterError.UserTaken, + }); + } + if (err.errcode === ErrorCode.M_INVALID_USERNAME) { + throw new MatrixError({ + errcode: RegisterError.UserInvalid, + }); + } + if (err.errcode === ErrorCode.M_EXCLUSIVE) { + throw new MatrixError({ + errcode: RegisterError.UserExclusive, + }); + } + if (err.errcode === ErrorCode.M_WEAK_PASSWORD) { + throw new MatrixError({ + errcode: RegisterError.PasswordWeak, + }); + } + + if (err.httpStatus === 429) { + throw new MatrixError({ + errcode: RegisterError.RateLimited, + }); + } + + if (err.httpStatus === 400) { + throw new MatrixError({ + errcode: RegisterError.InvalidRequest, + }); + } + + if (err.httpStatus === 403) { + throw new MatrixError({ + errcode: RegisterError.Forbidden, + }); + } + + throw new MatrixError({ + errcode: RegisterError.Unknown, + }); + } + return [ + undefined, + { + baseUrl: mx.baseUrl, + response: res, + }, + ]; +}; + +export const useRegisterComplete = (data?: CustomRegisterResponse) => { + const navigate = useNavigate(); + + useEffect(() => { + if (data) { + const { response, baseUrl } = data; + + const userId = response.user_id; + const accessToken = response.access_token; + const deviceId = response.device_id; + + if (accessToken && deviceId) { + updateLocalStore(accessToken, deviceId, userId, baseUrl); + // TODO: add after register redirect url + navigate(ROOT_PATH, { replace: true }); + } else { + // TODO: navigate to login with userId + navigate(ROOT_PATH, { replace: true }); + } + } + }, [data, navigate]); +}; From f142f611a282263e39405193ef29ead11c4fa4d4 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:27:49 +0530 Subject: [PATCH 40/68] handle register flow - WIP --- .../auth/register/PasswordRegisterForm.tsx | 220 ++++++++++-------- 1 file changed, 125 insertions(+), 95 deletions(-) diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx index 0476e835b3..65e749b639 100644 --- a/src/app/pages/auth/register/PasswordRegisterForm.tsx +++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx @@ -15,11 +15,9 @@ import { IAuthData, MatrixError, RegisterRequest, - RegisterResponse, UIAFlow, createClient, } from 'matrix-js-sdk'; -import to from 'await-to-js'; import { PasswordInput } from '../../../components/password-input/PasswordInput'; import { getLoginTermUrl, @@ -28,9 +26,10 @@ import { requiredStageInFlows, } from '../../../utils/matrix-uia'; import { useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows'; -import { useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; -import { RegisterFlowStatus, parseRegisterErrResp } from '../../../hooks/useAuthFlows'; +import { RegisterError, RegisterResult, register, useRegisterComplete } from './registerUtil'; +import { FieldError } from '../FiledError'; export const SUPPORTED_REGISTER_STAGES = [ AuthType.RegistrationToken, @@ -69,10 +68,12 @@ const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => { }; type RegisterUIAFlowProps = { + formData: FormData; flow: UIAFlow; authData: IAuthData; + onRegister: (registerReqData: RegisterRequest) => void; }; -function RegisterUIAFlow({ flow, authData }: RegisterUIAFlowProps) { +function RegisterUIAFlow({ formData, flow, authData, onRegister }: RegisterUIAFlowProps) { const { getStageToComplete } = useUIAFlow(authData, flow); const stageToComplete = getStageToComplete(); @@ -117,31 +118,19 @@ export function PasswordRegisterForm({ const termUrl = getLoginTermUrl(params); const [formData, setFormData] = useState(); - const [flowData, setFlowData] = useState<{ flow: UIAFlow; authData: IAuthData } | undefined>(); + const [ongoingFlow, setOngoingFlow] = useState(); - const [registerState, register] = useAsyncCallback( - useCallback( - async (registerReqData: RegisterRequest) => { - const [err, res] = await to( - mx.registerRequest(registerReqData) - ); - if (err) { - const errRes = parseRegisterErrResp(err); - if (errRes.status === RegisterFlowStatus.FlowRequired) { - setFlowData((d) => d && { ...d, authData: errRes.data }); - } + const [registerState, handleRegister] = useAsyncCallback< + RegisterResult, + MatrixError, + [RegisterRequest] + >(useCallback(async (registerReqData) => register(mx, registerReqData), [mx])); + const [ongoingAuthData, customRegisterResp] = + registerState.status === AsyncStatus.Success ? registerState.data : []; + const registerError = + registerState.status === AsyncStatus.Error ? registerState.error : undefined; - return; - } - const userId = res.user_id; - const accessToken = res.access_token; - const deviceId = res.device_id; - console.log(userId, accessToken, deviceId); - // TODO: registered => Redirect maybe? - }, - [mx] - ) - ); + useRegisterComplete(customRegisterResp); const handleSubmit: ChangeEventHandler = (evt) => { evt.preventDefault(); @@ -169,101 +158,142 @@ export function PasswordRegisterForm({ return; } - setFormData({ + const fData: FormData = { username, password, token, email, terms, + }; + const pickedStages = pickStages(uiaFlows, fData); + const pickedFlow = getUIAFlowForStages(uiaFlows, pickedStages); + setOngoingFlow(pickedFlow); + setFormData(fData); + handleRegister({ + username, + password, + auth: { + session: authData.session, + }, + initial_device_display_name: 'Cinny Web', }); }; return ( - - - - Username - - - - - - Password - - - - - - Confirm Password - - - - {hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && ( + <> + - {requiredStageInFlows(uiaFlows, AuthType.RegistrationToken) - ? 'Registration Token' - : 'Registration Token (Optional)'} + Username + {registerError?.errcode === RegisterError.UserTaken && ( + + )} + {registerError?.errcode === RegisterError.UserInvalid && ( + + )} + {registerError?.errcode === RegisterError.UserExclusive && ( + + )} - )} - {hasStageInFlows(uiaFlows, AuthType.Email) && ( - {requiredStageInFlows(uiaFlows, AuthType.Email) ? 'Email' : 'Email (Optional)'} + Password - + {registerError?.errcode === RegisterError.PasswordWeak && ( + + )} + + + + Confirm Password + + - )} + {hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && ( + + + {requiredStageInFlows(uiaFlows, AuthType.RegistrationToken) + ? 'Registration Token' + : 'Registration Token (Optional)'} + + + + )} + {hasStageInFlows(uiaFlows, AuthType.Email) && ( + + + {requiredStageInFlows(uiaFlows, AuthType.Email) ? 'Email' : 'Email (Optional)'} + + + + )} - {hasStageInFlows(uiaFlows, AuthType.Terms) && termUrl && ( - - - - I accept server{' '} - - Terms and Conditions - - . + {hasStageInFlows(uiaFlows, AuthType.Terms) && termUrl && ( + + + + I accept server{' '} + + Terms and Conditions + + . + + + )} + + + + {registerState.status === AsyncStatus.Success && + formData && + ongoingFlow && + ongoingAuthData && ( + + )} + {registerState.status === AsyncStatus.Loading && ( + }> + + )} - - - {flowData && } - + ); } From 2f1d278d8aaf22d3966c30175d2f3a18cc9b3a5d Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:35:43 +0530 Subject: [PATCH 41/68] update unsupported auth flow method reasons --- src/app/pages/auth/login/Login.tsx | 5 +++-- src/app/pages/auth/register/Register.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx index 72508428c4..687d9e87e4 100644 --- a/src/app/pages/auth/login/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -30,6 +30,7 @@ export function Login() { const [ssoRedirectUrl] = window.location.href.split('?'); const parsedFlows = useParsedLoginFlows(loginFlows.flows); + console.log(parsedFlows); return ( @@ -61,10 +62,10 @@ export function Login() { )} - {Object.entries(parsedFlows).every(([, flow]) => flow === undefined) && ( + {!parsedFlows.password && !parsedFlows.sso && ( <> - {`This client does not support any login method return by "${server}" homeserver.`} + {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`} diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx index 697677a013..1ddcaa9b58 100644 --- a/src/app/pages/auth/register/Register.tsx +++ b/src/app/pages/auth/register/Register.tsx @@ -50,7 +50,7 @@ export function Register() { )} {registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && ( - Invalid Request! + Invalid Request! Failed to get any registration options. )} {registerFlows.status === RegisterFlowStatus.FlowRequired && ( From a3b36c51557a9190daa374bd8315c6ad7d339b68 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:19:58 +0530 Subject: [PATCH 42/68] improve password input styles --- src/app/components/password-input/PasswordInput.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/components/password-input/PasswordInput.tsx b/src/app/components/password-input/PasswordInput.tsx index ca4a645a27..184a097c49 100644 --- a/src/app/components/password-input/PasswordInput.tsx +++ b/src/app/components/password-input/PasswordInput.tsx @@ -7,7 +7,7 @@ type PasswordInputProps = Omit, 'type' | 'size'> & }; export const PasswordInput = forwardRef( ({ variant, size, style, after, ...props }, ref) => { - const btnSize: ComponentProps['size'] = size === '500' ? '400' : '300'; + const paddingRight: string = size === '500' ? config.space.S300 : config.space.S200; return ( @@ -15,10 +15,10 @@ export const PasswordInput = forwardRef( {after} @@ -26,7 +26,7 @@ export const PasswordInput = forwardRef( onClick={() => setVisible(!visible)} type="button" variant={visible ? 'Warning' : variant} - size={btnSize} + size="300" radii="300" > Date: Wed, 17 Jan 2024 17:22:07 +0530 Subject: [PATCH 43/68] Improve UIA flow next stage calculation complete stages can have any order so we will look for first stage which is not in completed --- src/app/hooks/useUIAFlows.ts | 20 +++++++++----------- src/app/utils/matrix-uia.ts | 7 +++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/app/hooks/useUIAFlows.ts b/src/app/hooks/useUIAFlows.ts index 67915ff9db..22acd6bac3 100644 --- a/src/app/hooks/useUIAFlows.ts +++ b/src/app/hooks/useUIAFlows.ts @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'; import { getSupportedUIAFlows, getUIACompleted, + getUIAError, getUIAErrorCode, getUIAParams, getUIASession, @@ -32,12 +33,16 @@ export const useUIASession = (authData: IAuthData) => export const useUIAErrorCode = (authData: IAuthData) => useMemo(() => getUIAErrorCode(authData), [authData]); +export const useUIAError = (authData: IAuthData) => + useMemo(() => getUIAError(authData), [authData]); + export type StageInfo = Record; export type AuthStageData = { type: string; info?: StageInfo; session?: string; errorCode?: string; + error?: string; }; export type AuthStageDataGetter = () => AuthStageData | undefined; @@ -51,19 +56,11 @@ export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterf const params = useUIAParams(authData); const session = useUIASession(authData); const errorCode = useUIAErrorCode(authData); - - const getPrevCompletedStage = useCallback(() => { - const prevCompletedI = completed.length - 1; - const prevCompletedStage = prevCompletedI !== -1 ? completed[prevCompletedI] : undefined; - return prevCompletedStage; - }, [completed]); + const error = useUIAError(authData); const getStageToComplete: AuthStageDataGetter = useCallback(() => { const { stages } = uiaFlow; - const prevCompletedStage = getPrevCompletedStage(); - - const nextStageIndex = stages.findIndex((stage) => stage === prevCompletedStage) + 1; - const nextStage = nextStageIndex < stages.length ? stages[nextStageIndex] : undefined; + const nextStage = stages.find((stage) => !completed.includes(stage)); if (!nextStage) return undefined; const info = params[nextStage]; @@ -73,8 +70,9 @@ export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterf info, session, errorCode, + error, }; - }, [uiaFlow, getPrevCompletedStage, params, errorCode, session]); + }, [uiaFlow, completed, params, errorCode, error, session]); const hasStage = useCallback( (stageType: string): boolean => uiaFlow.stages.includes(stageType), diff --git a/src/app/utils/matrix-uia.ts b/src/app/utils/matrix-uia.ts index 91ebf614a2..15c5799cf9 100644 --- a/src/app/utils/matrix-uia.ts +++ b/src/app/utils/matrix-uia.ts @@ -31,6 +31,13 @@ export const getUIAErrorCode = (authData: IAuthData): string | undefined => { return errorCode; }; +export const getUIAError = (authData: IAuthData): string | undefined => { + const errorCode = + 'error' in authData && typeof authData.error === 'string' ? authData.error : undefined; + + return errorCode; +}; + export const getUIAFlowForStages = (uiaFlows: UIAFlow[], stages: string[]): UIAFlow | undefined => { const matchedFlows = uiaFlows .filter((flow) => { From a3b1878c1c1b467f7110ae8809aaae10ea0da1d4 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:22:36 +0530 Subject: [PATCH 44/68] process register UIA flow stages --- package-lock.json | 10 + package.json | 1 + src/app/cs-errorcode.ts | 1 + src/app/pages/auth/login/Login.tsx | 1 - .../pages/auth/login/PasswordLoginForm.tsx | 5 +- .../auth/register/PasswordRegisterForm.tsx | 554 ++++++++++++++++-- src/app/pages/auth/register/registerUtil.ts | 6 + 7 files changed, 539 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 033a5465fd..fba4072e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "@types/prismjs": "1.26.0", "@types/react": "18.2.39", "@types/react-dom": "18.2.17", + "@types/react-google-recaptcha": "2.1.8", "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", @@ -2998,6 +2999,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-google-recaptcha": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.8.tgz", + "integrity": "sha512-nYI3ZDoteZ0g4FYusyKWqz7AZqRdu70R3wDkosCcN0peb2WLn57i0Alm4IPiCRIx59yTUVPTiOELZH08gV1wXA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", diff --git a/package.json b/package.json index dea1bd9408..56e7b8c01e 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@types/prismjs": "1.26.0", "@types/react": "18.2.39", "@types/react-dom": "18.2.17", + "@types/react-google-recaptcha": "2.1.8", "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", diff --git a/src/app/cs-errorcode.ts b/src/app/cs-errorcode.ts index 02f3ad40fb..6c21d670c4 100644 --- a/src/app/cs-errorcode.ts +++ b/src/app/cs-errorcode.ts @@ -14,6 +14,7 @@ export enum ErrorCode { M_USER_IN_USE = 'M_USER_IN_USE', M_INVALID_USERNAME = 'M_INVALID_USERNAME', M_WEAK_PASSWORD = 'M_WEAK_PASSWORD', + M_PASSWORD_TOO_SHORT = 'M_PASSWORD_TOO_SHORT', M_ROOM_IN_USE = 'M_ROOM_IN_USE', M_INVALID_ROOM_STATE = 'M_INVALID_ROOM_STATE', M_THREEPID_IN_USE = 'M_THREEPID_IN_USE', diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx index 687d9e87e4..79bc43c721 100644 --- a/src/app/pages/auth/login/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -30,7 +30,6 @@ export function Login() { const [ssoRedirectUrl] = window.location.href.split('?'); const parsedFlows = useParsedLoginFlows(loginFlows.flows); - console.log(parsedFlows); return ( diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx index 6d4a60525a..8fa71b0412 100644 --- a/src/app/pages/auth/login/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -91,8 +91,9 @@ function UsernameHint({ server }: { server: string }) { ref={targetRef} type="button" variant="Background" - size="400" + size="300" radii="300" + aria-pressed={open} > @@ -199,7 +200,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog { @@ -67,31 +86,459 @@ const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => { return pickedStages; }; +type ConfirmPasswordMatchProps = { + initialValue: boolean; + children: ( + match: boolean, + doMatch: () => void, + passRef: RefObject, + confPassRef: RefObject + ) => ReactNode; +}; +function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) { + const [match, setMatch] = useState(initialValue); + const passRef = useRef(null); + const confPassRef = useRef(null); + + const doMatch = useDebounce( + useCallback(() => { + const pass = passRef.current?.value; + const confPass = confPassRef.current?.value; + if (!confPass) { + setMatch(initialValue); + return; + } + setMatch(pass === confPass); + }, [initialValue]), + { + wait: 500, + immediate: false, + } + ); + + return children(match, doMatch, passRef, confPassRef); +} + +type StageComponentProps = { + stageData: AuthStageData; + submitAuthDict: (authDict: AuthDict) => void; +}; + +function TermsStage({ stageData, submitAuthDict }: StageComponentProps) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback( + () => + submitAuthDict({ + type: AuthType.Terms, + session, + }), + [session, submitAuthDict] + ); + + useEffect(() => { + if (session && !errorCode) { + handleSubmit(); + } + }, [session, errorCode, handleSubmit]); + + if (errorCode) { + return ( + + + + {errorCode} + {error ?? 'Failed to submit Terms and Condition Acceptance.'} + + + + + + ); + } + + return ; +} + +function ReCaptchaStage({ stageData, submitAuthDict }: StageComponentProps) { + const { info, session } = stageData; + + const publicKey = info?.public_key; + + const handleChange = (token: string | null) => { + if (!token) { + return; + } + submitAuthDict({ + type: AuthType.Recaptcha, + response: token, + session, + }); + }; + + if (typeof publicKey !== 'string' || !session) { + return ( + + + + Invalid Data + No valid data found to proceed with ReCAPTCHA. + + + + + ); + } + + return ; +} + +function EmailStage({ + mx, + email, + clientSecret, + stageData, + submitAuthDict, +}: StageComponentProps & { + email?: string; + clientSecret: string; + mx: MatrixClient; +}) { + const { errorCode, error, session } = stageData; + + const sendAttemptRef = useRef(1); + + const [verifyState, verify] = useAsyncCallback< + { + email: string; + result: IRequestTokenResponse; + }, + MatrixError, + [userEmail: string] + >( + useCallback( + async (userEmail) => { + const sendAttempt = sendAttemptRef.current; + sendAttemptRef.current += 1; + const result = await mx.requestRegisterEmailToken(userEmail, clientSecret, sendAttempt); + return { + email: userEmail, + result, + }; + }, + [clientSecret, mx] + ) + ); + + const handleSubmit = useCallback( + (sessionId: string) => { + const threepIDCreds = { + sid: sessionId, + client_secret: clientSecret, + }; + submitAuthDict({ + type: AuthType.Email, + threepid_creds: threepIDCreds, + threepidCreds: threepIDCreds, + session, + }); + }, + [submitAuthDict, session, clientSecret] + ); + + useEffect(() => { + if (email && !errorCode) verify(email); + }, [email, errorCode, verify]); + + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { retryEmailInput } = evt.target as HTMLFormElement & { + retryEmailInput: HTMLInputElement; + }; + const e = retryEmailInput.value; + verify(e); + }; + + if (verifyState.status === AsyncStatus.Loading) { + return ( + + + Sending verification email... + + ); + } + + if (errorCode || !email || verifyState.status === AsyncStatus.Error) { + const veifyErr = verifyState.status === AsyncStatus.Error ? verifyState.error : undefined; + return ( + + + + {veifyErr ? ( + <> + {veifyErr.errcode ?? 'Verify Email'} + + {veifyErr?.data?.error ?? + veifyErr.message ?? + 'Failed to send Email verification request.'} + + + ) : ( + <> + {errorCode ?? 'Provide Email'} + + {error ?? 'Please Enter you email address to send verification request.'} + + + )} + + Email + + + + + + + + ); + } + + if (verifyState.status === AsyncStatus.Success) { + return ( + + + + Verification Request Sent + {`Please check your email "${verifyState.data.email}" and validate before continuing further.`} + + + + + ); + } + + return ; +} + +function RegistrationTokenStage({ + token, + stageData, + submitAuthDict, +}: StageComponentProps & { + token?: string; +}) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback( + (t: string) => { + submitAuthDict({ + type: AuthType.RegistrationToken, + token: t, + session, + }); + }, + [session, submitAuthDict] + ); + + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { retryTokenInput } = evt.target as HTMLFormElement & { + retryTokenInput: HTMLInputElement; + }; + const t = retryTokenInput.value; + handleSubmit(t); + }; + + useEffect(() => { + if (token && !errorCode) handleSubmit(token); + }, [handleSubmit, token, errorCode]); + + if (errorCode || !token) { + return ( + + + + {errorCode ?? 'Request on Hold'} + {error ?? 'Invalid registration token provided.'} + + Registration Token + + + + + + + + ); + } + + return ; +} + +function DummyStage({ stageData, submitAuthDict }: StageComponentProps) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback(() => { + submitAuthDict({ + type: AuthType.Dummy, + session, + }); + }, [session, submitAuthDict]); + + useEffect(() => { + if (!errorCode) handleSubmit(); + }, [handleSubmit, errorCode]); + + if (errorCode) { + return ( + + + + {errorCode} + {error ?? 'Failed to submit final authentication request.'} + + + + + + ); + } + + return ; +} + type RegisterUIAFlowProps = { + mx: MatrixClient; formData: FormData; flow: UIAFlow; authData: IAuthData; onRegister: (registerReqData: RegisterRequest) => void; }; -function RegisterUIAFlow({ formData, flow, authData, onRegister }: RegisterUIAFlowProps) { +function RegisterUIAFlow({ mx, formData, flow, authData, onRegister }: RegisterUIAFlowProps) { const { getStageToComplete } = useUIAFlow(authData, flow); const stageToComplete = getStageToComplete(); - if (!stageToComplete) { - return

Completed

; - } + const handleAuthDict = useCallback( + (authDict: AuthDict) => { + const { password, username } = formData; + onRegister({ + auth: authDict, + password, + username, + initial_device_display_name: 'Cinny Web', + }); + }, + [onRegister, formData] + ); + if (!stageToComplete) return null; return ( }> {stageToComplete.type === AuthType.RegistrationToken && ( - + + )} + {stageToComplete.type === AuthType.Terms && ( + + )} + {stageToComplete.type === AuthType.Recaptcha && ( + + )} + {stageToComplete.type === AuthType.Email && ( + + )} + {stageToComplete.type === AuthType.Dummy && ( + )} - {stageToComplete.type === AuthType.Terms && } - {stageToComplete.type === AuthType.Recaptcha && } - {stageToComplete.type === AuthType.Email && } - {stageToComplete.type === AuthType.Dummy && } ); @@ -144,13 +591,12 @@ export function PasswordRegisterForm({ } = evt.target as HTMLFormElement & RegisterFormInputs; const token = tokenInput?.value.trim(); const username = usernameInput.value.trim(); - // TODO: check username availability const password = passwordInput.value; const confirmPassword = confirmPasswordInput.value; - // TODO: display password doesn't match error - if (password !== confirmPassword) return; + if (password !== confirmPassword) { + return; + } const email = emailInput?.value.trim(); - // TODO: verify email const terms = termsInput?.value === 'on'; if (!username) { @@ -164,6 +610,7 @@ export function PasswordRegisterForm({ token, email, terms, + clientSecret: mx.generateClientSecret(), }; const pickedStages = pickStages(uiaFlows, fData); const pickedFlow = getUIAFlowForStages(uiaFlows, pickedStages); @@ -204,27 +651,47 @@ export function PasswordRegisterForm({ )}
- - - Password - - - {registerError?.errcode === RegisterError.PasswordWeak && ( - + + {(match, doMatch, passRef, confPassRef) => ( + <> + + + Password + + + {registerError?.errcode === RegisterError.PasswordWeak && ( + + )} + {registerError?.errcode === RegisterError.PasswordShort && ( + + )} + + + + Confirm Password + + + + )} - - - - Confirm Password - - - + {hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && ( @@ -271,6 +738,18 @@ export function PasswordRegisterForm({ )} + {registerError?.errcode === RegisterError.RateLimited && ( + + )} + {registerError?.errcode === RegisterError.Forbidden && ( + + )} + {registerError?.errcode === RegisterError.InvalidRequest && ( + + )} + {registerError?.errcode === RegisterError.Unknown && ( + + )} + +
+ + ); +} + +export function AutoDummyStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback(() => { + submitAuthDict({ + type: AuthType.Dummy, + session, + }); + }, [session, submitAuthDict]); + + useEffect(() => { + if (!errorCode) handleSubmit(); + }, [handleSubmit, errorCode]); + + if (errorCode) { + return ( + + ); + } + + return null; +} diff --git a/src/app/components/uia-stages/EmailStage.tsx b/src/app/components/uia-stages/EmailStage.tsx new file mode 100644 index 0000000000..675d3ae687 --- /dev/null +++ b/src/app/components/uia-stages/EmailStage.tsx @@ -0,0 +1,175 @@ +import React, { useEffect, useCallback, FormEventHandler } from 'react'; +import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds'; +import { AuthType, MatrixError } from 'matrix-js-sdk'; +import { StageComponentProps } from './types'; +import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback'; +import { RegisterEmailCallback, RegisteredEmailResponse } from '../../hooks/useRegisterEmail'; + +function EmailErrorDialog({ + title, + message, + defaultEmail, + onRetry, + onCancel, +}: { + title: string; + message: string; + defaultEmail?: string; + onRetry: (email: string) => void; + onCancel: () => void; +}) { + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { retryEmailInput } = evt.target as HTMLFormElement & { + retryEmailInput: HTMLInputElement; + }; + const t = retryEmailInput.value; + onRetry(t); + }; + + return ( + + + + {title} + {message} + + Email + + + + + + + + ); +} + +export function EmailStageDialog({ + email, + clientSecret, + stageData, + registerEmailState, + registerEmail, + submitAuthDict, + onCancel, +}: StageComponentProps & { + email?: string; + clientSecret: string; + registerEmailState: AsyncState; + registerEmail: RegisterEmailCallback; +}) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback( + (sessionId: string) => { + const threepIDCreds = { + sid: sessionId, + client_secret: clientSecret, + }; + submitAuthDict({ + type: AuthType.Email, + threepid_creds: threepIDCreds, + threepidCreds: threepIDCreds, + session, + }); + }, + [submitAuthDict, session, clientSecret] + ); + + const handleEmailSubmit = useCallback( + (userEmail: string) => { + registerEmail(userEmail, clientSecret); + }, + [clientSecret, registerEmail] + ); + + useEffect(() => { + if (email && !errorCode && registerEmailState.status === AsyncStatus.Idle) { + registerEmail(email, clientSecret); + } + }, [email, errorCode, clientSecret, registerEmailState, registerEmail]); + + if (registerEmailState.status === AsyncStatus.Loading) { + return ( + + + Sending verification email... + + ); + } + + if (registerEmailState.status === AsyncStatus.Error) { + return ( + + ); + } + + if (registerEmailState.status === AsyncStatus.Success) { + return ( + + + + Verification Request Sent + {`Please check your email "${registerEmailState.data.email}" and validate before continuing further.`} + + {errorCode && ( + {`${errorCode}: ${error}`} + )} + + + + + ); + } + + if (!email) { + return ( + + ); + } + + return null; +} diff --git a/src/app/components/uia-stages/ReCaptchaStage.tsx b/src/app/components/uia-stages/ReCaptchaStage.tsx new file mode 100644 index 0000000000..68b3fcf484 --- /dev/null +++ b/src/app/components/uia-stages/ReCaptchaStage.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Dialog, Text, Box, Button, config } from 'folds'; +import { AuthType } from 'matrix-js-sdk'; +import ReCAPTCHA from 'react-google-recaptcha'; +import { StageComponentProps } from './types'; + +function ReCaptchaErrorDialog({ + title, + message, + onCancel, +}: { + title: string; + message: string; + onCancel: () => void; +}) { + return ( + + + + {title} + {message} + + + + + ); +} + +export function ReCaptchaStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) { + const { info, session } = stageData; + + const publicKey = info?.public_key; + + const handleChange = (token: string | null) => { + submitAuthDict({ + type: AuthType.Recaptcha, + response: token, + session, + }); + }; + + if (typeof publicKey !== 'string' || !session) { + return ( + + ); + } + + return ( + + + Please check the box below to proceed. + + + + ); +} diff --git a/src/app/components/uia-stages/RegistrationTokenStage.tsx b/src/app/components/uia-stages/RegistrationTokenStage.tsx new file mode 100644 index 0000000000..ed8a304586 --- /dev/null +++ b/src/app/components/uia-stages/RegistrationTokenStage.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useCallback, FormEventHandler } from 'react'; +import { Dialog, Text, Box, Button, config, Input } from 'folds'; +import { AuthType } from 'matrix-js-sdk'; +import { StageComponentProps } from './types'; + +function RegistrationTokenErrorDialog({ + title, + message, + defaultToken, + onRetry, + onCancel, +}: { + title: string; + message: string; + defaultToken?: string; + onRetry: (token: string) => void; + onCancel: () => void; +}) { + const handleFormSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { retryTokenInput } = evt.target as HTMLFormElement & { + retryTokenInput: HTMLInputElement; + }; + const t = retryTokenInput.value; + onRetry(t); + }; + + return ( + + + + {title} + {message} + + Registration Token + + + + + + + + ); +} + +export function RegistrationTokenStageDialog({ + token, + stageData, + submitAuthDict, + onCancel, +}: StageComponentProps & { + token?: string; +}) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback( + (t: string) => { + submitAuthDict({ + type: AuthType.RegistrationToken, + token: t, + session, + }); + }, + [session, submitAuthDict] + ); + + useEffect(() => { + if (token && !errorCode) handleSubmit(token); + }, [handleSubmit, token, errorCode]); + + if (errorCode) { + return ( + + ); + } + + if (!token) { + return ( + + ); + } + + return null; +} diff --git a/src/app/components/uia-stages/TermsStage.tsx b/src/app/components/uia-stages/TermsStage.tsx new file mode 100644 index 0000000000..f697705343 --- /dev/null +++ b/src/app/components/uia-stages/TermsStage.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useCallback } from 'react'; +import { Dialog, Text, Box, Button, config } from 'folds'; +import { AuthType } from 'matrix-js-sdk'; +import { StageComponentProps } from './types'; + +function TermsErrorDialog({ + title, + message, + onRetry, + onCancel, +}: { + title: string; + message: string; + onRetry: () => void; + onCancel: () => void; +}) { + return ( + + + + {title} + {message} + + + + + + ); +} + +export function AutoTermsStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) { + const { errorCode, error, session } = stageData; + + const handleSubmit = useCallback( + () => + submitAuthDict({ + type: AuthType.Terms, + session, + }), + [session, submitAuthDict] + ); + + useEffect(() => { + if (!errorCode) { + handleSubmit(); + } + }, [session, errorCode, handleSubmit]); + + if (errorCode) { + return ( + + ); + } + + return null; +} diff --git a/src/app/components/uia-stages/index.ts b/src/app/components/uia-stages/index.ts new file mode 100644 index 0000000000..95c19a79c4 --- /dev/null +++ b/src/app/components/uia-stages/index.ts @@ -0,0 +1,6 @@ +export * from './types'; +export * from './DummyStage'; +export * from './EmailStage'; +export * from './ReCaptchaStage'; +export * from './RegistrationTokenStage'; +export * from './TermsStage'; diff --git a/src/app/components/uia-stages/types.ts b/src/app/components/uia-stages/types.ts new file mode 100644 index 0000000000..cc6674c5e9 --- /dev/null +++ b/src/app/components/uia-stages/types.ts @@ -0,0 +1,8 @@ +import { AuthDict } from 'matrix-js-sdk'; +import { AuthStageData } from '../../hooks/useUIAFlows'; + +export type StageComponentProps = { + stageData: AuthStageData; + submitAuthDict: (authDict: AuthDict) => void; + onCancel: () => void; +}; diff --git a/src/app/hooks/useRegisterEmail.ts b/src/app/hooks/useRegisterEmail.ts new file mode 100644 index 0000000000..c81313ba2d --- /dev/null +++ b/src/app/hooks/useRegisterEmail.ts @@ -0,0 +1,39 @@ +import { IRequestTokenResponse, MatrixClient, MatrixError } from 'matrix-js-sdk'; +import { useCallback, useRef } from 'react'; +import { AsyncState, useAsyncCallback } from './useAsyncCallback'; + +export type RegisteredEmailResponse = { + email: string; + result: IRequestTokenResponse; +}; +export type RegisterEmailCallback = ( + email: string, + clientSecret: string, + nextLink?: string +) => Promise; +export const useRegisterEmail = ( + mx: MatrixClient +): [AsyncState, RegisterEmailCallback] => { + const sendAttemptRef = useRef(1); + + const registerEmailCallback: RegisterEmailCallback = useCallback( + async (email, clientSecret, nextLink) => { + const sendAttempt = sendAttemptRef.current; + sendAttemptRef.current += 1; + const result = await mx.requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink); + return { + email, + result, + }; + }, + [mx] + ); + + const [registerEmailState, registerEmail] = useAsyncCallback< + RegisteredEmailResponse, + MatrixError, + Parameters + >(registerEmailCallback); + + return [registerEmailState, registerEmail]; +}; diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx index 3af7ac8847..ec2ac6da4b 100644 --- a/src/app/pages/auth/register/PasswordRegisterForm.tsx +++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx @@ -2,7 +2,6 @@ import { Box, Button, Checkbox, - Dialog, Input, Overlay, OverlayBackdrop, @@ -10,15 +9,12 @@ import { Spinner, Text, color, - config, } from 'folds'; import React, { ChangeEventHandler, - FormEventHandler, ReactNode, RefObject, useCallback, - useEffect, useMemo, useRef, useState, @@ -27,14 +23,11 @@ import { AuthDict, AuthType, IAuthData, - IRequestTokenResponse, - MatrixClient, MatrixError, RegisterRequest, UIAFlow, createClient, } from 'matrix-js-sdk'; -import ReCAPTCHA from 'react-google-recaptcha'; import { PasswordInput } from '../../../components/password-input/PasswordInput'; import { getLoginTermUrl, @@ -42,12 +35,24 @@ import { hasStageInFlows, requiredStageInFlows, } from '../../../utils/matrix-uia'; -import { AuthStageData, useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows'; -import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows'; +import { AsyncState, AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; import { RegisterError, RegisterResult, register, useRegisterComplete } from './registerUtil'; import { FieldError } from '../FiledError'; import { useDebounce } from '../../../hooks/useDebounce'; +import { + AutoDummyStageDialog, + AutoTermsStageDialog, + EmailStageDialog, + ReCaptchaStageDialog, + RegistrationTokenStageDialog, +} from '../../../components/uia-stages'; +import { + RegisterEmailCallback, + RegisteredEmailResponse, + useRegisterEmail, +} from '../../../hooks/useRegisterEmail'; export const SUPPORTED_REGISTER_STAGES = [ AuthType.RegistrationToken, @@ -119,380 +124,22 @@ function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchPr return children(match, doMatch, passRef, confPassRef); } -type StageComponentProps = { - stageData: AuthStageData; - submitAuthDict: (authDict: AuthDict) => void; -}; - -function TermsStage({ stageData, submitAuthDict }: StageComponentProps) { - const { errorCode, error, session } = stageData; - - const handleSubmit = useCallback( - () => - submitAuthDict({ - type: AuthType.Terms, - session, - }), - [session, submitAuthDict] - ); - - useEffect(() => { - if (session && !errorCode) { - handleSubmit(); - } - }, [session, errorCode, handleSubmit]); - - if (errorCode) { - return ( - - - - {errorCode} - {error ?? 'Failed to submit Terms and Condition Acceptance.'} - - - - - - ); - } - - return ; -} - -function ReCaptchaStage({ stageData, submitAuthDict }: StageComponentProps) { - const { info, session } = stageData; - - const publicKey = info?.public_key; - - const handleChange = (token: string | null) => { - if (!token) { - return; - } - submitAuthDict({ - type: AuthType.Recaptcha, - response: token, - session, - }); - }; - - if (typeof publicKey !== 'string' || !session) { - return ( - - - - Invalid Data - No valid data found to proceed with ReCAPTCHA. - - - - - ); - } - - return ; -} - -function EmailStage({ - mx, - email, - clientSecret, - stageData, - submitAuthDict, -}: StageComponentProps & { - email?: string; - clientSecret: string; - mx: MatrixClient; -}) { - const { errorCode, error, session } = stageData; - - const sendAttemptRef = useRef(1); - - const [verifyState, verify] = useAsyncCallback< - { - email: string; - result: IRequestTokenResponse; - }, - MatrixError, - [userEmail: string] - >( - useCallback( - async (userEmail) => { - const sendAttempt = sendAttemptRef.current; - sendAttemptRef.current += 1; - const result = await mx.requestRegisterEmailToken(userEmail, clientSecret, sendAttempt); - return { - email: userEmail, - result, - }; - }, - [clientSecret, mx] - ) - ); - - const handleSubmit = useCallback( - (sessionId: string) => { - const threepIDCreds = { - sid: sessionId, - client_secret: clientSecret, - }; - submitAuthDict({ - type: AuthType.Email, - threepid_creds: threepIDCreds, - threepidCreds: threepIDCreds, - session, - }); - }, - [submitAuthDict, session, clientSecret] - ); - - useEffect(() => { - if (email && !errorCode) verify(email); - }, [email, errorCode, verify]); - - const handleFormSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - const { retryEmailInput } = evt.target as HTMLFormElement & { - retryEmailInput: HTMLInputElement; - }; - const e = retryEmailInput.value; - verify(e); - }; - - if (verifyState.status === AsyncStatus.Loading) { - return ( - - - Sending verification email... - - ); - } - - if (errorCode || !email || verifyState.status === AsyncStatus.Error) { - const veifyErr = verifyState.status === AsyncStatus.Error ? verifyState.error : undefined; - return ( - - - - {veifyErr ? ( - <> - {veifyErr.errcode ?? 'Verify Email'} - - {veifyErr?.data?.error ?? - veifyErr.message ?? - 'Failed to send Email verification request.'} - - - ) : ( - <> - {errorCode ?? 'Provide Email'} - - {error ?? 'Please Enter you email address to send verification request.'} - - - )} - - Email - - - - - - - - ); - } - - if (verifyState.status === AsyncStatus.Success) { - return ( - - - - Verification Request Sent - {`Please check your email "${verifyState.data.email}" and validate before continuing further.`} - - - - - ); - } - - return ; -} - -function RegistrationTokenStage({ - token, - stageData, - submitAuthDict, -}: StageComponentProps & { - token?: string; -}) { - const { errorCode, error, session } = stageData; - - const handleSubmit = useCallback( - (t: string) => { - submitAuthDict({ - type: AuthType.RegistrationToken, - token: t, - session, - }); - }, - [session, submitAuthDict] - ); - - const handleFormSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - const { retryTokenInput } = evt.target as HTMLFormElement & { - retryTokenInput: HTMLInputElement; - }; - const t = retryTokenInput.value; - handleSubmit(t); - }; - - useEffect(() => { - if (token && !errorCode) handleSubmit(token); - }, [handleSubmit, token, errorCode]); - - if (errorCode || !token) { - return ( - - - - {errorCode ?? 'Request on Hold'} - {error ?? 'Invalid registration token provided.'} - - Registration Token - - - - - - - - ); - } - - return ; -} - -function DummyStage({ stageData, submitAuthDict }: StageComponentProps) { - const { errorCode, error, session } = stageData; - - const handleSubmit = useCallback(() => { - submitAuthDict({ - type: AuthType.Dummy, - session, - }); - }, [session, submitAuthDict]); - - useEffect(() => { - if (!errorCode) handleSubmit(); - }, [handleSubmit, errorCode]); - - if (errorCode) { - return ( - - - - {errorCode} - {error ?? 'Failed to submit final authentication request.'} - - - - - - ); - } - - return ; -} - type RegisterUIAFlowProps = { - mx: MatrixClient; formData: FormData; flow: UIAFlow; authData: IAuthData; + registerEmailState: AsyncState; + registerEmail: RegisterEmailCallback; onRegister: (registerReqData: RegisterRequest) => void; }; -function RegisterUIAFlow({ mx, formData, flow, authData, onRegister }: RegisterUIAFlowProps) { +function RegisterUIAFlow({ + formData, + flow, + authData, + registerEmailState, + registerEmail, + onRegister, +}: RegisterUIAFlowProps) { const { getStageToComplete } = useUIAFlow(authData, flow); const stageToComplete = getStageToComplete(); @@ -510,34 +157,53 @@ function RegisterUIAFlow({ mx, formData, flow, authData, onRegister }: RegisterU [onRegister, formData] ); + const handleChange = useCallback(() => { + window.location.reload(); + }, []); + if (!stageToComplete) return null; return ( }> {stageToComplete.type === AuthType.RegistrationToken && ( - )} {stageToComplete.type === AuthType.Terms && ( - + )} {stageToComplete.type === AuthType.Recaptcha && ( - + )} {stageToComplete.type === AuthType.Email && ( - )} {stageToComplete.type === AuthType.Dummy && ( - + )} @@ -567,6 +233,8 @@ export function PasswordRegisterForm({ const [ongoingFlow, setOngoingFlow] = useState(); + const [registerEmailState, registerEmail] = useRegisterEmail(mx); + const [registerState, handleRegister] = useAsyncCallback< RegisterResult, MatrixError, @@ -762,10 +430,11 @@ export function PasswordRegisterForm({ ongoingFlow && ongoingAuthData && ( )} From 22eb46ea251702a71cb5a63b24c5f2c79bd38c06 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:01:40 +0530 Subject: [PATCH 46/68] improve register error messages --- .../pages/auth/register/PasswordRegisterForm.tsx | 16 +++++++++++++--- src/app/pages/auth/register/registerUtil.ts | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx index ec2ac6da4b..aae95d21b6 100644 --- a/src/app/pages/auth/register/PasswordRegisterForm.tsx +++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx @@ -336,10 +336,20 @@ export function PasswordRegisterForm({ required /> {registerError?.errcode === RegisterError.PasswordWeak && ( - + )} {registerError?.errcode === RegisterError.PasswordShort && ( - + )}
@@ -416,7 +426,7 @@ export function PasswordRegisterForm({ )} {registerError?.errcode === RegisterError.Unknown && ( - + )} + + + + + + ); +} + +type PasswordResetFormProps = { + defaultEmail?: string; +}; +export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) { + const server = useAuthServer(); + + const serverDiscovery = useAutoDiscoveryInfo(); + const baseUrl = serverDiscovery['m.homeserver'].base_url; + const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); + + const [formData, setFormData] = useState(); + + const [passwordEmailState, passwordEmail] = usePasswordEmail(mx); + + const [resetPasswordState, handleResetPassword] = useAsyncCallback< + ResetPasswordResult, + MatrixError, + [AuthDict, string] + >(useCallback(async (authDict, newPassword) => resetPassword(mx, authDict, newPassword), [mx])); + + const [ongoingAuthData, resetPasswordResult] = + resetPasswordState.status === AsyncStatus.Success ? resetPasswordState.data : []; + const resetPasswordError = + resetPasswordState.status === AsyncStatus.Error ? resetPasswordState.error : undefined; + + const flowErrorCode = ongoingAuthData && getUIAErrorCode(ongoingAuthData); + const flowError = ongoingAuthData && getUIAError(ongoingAuthData); + + let waitingToVerifyEmail = true; + if (resetPasswordResult) waitingToVerifyEmail = false; + if (ongoingAuthData && flowErrorCode === undefined) waitingToVerifyEmail = false; + if (resetPasswordError) waitingToVerifyEmail = false; + if (resetPasswordState.status === AsyncStatus.Loading) waitingToVerifyEmail = false; + + // We only support UIA m.login.password stage for reset password + // So we will assume to process it as soon as + // we have 401 with no error on initial request. + useEffect(() => { + if (formData && ongoingAuthData && !flowErrorCode) { + handleResetPassword( + { + type: AuthType.Password, + identifier: { + type: 'm.id.thirdparty', + medium: 'email', + address: formData.email, + }, + password: formData.password, + }, + formData.password + ); + } + }, [ongoingAuthData, flowErrorCode, formData, handleResetPassword]); + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const { emailInput, passwordInput, confirmPasswordInput } = evt.target as HTMLFormElement & { + emailInput: HTMLInputElement; + passwordInput: HTMLInputElement; + confirmPasswordInput: HTMLInputElement; + }; + + const email = emailInput.value.trim(); + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + if (!email) { + emailInput.focus(); + return; + } + if (password !== confirmPassword) return; + + const clientSecret = mx.generateClientSecret(); + passwordEmail(email, clientSecret); + setFormData({ + email, + password, + clientSecret, + }); + }; + + const handleCancel = () => { + window.location.reload(); + }; + + const handleSubmitRequest = useCallback( + (authDict: AuthDict) => { + if (!formData) return; + const { password } = formData; + handleResetPassword(authDict, password); + }, + [formData, handleResetPassword] + ); + + return ( + + + Homeserver {server} will send you an email to let you reset your password. + + + + Email + + + {passwordEmailState.status === AsyncStatus.Error && ( + + )} + + + {(match, doMatch, passRef, confPassRef) => ( + <> + + + New Password + + + + + + Confirm Password + + + + + )} + + {resetPasswordError && ( + + )} + + + + {resetPasswordResult && } + + {passwordEmailState.status === AsyncStatus.Success && formData && waitingToVerifyEmail && ( + + + + )} + + } + > + + + + + + ); +} diff --git a/src/app/pages/auth/reset-password/ResetPassword.tsx b/src/app/pages/auth/reset-password/ResetPassword.tsx index f38e8bbff4..f8d54b20aa 100644 --- a/src/app/pages/auth/reset-password/ResetPassword.tsx +++ b/src/app/pages/auth/reset-password/ResetPassword.tsx @@ -1,19 +1,35 @@ import { Box, Text } from 'folds'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { getLoginPath } from '../../pathUtils'; import { useAuthServer } from '../../../hooks/useAuthServer'; +import { PasswordResetForm } from './PasswordResetForm'; + +export type ResetPasswordSearchParams = { + email?: string; +}; + +const getResetPasswordSearchParams = ( + searchParams: URLSearchParams +): ResetPasswordSearchParams => ({ + email: searchParams.get('email') ?? undefined, +}); export function ResetPassword() { const server = useAuthServer(); + const [searchParams] = useSearchParams(); + const resetPasswordSearchParams = getResetPasswordSearchParams(searchParams); return ( Reset Password + + + - Know you password? Login + Remember you password? Login ); diff --git a/src/app/pages/auth/reset-password/resetPasswordUtil.ts b/src/app/pages/auth/reset-password/resetPasswordUtil.ts new file mode 100644 index 0000000000..5eb436fa4a --- /dev/null +++ b/src/app/pages/auth/reset-password/resetPasswordUtil.ts @@ -0,0 +1,23 @@ +import to from 'await-to-js'; +import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk'; + +export type ResetPasswordResponse = Record; +export type ResetPasswordResult = [IAuthData, undefined] | [undefined, ResetPasswordResponse]; +export const resetPassword = async ( + mx: MatrixClient, + authDict: AuthDict, + newPassword: string +): Promise => { + const [err, res] = await to( + mx.setPassword(authDict, newPassword, false) + ); + + if (err) { + if (err.httpStatus === 401) { + const authData = err.data as IAuthData; + return [authData, undefined]; + } + throw err; + } + return [undefined, res]; +}; diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index 20745a63fd..db39ce3916 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -1,6 +1,15 @@ import { generatePath } from 'react-router-dom'; import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths'; +export const withSearchParam = >( + path: string, + searchParam: T +): string => { + const params = new URLSearchParams(searchParam); + + return `${path}?${params}`; +}; + export const getRootPath = (): string => ROOT_PATH; export const getLoginPath = (server?: string): string => { diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index df6079e132..cd6226414b 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -1,5 +1,17 @@ export const ROOT_PATH = '/'; +export type LoginPathSearchParams = { + username?: string; + email?: string; + loginToken?: string; +}; export const LOGIN_PATH = '/login/:server?/'; + +export type RegisterPathSearchParams = { + username?: string; + email?: string; + token?: string; +}; export const REGISTER_PATH = '/register/:server?/'; + export const RESET_PASSWORD_PATH = '/reset-password/:server?/'; From a6dc96aa805d16f010549b0b06d65f175d8ca0e9 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:40:13 +0530 Subject: [PATCH 55/68] add netlify rewrites --- _redirects | 3 --- netlify.toml | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 _redirects create mode 100644 netlify.toml diff --git a/_redirects b/_redirects deleted file mode 100644 index 270cd33862..0000000000 --- a/_redirects +++ /dev/null @@ -1,3 +0,0 @@ -# Redirects from what the browser requests to what we serve -/login / -/register / diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000000..262b25f0e1 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[[redirects]] +from = "/*" +to = "/" +status = 200 \ No newline at end of file From aacbcc82faf1a3412e9a38c90b4829b54acc745a Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:49:18 +0530 Subject: [PATCH 56/68] fix netlify file indentation --- netlify.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netlify.toml b/netlify.toml index 262b25f0e1..1cb2010f3e 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,4 @@ [[redirects]] -from = "/*" -to = "/" -status = 200 \ No newline at end of file + from = "/*" + to = "/" + status = 200 From a66c31e2ca50717aec3257937f7e1a6b6615fdf1 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:52:23 +0530 Subject: [PATCH 57/68] test netlify redirect --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 1cb2010f3e..b87b8d3dda 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,4 @@ [[redirects]] from = "/*" - to = "/" + to = "/index.html" status = 200 From c2c8c027e414e4010db36da038a5f4d1a2a2e3f7 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:57:24 +0530 Subject: [PATCH 58/68] fix vite to include netlify toml --- vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index 8357339842..2657e5b63f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -18,7 +18,7 @@ const copyFiles = { dest: '', }, { - src: '_redirects', + src: 'netlify.toml', dest: '', }, { From c7d90f1bd3b44283aa2d8ed0ea4d19b0400cdc5b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:07:24 +0530 Subject: [PATCH 59/68] add more netlify redirects --- netlify.toml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index b87b8d3dda..c2c13447e2 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,4 +1,34 @@ +[[redirects]] + from = "/config.json" + to = "/config.json" + status = 200 + +[[redirects]] + from = "/manifest.json" + to = "/manifest.json" + status = 200 + +[[redirects]] + from = "/olm.wasm" + to = "/olm.wasm" + status = 200 + +[[redirects]] + from = "/pdf.worker.min.js" + to = "/pdf.worker.min.js" + status = 200 + +[[redirects]] + from = "/public/*" + to = "/public/" + status = 200 + +[[redirects]] + from = "/assets/*" + to = "/assets/" + status = 200 + [[redirects]] from = "/*" to = "/index.html" - status = 200 + status = 200 \ No newline at end of file From 3a0461617744fcf0ba4e668b8ce784746281a487 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:11:24 +0530 Subject: [PATCH 60/68] add splat to public and assets path --- netlify.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netlify.toml b/netlify.toml index c2c13447e2..e7d948e62f 100644 --- a/netlify.toml +++ b/netlify.toml @@ -20,12 +20,12 @@ [[redirects]] from = "/public/*" - to = "/public/" + to = "/public/:splat" status = 200 [[redirects]] from = "/assets/*" - to = "/assets/" + to = "/assets/:splat" status = 200 [[redirects]] From fdf252119294326ca1ca11a121ba93acf9d3b4fd Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:23:12 +0530 Subject: [PATCH 61/68] fix vite base name --- vite.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index 2657e5b63f..84b5f2cfab 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,6 +6,7 @@ import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import inject from '@rollup/plugin-inject'; import { svgLoader } from './viteSvgLoader'; +import config from './config.json' const copyFiles = { targets: [ @@ -39,7 +40,7 @@ const copyFiles = { export default defineConfig({ appType: 'spa', publicDir: false, - base: "", + base: config.basename ?? "/", server: { port: 8080, host: true, From 845eebe98af4405bb0840b74d538b5c64522a676 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:58:22 +0530 Subject: [PATCH 62/68] add option to use hash router in config and remove appVersion --- build.config.ts | 3 + config.json | 9 ++- src/app/components/ClientConfigLoader.tsx | 4 +- src/app/hooks/useClientConfig.ts | 7 ++- src/app/hooks/usePathWithOrigin.ts | 25 ++++----- src/app/pages/App.tsx | 67 ++++++++++++----------- src/app/pages/auth/AuthFooter.tsx | 5 +- src/app/utils/common.ts | 12 ++-- src/ext.d.ts | 2 + tsconfig.json | 1 + vite.config.js | 6 +- 11 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 build.config.ts diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000000..ec8a41d087 --- /dev/null +++ b/build.config.ts @@ -0,0 +1,3 @@ +export default { + base: '/', +}; diff --git a/config.json b/config.json index 2386a1f690..484c7cd783 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,4 @@ { - "appVersion": "3.2.0", - "basename": "/", "defaultHomeserver": 2, "homeserverList": [ "converser.eu", @@ -10,5 +8,10 @@ "mozilla.org", "xmr.se" ], - "allowCustomHomeservers": true + "allowCustomHomeservers": true, + + "hashRouter": { + "enabled": false, + "basename": "/" + } } diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx index 43b7812f71..99acd6d647 100644 --- a/src/app/components/ClientConfigLoader.tsx +++ b/src/app/components/ClientConfigLoader.tsx @@ -1,9 +1,11 @@ import { ReactNode, useEffect } from 'react'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { ClientConfig } from '../hooks/useClientConfig'; +import { trimTrailingSlash } from '../utils/common'; const getClientConfig = async (): Promise => { - const config = await fetch('/config.json', { method: 'GET' }); + const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`; + const config = await fetch(url, { method: 'GET' }); return config.json(); }; diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index ebaca07dd4..8406668ded 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -1,11 +1,14 @@ import { createContext, useContext } from 'react'; export type ClientConfig = { - appVersion?: string; - basename?: string; defaultHomeserver?: number; homeserverList?: string[]; allowCustomHomeservers?: boolean; + + hashRouter?: { + enabled?: boolean; + basename?: string; + }; }; const ClientConfigContext = createContext(null); diff --git a/src/app/hooks/usePathWithOrigin.ts b/src/app/hooks/usePathWithOrigin.ts index 57945e677b..4430d06c01 100644 --- a/src/app/hooks/usePathWithOrigin.ts +++ b/src/app/hooks/usePathWithOrigin.ts @@ -1,27 +1,26 @@ import { useMemo } from 'react'; import { useClientConfig } from './useClientConfig'; - -const START_SLASHES_REG = /^\/+/g; -const END_SLASHES_REG = /\/+$/g; -const trimStartSlash = (str: string): string => str.replace(START_SLASHES_REG, ''); -const trimEndSlash = (str: string): string => str.replace(END_SLASHES_REG, ''); - -const trimSlash = (str: string): string => trimStartSlash(trimEndSlash(str)); +import { trimLeadingSlash, trimSlash, trimTrailingSlash } from '../utils/common'; export const usePathWithOrigin = (path: string): string => { - const clientConfig = useClientConfig(); - const basename = clientConfig.basename ?? ''; + const { hashRouter } = useClientConfig(); const { origin } = window.location; const pathWithOrigin = useMemo(() => { let url: string = trimSlash(origin); - url += `/${trimSlash(basename)}`; - url = trimEndSlash(url); - url += `/${trimStartSlash(path)}`; + url += `/${trimSlash(import.meta.env.BASE_URL ?? '')}`; + url = trimTrailingSlash(url); + + if (hashRouter?.enabled) { + url += `/#/${trimSlash(hashRouter.basename ?? '')}`; + url = trimTrailingSlash(url); + } + + url += `/${trimLeadingSlash(path)}`; return url; - }, [path, basename, origin]); + }, [path, hashRouter, origin]); return pathWithOrigin; }; diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 7a3c3295bb..bb113ec04a 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -4,6 +4,7 @@ import { Route, RouterProvider, createBrowserRouter, + createHashRouter, createRoutesFromElements, redirect, } from 'react-router-dom'; @@ -17,40 +18,44 @@ import Client from '../templates/client/Client'; import { getLoginPath } from './pathUtils'; const createRouter = (clientConfig: ClientConfig) => { - const { basename } = clientConfig; - const router = createBrowserRouter( - createRoutesFromElements( - - { - if (isAuthenticated()) return redirect('/home'); - return redirect(getLoginPath()); - }} - /> - }> - } /> - } /> - } /> - + const { hashRouter } = clientConfig; - { - if (!isAuthenticated()) return redirect(getLoginPath()); - return null; - }} - > - } /> - direct

} /> - :spaceIdOrAlias

} /> - explore

} /> -
- Page not found

} /> + const routes = createRoutesFromElements( + + { + if (isAuthenticated()) return redirect('/home'); + return redirect(getLoginPath()); + }} + /> + }> + } /> + } /> + } /> - ), - { basename } + + { + if (!isAuthenticated()) return redirect(getLoginPath()); + return null; + }} + > + } /> + direct

} /> + :spaceIdOrAlias

} /> + explore

} /> +
+ Page not found

} /> +
); - return router; + + if (hashRouter?.enabled) { + return createHashRouter(routes, { basename: hashRouter.basename }); + } + return createBrowserRouter(routes, { + basename: import.meta.env.BASE_URL, + }); }; // TODO: app crash boundary diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index f2367dfd8d..6454161831 100644 --- a/src/app/pages/auth/AuthFooter.tsx +++ b/src/app/pages/auth/AuthFooter.tsx @@ -1,11 +1,8 @@ import React from 'react'; import { Box, Text } from 'folds'; import * as css from './styles.css'; -import { useClientConfig } from '../../hooks/useClientConfig'; export function AuthFooter() { - const { appVersion } = useClientConfig(); - return ( @@ -18,7 +15,7 @@ export function AuthFooter() { target="_blank" rel="noreferrer" > - {`v${appVersion ?? '0.0.0'}`} + v3.2.0 Twitter diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index 9bb597d60e..5cbe3806b4 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -89,9 +89,9 @@ export const parseGeoUri = (location: string) => { }; }; -export const trimTrailingSlash = (str: string) => { - if (str.endsWith('/')) { - return str.slice(0, str.length - 1); - } - return str; -}; +const START_SLASHES_REG = /^\/+/g; +const END_SLASHES_REG = /\/+$/g; +export const trimLeadingSlash = (str: string): string => str.replace(START_SLASHES_REG, ''); +export const trimTrailingSlash = (str: string): string => str.replace(END_SLASHES_REG, ''); + +export const trimSlash = (str: string): string => trimLeadingSlash(trimTrailingSlash(str)); diff --git a/src/ext.d.ts b/src/ext.d.ts index 27ff11cfb1..72acc587d7 100644 --- a/src/ext.d.ts +++ b/src/ext.d.ts @@ -1,3 +1,5 @@ +/// + declare module 'browser-encrypt-attachment' { export interface EncryptedAttachmentInfo { v: string; diff --git a/tsconfig.json b/tsconfig.json index d2f1e8a108..60ff185386 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "strict": true, "esModuleInterop": true, "moduleResolution": "Node", + "resolveJsonModule": true, "outDir": "dist", "skipLibCheck": true }, diff --git a/vite.config.js b/vite.config.js index 84b5f2cfab..20c7765c58 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,8 +5,8 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'; import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"; import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import inject from '@rollup/plugin-inject'; -import { svgLoader } from './viteSvgLoader'; -import config from './config.json' +import { svgLoader } from './viteSvgLoader' +import buildConfig from "./build.config" const copyFiles = { targets: [ @@ -40,7 +40,7 @@ const copyFiles = { export default defineConfig({ appType: 'spa', publicDir: false, - base: config.basename ?? "/", + base: buildConfig.base, server: { port: 8080, host: true, From 3a087855cf28ce1009affb6558fea688374081ce Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:37:47 +0530 Subject: [PATCH 63/68] add splash screen component --- .../splash-screen/SplashScreen.css.ts | 12 ++++++++ .../components/splash-screen/SplashScreen.tsx | 29 +++++++++++++++++++ src/app/components/splash-screen/index.ts | 1 + 3 files changed, 42 insertions(+) create mode 100644 src/app/components/splash-screen/SplashScreen.css.ts create mode 100644 src/app/components/splash-screen/SplashScreen.tsx create mode 100644 src/app/components/splash-screen/index.ts diff --git a/src/app/components/splash-screen/SplashScreen.css.ts b/src/app/components/splash-screen/SplashScreen.css.ts new file mode 100644 index 0000000000..bd3c300a72 --- /dev/null +++ b/src/app/components/splash-screen/SplashScreen.css.ts @@ -0,0 +1,12 @@ +import { style } from '@vanilla-extract/css'; +import { color, config } from 'folds'; + +export const SplashScreen = style({ + minHeight: '100%', + backgroundColor: color.Background.Container, + color: color.Background.OnContainer, +}); + +export const SplashScreenFooter = style({ + padding: config.space.S400, +}); diff --git a/src/app/components/splash-screen/SplashScreen.tsx b/src/app/components/splash-screen/SplashScreen.tsx new file mode 100644 index 0000000000..27adadbade --- /dev/null +++ b/src/app/components/splash-screen/SplashScreen.tsx @@ -0,0 +1,29 @@ +import { Box, Text } from 'folds'; +import React, { ReactNode } from 'react'; +import classNames from 'classnames'; +import * as patternsCSS from '../../styles/Patterns.css'; +import * as css from './SplashScreen.css'; + +type SplashScreenProps = { + children: ReactNode; +}; +export function SplashScreen({ children }: SplashScreenProps) { + return ( + + {children} + + + Cinny + + + + ); +} diff --git a/src/app/components/splash-screen/index.ts b/src/app/components/splash-screen/index.ts new file mode 100644 index 0000000000..e3e5dd34d7 --- /dev/null +++ b/src/app/components/splash-screen/index.ts @@ -0,0 +1 @@ +export * from './SplashScreen'; From 59483cfed4f583fef011856ce9641d8f234469d0 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:38:12 +0530 Subject: [PATCH 64/68] add client config loading and error screen --- src/app/components/ClientConfigLoader.tsx | 12 ++++- src/app/pages/App.tsx | 9 +++- src/app/pages/ConfigConfig.tsx | 53 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/app/pages/ConfigConfig.tsx diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx index 99acd6d647..72d367c067 100644 --- a/src/app/components/ClientConfigLoader.tsx +++ b/src/app/components/ClientConfigLoader.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; import { ClientConfig } from '../hooks/useClientConfig'; import { trimTrailingSlash } from '../utils/common'; @@ -11,10 +11,14 @@ const getClientConfig = async (): Promise => { type ClientConfigLoaderProps = { fallback?: () => ReactNode; + error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode; children: (config: ClientConfig) => ReactNode; }; -export function ClientConfigLoader({ fallback, children }: ClientConfigLoaderProps) { +export function ClientConfigLoader({ fallback, error, children }: ClientConfigLoaderProps) { const [state, load] = useAsyncCallback(getClientConfig); + const [ignoreError, setIgnoreError] = useState(false); + + const ignoreCallback = useCallback(() => setIgnoreError(true), []); useEffect(() => { load(); @@ -24,6 +28,10 @@ export function ClientConfigLoader({ fallback, children }: ClientConfigLoaderPro return fallback?.(); } + if (!ignoreError && state.status === AsyncStatus.Error) { + return error?.(state.error, load, ignoreCallback); + } + const config: ClientConfig = state.status === AsyncStatus.Success ? state.data : {}; return children(config); diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index bb113ec04a..6cefe999cb 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -16,6 +16,7 @@ import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './pat import { isAuthenticated } from '../../client/state/auth'; import Client from '../templates/client/Client'; import { getLoginPath } from './pathUtils'; +import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig'; const createRouter = (clientConfig: ClientConfig) => { const { hashRouter } = clientConfig; @@ -61,8 +62,12 @@ const createRouter = (clientConfig: ClientConfig) => { // TODO: app crash boundary function App() { return ( - // TODO: initial loading screen -

loading

}> + } + error={(err, retry, ignore) => ( + + )} + > {(clientConfig) => ( diff --git a/src/app/pages/ConfigConfig.tsx b/src/app/pages/ConfigConfig.tsx new file mode 100644 index 0000000000..dbcdca7363 --- /dev/null +++ b/src/app/pages/ConfigConfig.tsx @@ -0,0 +1,53 @@ +import { Box, Button, Dialog, Spinner, Text, color, config } from 'folds'; +import React from 'react'; +import { SplashScreen } from '../components/splash-screen'; + +export function ConfigConfigLoading() { + return ( + + + + Heating up + + + ); +} + +type ConfigConfigErrorProps = { + error: unknown; + retry: () => void; + ignore: () => void; +}; +export function ConfigConfigError({ error, retry, ignore }: ConfigConfigErrorProps) { + return ( + + + + + + Failed to load client configuration file. + {typeof error === 'object' && + error && + 'message' in error && + typeof error.message === 'string' && ( + + {error.message} + + )} + + + + + + + + ); +} From 5da9043bea1ae3035d8c57fc0d00350482becfeb Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 21 Jan 2024 10:10:17 +0530 Subject: [PATCH 65/68] fix server picker bug --- src/app/pages/auth/ServerPicker.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/pages/auth/ServerPicker.tsx b/src/app/pages/auth/ServerPicker.tsx index 9013a8fcb6..5f5dcf65c7 100644 --- a/src/app/pages/auth/ServerPicker.tsx +++ b/src/app/pages/auth/ServerPicker.tsx @@ -2,6 +2,7 @@ import React, { ChangeEventHandler, KeyboardEventHandler, MouseEventHandler, + useEffect, useRef, useState, } from 'react'; @@ -35,9 +36,12 @@ export function ServerPicker({ const [serverMenu, setServerMenu] = useState(false); const serverInputRef = useRef(null); - if (serverInputRef.current && serverInputRef.current.value !== server) { - serverInputRef.current.value = server; - } + useEffect(() => { + // sync input with it outside server changes + if (serverInputRef.current && serverInputRef.current.value !== server) { + serverInputRef.current.value = server; + } + }, [server]); const debounceServerSelect = useDebounce(onServerChange, { wait: 700 }); @@ -54,7 +58,7 @@ export function ServerPicker({ if (evt.key === 'Enter') { evt.preventDefault(); const inputServer = evt.currentTarget.value.trim(); - if (inputServer) debounceServerSelect(inputServer); + if (inputServer) onServerChange(inputServer); } }; From 8a8d7d4d873150007fd07b07fdadeb6040166e80 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 21 Jan 2024 10:16:52 +0530 Subject: [PATCH 66/68] fix reset password email input type --- src/app/pages/auth/reset-password/PasswordResetForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/auth/reset-password/PasswordResetForm.tsx b/src/app/pages/auth/reset-password/PasswordResetForm.tsx index 7d5b6ae403..7c71de021d 100644 --- a/src/app/pages/auth/reset-password/PasswordResetForm.tsx +++ b/src/app/pages/auth/reset-password/PasswordResetForm.tsx @@ -175,6 +175,7 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
Date: Sun, 21 Jan 2024 10:38:32 +0530 Subject: [PATCH 67/68] make auth page small screen responsive --- src/app/pages/auth/styles.css.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts index 5d2369b22e..5834ad8493 100644 --- a/src/app/pages/auth/styles.css.ts +++ b/src/app/pages/auth/styles.css.ts @@ -13,7 +13,7 @@ export const AuthLayout = style({ export const AuthCard = style({ marginTop: '1vh', - maxWidth: config.size.ModalWidth300, + maxWidth: toRem(460), width: '100%', backgroundColor: color.Surface.Container, color: color.Surface.OnContainer, @@ -39,8 +39,12 @@ export const AuthHeader = style({ }); export const AuthCardContent = style({ - padding: toRem(44), + maxWidth: toRem(402), + width: '100%', + margin: 'auto', + padding: config.space.S400, paddingTop: config.space.S700, + paddingBottom: toRem(44), gap: toRem(44), }); From f9d0407074191d7e8f4421b1d88d57175290cd51 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:48:32 +0530 Subject: [PATCH 68/68] fix typo in reset password screen --- src/app/pages/auth/reset-password/ResetPassword.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/auth/reset-password/ResetPassword.tsx b/src/app/pages/auth/reset-password/ResetPassword.tsx index f8d54b20aa..1ada9afd72 100644 --- a/src/app/pages/auth/reset-password/ResetPassword.tsx +++ b/src/app/pages/auth/reset-password/ResetPassword.tsx @@ -29,7 +29,7 @@ export function ResetPassword() { - Remember you password? Login + Remember your password? Login
);