diff --git a/_redirects b/_redirects deleted file mode 100644 index 270cd33..0000000 --- a/_redirects +++ /dev/null @@ -1,3 +0,0 @@ -# Redirects from what the browser requests to what we serve -/login / -/register / diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000..ec8a41d --- /dev/null +++ b/build.config.ts @@ -0,0 +1,3 @@ +export default { + base: '/', +}; diff --git a/config.json b/config.json index 0ff493a..484c7cd 100644 --- a/config.json +++ b/config.json @@ -8,5 +8,10 @@ "mozilla.org", "xmr.se" ], - "allowCustomHomeservers": true + "allowCustomHomeservers": true, + + "hashRouter": { + "enabled": false, + "basename": "/" + } } diff --git a/index.html b/index.html index 6bc955c..48f8e69 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,6 @@ - + diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..e7d948e --- /dev/null +++ b/netlify.toml @@ -0,0 +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/:splat" + status = 200 + +[[redirects]] + from = "/assets/*" + to = "/assets/:splat" + status = 200 + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6c944ce..fba4072 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,13 +29,13 @@ "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", "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", @@ -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,14 @@ "@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/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", "@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", @@ -90,7 +92,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": { @@ -110,44 +112,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==", + "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.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", + "@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 +161,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 +175,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 +188,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 +216,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==", + "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.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" + "@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 +356,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 +381,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 +396,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 +446,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==", + "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.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", + "@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 +479,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 +1894,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 +2584,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", @@ -2656,6 +2665,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", @@ -2705,6 +2883,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 +2981,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 +2991,18 @@ } }, "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": "*" + } + }, + "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": "*" @@ -3208,22 +3436,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 +3778,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 +3789,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 +3863,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 +3874,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -3806,9 +4042,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", @@ -3997,13 +4233,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": { @@ -4070,9 +4306,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", @@ -4959,9 +5195,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", @@ -5045,9 +5281,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, @@ -5762,54 +5998,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": { + "@types/react": { "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": { - "optional": true - }, - "jotai-zustand": { + "react": { "optional": true } } @@ -6281,9 +6484,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", @@ -6329,9 +6532,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", @@ -6654,9 +6857,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", @@ -6672,7 +6875,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -6788,12 +6991,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" @@ -6882,13 +7084,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" }, @@ -6911,24 +7113,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": { @@ -7010,6 +7211,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", @@ -7037,9 +7268,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" } @@ -7149,18 +7380,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" } }, @@ -7271,12 +7515,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": { @@ -7296,9 +7539,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" } @@ -7810,9 +8053,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", @@ -7821,6 +8064,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -7828,7 +8075,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -7858,27 +8105,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": "*", @@ -7891,6 +8142,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -7959,9 +8213,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" ], @@ -7975,9 +8229,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" ], @@ -7991,9 +8245,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" ], @@ -8007,9 +8261,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" ], @@ -8023,9 +8277,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" ], @@ -8039,9 +8293,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" ], @@ -8055,9 +8309,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" ], @@ -8071,9 +8325,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" ], @@ -8087,9 +8341,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" ], @@ -8103,9 +8357,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" ], @@ -8119,9 +8373,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" ], @@ -8135,9 +8389,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" ], @@ -8151,9 +8405,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" ], @@ -8167,9 +8421,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" ], @@ -8183,9 +8437,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" ], @@ -8199,9 +8453,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" ], @@ -8215,9 +8469,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" ], @@ -8231,9 +8485,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" ], @@ -8247,9 +8501,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" ], @@ -8263,9 +8517,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" ], @@ -8279,9 +8533,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" ], @@ -8295,9 +8549,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" ], @@ -8311,9 +8565,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": { @@ -8323,28 +8577,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 cb6111b..56e7b8c 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,13 @@ "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", "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", @@ -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,14 @@ "@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/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", "@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", @@ -100,7 +102,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" } } diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx new file mode 100644 index 0000000..f21bad0 --- /dev/null +++ b/src/app/components/AuthFlowsLoader.tsx @@ -0,0 +1,64 @@ +import { ReactNode, useCallback, useEffect, useMemo } from 'react'; +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, + parseRegisterErrResp, +} from '../hooks/useAuthFlows'; + +type AuthFlowsLoaderProps = { + fallback?: () => ReactNode; + error?: (err: unknown) => ReactNode; + children: (authFlows: 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 registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined; + let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest }; + + if (typeof registerResp === 'object' && registerResp.httpStatus) { + registerFlows = parseRegisterErrResp(registerResp); + } + + 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]) + ); + + useEffect(() => { + load(); + }, [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/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx new file mode 100644 index 0000000..72d367c --- /dev/null +++ b/src/app/components/ClientConfigLoader.tsx @@ -0,0 +1,38 @@ +import { ReactNode, useCallback, useEffect, useState } from 'react'; +import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; +import { ClientConfig } from '../hooks/useClientConfig'; +import { trimTrailingSlash } from '../utils/common'; + +const getClientConfig = async (): Promise => { + const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`; + const config = await fetch(url, { method: 'GET' }); + return config.json(); +}; + +type ClientConfigLoaderProps = { + fallback?: () => ReactNode; + error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode; + children: (config: ClientConfig) => ReactNode; +}; +export function ClientConfigLoader({ fallback, error, children }: ClientConfigLoaderProps) { + const [state, load] = useAsyncCallback(getClientConfig); + const [ignoreError, setIgnoreError] = useState(false); + + const ignoreCallback = useCallback(() => setIgnoreError(true), []); + + useEffect(() => { + load(); + }, [load]); + + if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) { + 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/components/ConfirmPasswordMatch.tsx b/src/app/components/ConfirmPasswordMatch.tsx new file mode 100644 index 0000000..bb50eb6 --- /dev/null +++ b/src/app/components/ConfirmPasswordMatch.tsx @@ -0,0 +1,35 @@ +import { ReactNode, RefObject, useCallback, useRef, useState } from 'react'; +import { useDebounce } from '../hooks/useDebounce'; + +type ConfirmPasswordMatchProps = { + initialValue: boolean; + children: ( + match: boolean, + doMatch: () => void, + passRef: RefObject, + confPassRef: RefObject + ) => ReactNode; +}; +export 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); +} diff --git a/src/app/components/SpecVersionsLoader.tsx b/src/app/components/SpecVersionsLoader.tsx new file mode 100644 index 0000000..56d7f8b --- /dev/null +++ b/src/app/components/SpecVersionsLoader.tsx @@ -0,0 +1,32 @@ +import { ReactNode, useCallback, useEffect } 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]) + ); + + useEffect(() => { + load(); + }, [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/components/SupportedUIAFlowsLoader.tsx b/src/app/components/SupportedUIAFlowsLoader.tsx new file mode 100644 index 0000000..442eb57 --- /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/UIAFlowOverlay.tsx b/src/app/components/UIAFlowOverlay.tsx new file mode 100644 index 0000000..f788eb0 --- /dev/null +++ b/src/app/components/UIAFlowOverlay.tsx @@ -0,0 +1,72 @@ +import React, { ReactNode } from 'react'; +import { + Overlay, + OverlayBackdrop, + Box, + config, + Text, + TooltipProvider, + Tooltip, + Icons, + Icon, + Chip, + IconButton, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; + +export type UIAFlowOverlayProps = { + currentStep: number; + stepCount: number; + children: ReactNode; + onCancel: () => void; +}; +export function UIAFlowOverlay({ + currentStep, + stepCount, + children, + onCancel, +}: UIAFlowOverlayProps) { + return ( + }> + + + + {children} + + + + {`Step ${currentStep}/${stepCount}`} + + + Exit + + } + position="Top" + > + {(anchorRef) => ( + + + + )} + + + + + + ); +} diff --git a/src/app/components/password-input/PasswordInput.tsx b/src/app/components/password-input/PasswordInput.tsx new file mode 100644 index 0000000..184a097 --- /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 paddingRight: string = size === '500' ? config.space.S300 : config.space.S200; + + return ( + + {(visible, setVisible) => ( + + {after} + setVisible(!visible)} + type="button" + variant={visible ? 'Warning' : variant} + size="300" + radii="300" + > + + + + } + /> + )} + + ); + } +); 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 0000000..bd3c300 --- /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 0000000..27adadb --- /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 0000000..e3e5dd3 --- /dev/null +++ b/src/app/components/splash-screen/index.ts @@ -0,0 +1 @@ +export * from './SplashScreen'; diff --git a/src/app/components/uia-stages/DummyStage.tsx b/src/app/components/uia-stages/DummyStage.tsx new file mode 100644 index 0000000..7e0f858 --- /dev/null +++ b/src/app/components/uia-stages/DummyStage.tsx @@ -0,0 +1,65 @@ +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 DummyErrorDialog({ + title, + message, + onRetry, + onCancel, +}: { + title: string; + message: string; + onRetry: () => void; + onCancel: () => void; +}) { + return ( + + + + {title} + {message} + + + + + + ); +} + +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 0000000..fdc2b61 --- /dev/null +++ b/src/app/components/uia-stages/EmailStage.tsx @@ -0,0 +1,172 @@ +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 { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types'; + +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, + emailTokenState, + requestEmailToken, + submitAuthDict, + onCancel, +}: StageComponentProps & { + email?: string; + clientSecret: string; + emailTokenState: AsyncState; + requestEmailToken: RequestEmailTokenCallback; +}) { + 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) => { + requestEmailToken(userEmail, clientSecret); + }, + [clientSecret, requestEmailToken] + ); + + useEffect(() => { + if (email && !errorCode && emailTokenState.status === AsyncStatus.Idle) { + requestEmailToken(email, clientSecret); + } + }, [email, errorCode, clientSecret, emailTokenState, requestEmailToken]); + + if (emailTokenState.status === AsyncStatus.Loading) { + return ( + + + Sending verification email... + + ); + } + + if (emailTokenState.status === AsyncStatus.Error) { + return ( + + ); + } + + if (emailTokenState.status === AsyncStatus.Success) { + return ( + + + + Verification Request Sent + {`Please check your email "${emailTokenState.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 0000000..68b3fcf --- /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 0000000..ed8a304 --- /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 0000000..f697705 --- /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 0000000..95c19a7 --- /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 0000000..cc6674c --- /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/cs-api.ts b/src/app/cs-api.ts new file mode 100644 index 0000000..b9c6771 --- /dev/null +++ b/src/app/cs-api.ts @@ -0,0 +1,115 @@ +import to from 'await-to-js'; +import { trimTrailingSlash } from './utils/common'; + +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) ? 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, + { + 'm.homeserver': { + base_url: host, + }, + }, + ]; + } + 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, + ]; + } + + 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]; +}; + +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'); +}; diff --git a/src/app/cs-errorcode.ts b/src/app/cs-errorcode.ts new file mode 100644 index 0000000..6c21d67 --- /dev/null +++ b/src/app/cs-errorcode.ts @@ -0,0 +1,37 @@ +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_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', + 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', +} diff --git a/src/app/hooks/types.ts b/src/app/hooks/types.ts new file mode 100644 index 0000000..9aac2b3 --- /dev/null +++ b/src/app/hooks/types.ts @@ -0,0 +1,12 @@ +import { IRequestTokenResponse } from 'matrix-js-sdk'; + +export type RequestEmailTokenResponse = { + email: string; + clientSecret: string; + result: IRequestTokenResponse; +}; +export type RequestEmailTokenCallback = ( + email: string, + clientSecret: string, + nextLink?: string +) => Promise; diff --git a/src/app/hooks/useAsyncCallback.ts b/src/app/hooks/useAsyncCallback.ts index 18b63ec..fc7dca6 100644 --- a/src/app/hooks/useAsyncCallback.ts +++ b/src/app/hooks/useAsyncCallback.ts @@ -1,4 +1,5 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { useAlive } from './useAlive'; export enum AsyncStatus { @@ -16,36 +17,56 @@ 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(); + // 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, + 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, + }); + }); }); + 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,10 +75,13 @@ export const useAsyncCallback = ( } return data; } catch (e) { + if (currentReqNumber !== reqNumberRef.current) { + throw new Error('AsyncCallbackHook: Request replaced!'); + } if (alive()) { setState({ status: AsyncStatus.Error, - error: e, + error: e as TError, }); } throw e; diff --git a/src/app/hooks/useAuthFlows.ts b/src/app/hooks/useAuthFlows.ts new file mode 100644 index 0000000..7bb7ddc --- /dev/null +++ b/src/app/hooks/useAuthFlows.ts @@ -0,0 +1,59 @@ +import { createContext, useContext } from 'react'; +import { IAuthData, MatrixError } 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 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; +}; + +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; +}; diff --git a/src/app/hooks/useAuthServer.ts b/src/app/hooks/useAuthServer.ts new file mode 100644 index 0000000..f77566f --- /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/hooks/useAutoDiscoveryInfo.ts b/src/app/hooks/useAutoDiscoveryInfo.ts new file mode 100644 index 0000000..b2f8bcb --- /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; +}; diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts new file mode 100644 index 0000000..8406668 --- /dev/null +++ b/src/app/hooks/useClientConfig.ts @@ -0,0 +1,33 @@ +import { createContext, useContext } from 'react'; + +export type ClientConfig = { + defaultHomeserver?: number; + homeserverList?: string[]; + allowCustomHomeservers?: boolean; + + hashRouter?: { + enabled?: boolean; + basename?: string; + }; +}; + +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; +} + +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/hooks/useCrossSigningStatus.js b/src/app/hooks/useCrossSigningStatus.js index 61b69d1..845c546 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/hooks/useParsedLoginFlows.ts b/src/app/hooks/useParsedLoginFlows.ts new file mode 100644 index 0000000..14ecfb9 --- /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/hooks/usePasswordEmail.ts b/src/app/hooks/usePasswordEmail.ts new file mode 100644 index 0000000..37e9643 --- /dev/null +++ b/src/app/hooks/usePasswordEmail.ts @@ -0,0 +1,32 @@ +import { MatrixClient, MatrixError } from 'matrix-js-sdk'; +import { useCallback, useRef } from 'react'; +import { AsyncState, useAsyncCallback } from './useAsyncCallback'; +import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types'; + +export const usePasswordEmail = ( + mx: MatrixClient +): [AsyncState, RequestEmailTokenCallback] => { + const sendAttemptRef = useRef(1); + + const passwordEmailCallback: RequestEmailTokenCallback = useCallback( + async (email, clientSecret, nextLink) => { + const sendAttempt = sendAttemptRef.current; + sendAttemptRef.current += 1; + const result = await mx.requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink); + return { + email, + clientSecret, + result, + }; + }, + [mx] + ); + + const [passwordEmailState, passwordEmail] = useAsyncCallback< + RequestEmailTokenResponse, + MatrixError, + Parameters + >(passwordEmailCallback); + + return [passwordEmailState, passwordEmail]; +}; diff --git a/src/app/hooks/usePathWithOrigin.ts b/src/app/hooks/usePathWithOrigin.ts new file mode 100644 index 0000000..4430d06 --- /dev/null +++ b/src/app/hooks/usePathWithOrigin.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react'; +import { useClientConfig } from './useClientConfig'; +import { trimLeadingSlash, trimSlash, trimTrailingSlash } from '../utils/common'; + +export const usePathWithOrigin = (path: string): string => { + const { hashRouter } = useClientConfig(); + const { origin } = window.location; + + const pathWithOrigin = useMemo(() => { + let url: string = trimSlash(origin); + + 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, hashRouter, origin]); + + return pathWithOrigin; +}; diff --git a/src/app/hooks/useRegisterEmail.ts b/src/app/hooks/useRegisterEmail.ts new file mode 100644 index 0000000..d29c9e6 --- /dev/null +++ b/src/app/hooks/useRegisterEmail.ts @@ -0,0 +1,32 @@ +import { MatrixClient, MatrixError } from 'matrix-js-sdk'; +import { useCallback, useRef } from 'react'; +import { AsyncState, useAsyncCallback } from './useAsyncCallback'; +import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types'; + +export const useRegisterEmail = ( + mx: MatrixClient +): [AsyncState, RequestEmailTokenCallback] => { + const sendAttemptRef = useRef(1); + + const registerEmailCallback: RequestEmailTokenCallback = useCallback( + async (email, clientSecret, nextLink) => { + const sendAttempt = sendAttemptRef.current; + sendAttemptRef.current += 1; + const result = await mx.requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink); + return { + email, + clientSecret, + result, + }; + }, + [mx] + ); + + const [registerEmailState, registerEmail] = useAsyncCallback< + RequestEmailTokenResponse, + MatrixError, + Parameters + >(registerEmailCallback); + + return [registerEmailState, registerEmail]; +}; diff --git a/src/app/hooks/useSpecVersions.ts b/src/app/hooks/useSpecVersions.ts new file mode 100644 index 0000000..42403c6 --- /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; +} diff --git a/src/app/hooks/useUIAFlows.ts b/src/app/hooks/useUIAFlows.ts new file mode 100644 index 0000000..22acd6b --- /dev/null +++ b/src/app/hooks/useUIAFlows.ts @@ -0,0 +1,96 @@ +import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk'; +import { useCallback, useMemo } from 'react'; +import { + getSupportedUIAFlows, + getUIACompleted, + getUIAError, + 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 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; + +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 error = useUIAError(authData); + + const getStageToComplete: AuthStageDataGetter = useCallback(() => { + const { stages } = uiaFlow; + const nextStage = stages.find((stage) => !completed.includes(stage)); + if (!nextStage) return undefined; + + const info = params[nextStage]; + + return { + type: nextStage, + info, + session, + errorCode, + error, + }; + }, [uiaFlow, completed, params, errorCode, error, 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/molecules/room-aliases/RoomAliases.jsx b/src/app/molecules/room-aliases/RoomAliases.jsx index 201c523..d573f7d 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 6a72a99..d9dd954 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 1c088e5..4adb116 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 2612aed..6009649 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 7a85287..a5e8e2d 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 6fe81cd..3ae1f29 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/room/message/UrlPreviewCard.tsx b/src/app/organisms/room/message/UrlPreviewCard.tsx index 9ae4d29..b085e18 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; diff --git a/src/app/organisms/space-manage/SpaceManage.jsx b/src/app/organisms/space-manage/SpaceManage.jsx index cf042da..60f00ad 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]; diff --git a/src/app/pages/App.jsx b/src/app/pages/App.jsx deleted file mode 100644 index 2828d7b..0000000 --- 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 0000000..6cefe99 --- /dev/null +++ b/src/app/pages/App.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Provider as JotaiProvider } from 'jotai'; +import { + Route, + RouterProvider, + createBrowserRouter, + createHashRouter, + createRoutesFromElements, + redirect, +} from 'react-router-dom'; + +import { ClientConfigLoader } from '../components/ClientConfigLoader'; +import { ClientConfig, ClientConfigProvider } from '../hooks/useClientConfig'; +import { AuthLayout, Login, Register, ResetPassword, authLayoutLoader } from './auth'; +import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths'; +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; + + const routes = createRoutesFromElements( + + { + if (isAuthenticated()) return redirect('/home'); + return redirect(getLoginPath()); + }} + /> + }> + } /> + } /> + } /> + + + { + if (!isAuthenticated()) return redirect(getLoginPath()); + return null; + }} + > + } /> + direct

} /> + :spaceIdOrAlias

} /> + explore

} /> +
+ Page not found

} /> +
+ ); + + if (hashRouter?.enabled) { + return createHashRouter(routes, { basename: hashRouter.basename }); + } + return createBrowserRouter(routes, { + basename: import.meta.env.BASE_URL, + }); +}; + +// TODO: app crash boundary +function App() { + return ( + } + error={(err, retry, ignore) => ( + + )} + > + {(clientConfig) => ( + + + + + + )} + + ); +} + +export default App; diff --git a/src/app/pages/ConfigConfig.tsx b/src/app/pages/ConfigConfig.tsx new file mode 100644 index 0000000..dbcdca7 --- /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} + + )} + + + + + + + + ); +} diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx new file mode 100644 index 0000000..6454161 --- /dev/null +++ b/src/app/pages/auth/AuthFooter.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Box, Text } from 'folds'; +import * as css from './styles.css'; + +export function AuthFooter() { + return ( + + + About + + + v3.2.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 0000000..c58ecdd --- /dev/null +++ b/src/app/pages/auth/AuthLayout.tsx @@ -0,0 +1,215 @@ +import React, { useCallback, useEffect } from 'react'; +import { Box, Header, 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 { + 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'; +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'; +import { AuthServerProvider } from '../../hooks/useAuthServer'; + +export const authLayoutLoader: LoaderFunction = () => { + if (isAuthenticated()) { + 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 clientConfig = useClientConfig(); + + const defaultServer = clientDefaultServer(clientConfig); + let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer; + + if (!clientAllowedServer(clientConfig, server)) { + server = defaultServer; + } + + const [discoveryState, discoverServer] = useAsyncCallback( + useCallback(async (serverName: string) => { + const response = await autoDiscovery(fetch, serverName); + return { + serverName, + response, + }; + }, []) + ); + + useEffect(() => { + 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) => { + if (newServer === server) { + if (discoveryState.status === AsyncStatus.Loading) return; + discoverServer(server); + return; + } + navigate( + generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) }) + ); + }, + [navigate, location, discoveryState, server, discoverServer] + ); + + const [autoDiscoveryError, autoDiscoveryInfo] = + discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : []; + + return ( + + + +
+ + Cinny Logo + Cinny + +
+ + + + Homeserver + + + + {discoveryState.status === AsyncStatus.Loading && ( + + )} + {discoveryState.status === AsyncStatus.Error && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && ( + + )} + {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && ( + + + ( + + )} + error={() => ( + + )} + > + {(specVersions) => ( + + ( + + )} + error={() => ( + + )} + > + {(authFlows) => ( + + + + )} + + + )} + + + + )} + +
+ +
+
+ ); +} diff --git a/src/app/pages/auth/FiledError.tsx b/src/app/pages/auth/FiledError.tsx new file mode 100644 index 0000000..d96fc87 --- /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/OrDivider.tsx b/src/app/pages/auth/OrDivider.tsx new file mode 100644 index 0000000..629d3f5 --- /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 new file mode 100644 index 0000000..a9c1c54 --- /dev/null +++ b/src/app/pages/auth/SSOLogin.tsx @@ -0,0 +1,68 @@ +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[]; + asIcons?: boolean; + redirectUrl: string; +}; +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 => 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 = `Continue with ${name}`; + + if (iconUrl && asIcons) { + return ( + + + + ); + } + + return ( + + ); + })} + + ); +} diff --git a/src/app/pages/auth/ServerPicker.tsx b/src/app/pages/auth/ServerPicker.tsx new file mode 100644 index 0000000..5f5dcf6 --- /dev/null +++ b/src/app/pages/auth/ServerPicker.tsx @@ -0,0 +1,140 @@ +import React, { + ChangeEventHandler, + KeyboardEventHandler, + MouseEventHandler, + useEffect, + 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({ + server, + serverList, + allowCustomServer, + onServerChange, +}: { + server: string; + serverList: string[]; + allowCustomServer?: boolean; + onServerChange: (server: string) => void; +}) { + const [serverMenu, setServerMenu] = useState(false); + const serverInputRef = useRef(null); + + 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 }); + + 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) onServerChange(inputServer); + } + }; + + const handleServerSelect: MouseEventHandler = (evt) => { + const selectedServer = evt.currentTarget.getAttribute('data-server'); + if (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((serverName) => ( + + {serverName} + + ))} +
+
+ + } + > + {(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 0000000..c4bd047 --- /dev/null +++ b/src/app/pages/auth/index.ts @@ -0,0 +1,4 @@ +export * from './AuthLayout'; +export * from './login'; +export * from './register'; +export * from './reset-password'; diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx new file mode 100644 index 0000000..29a7b0c --- /dev/null +++ b/src/app/pages/auth/login/Login.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Box, Text, color } from 'folds'; +import { Link, useSearchParams } from 'react-router-dom'; +import { useAuthFlows } from '../../../hooks/useAuthFlows'; +import { useAuthServer } from '../../../hooks/useAuthServer'; +import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; +import { PasswordLoginForm } from './PasswordLoginForm'; +import { SSOLogin } from '../SSOLogin'; +import { TokenLogin } from './TokenLogin'; +import { OrDivider } from '../OrDivider'; +import { getLoginPath, getRegisterPath } from '../../pathUtils'; +import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin'; +import { LoginPathSearchParams } from '../../paths'; + +const getLoginSearchParams = (searchParams: URLSearchParams): LoginPathSearchParams => ({ + username: searchParams.get('username') ?? undefined, + email: searchParams.get('email') ?? undefined, + loginToken: searchParams.get('loginToken') ?? undefined, +}); + +export function Login() { + const server = useAuthServer(); + const { loginFlows } = useAuthFlows(); + const [searchParams] = useSearchParams(); + const loginSearchParams = getLoginSearchParams(searchParams); + const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server)); + + const parsedFlows = useParsedLoginFlows(loginFlows.flows); + + return ( + + + Login + + {parsedFlows.token && loginSearchParams.loginToken && ( + + )} + {parsedFlows.password && ( + <> + + + {parsedFlows.sso && } + + )} + {parsedFlows.sso && ( + <> + 2 + } + /> + + + )} + {!parsedFlows.password && !parsedFlows.sso && ( + <> + + {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`} + + + + )} + + Do not have an account? Register + + + ); +} diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx new file mode 100644 index 0000000..ea52aad --- /dev/null +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -0,0 +1,272 @@ +import React, { FormEventHandler, useCallback, useState } from 'react'; +import { + Box, + Button, + Header, + Icon, + IconButton, + Icons, + Input, + Menu, + Overlay, + OverlayBackdrop, + OverlayCenter, + PopOut, + Spinner, + Text, + config, +} from 'folds'; +import FocusTrap from 'focus-trap-react'; +import { Link } 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 { useAuthServer } from '../../../hooks/useAuthServer'; +import { useClientConfig } from '../../../hooks/useClientConfig'; +import { + CustomLoginResponse, + LoginError, + factoryGetBaseUrl, + login, + useLoginComplete, +} from './loginUtil'; +import { PasswordInput } from '../../../components/password-input/PasswordInput'; +import { FieldError } from '../FiledError'; +import { getResetPasswordPath } from '../../pathUtils'; + +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="300" + radii="300" + aria-pressed={open} + > + + + )} +
+ ); +} + +type PasswordLoginFormProps = { + defaultUsername?: string; + defaultEmail?: string; +}; +export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { + const server = useAuthServer(); + const clientConfig = useClientConfig(); + + const serverDiscovery = useAutoDiscoveryInfo(); + const baseUrl = serverDiscovery['m.homeserver'].base_url; + + const [loginState, startLogin] = useAsyncCallback< + CustomLoginResponse, + MatrixError, + Parameters + >(useCallback(login, [])); + + useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined); + + const handleUsernameLogin = (username: string, password: string) => { + startLogin(baseUrl, { + type: 'm.login.password', + 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 getBaseUrl = factoryGetBaseUrl(clientConfig, mxIdServer); + + startLogin(getBaseUrl, { + type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: mxIdUsername, + }, + password, + initial_device_display_name: 'Cinny Web', + }); + }; + const handleEmailLogin = (email: string, password: string) => { + startLogin(baseUrl, { + type: 'm.login.password', + 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.ServerNotAllowed && ( + + )} + {loginState.error.errcode === LoginError.InvalidServer && ( + + )} + + )} + + + + Password + + + + {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 && ( + + )} + + )} + + + Forget Password? + + + + + + + } + > + + + + + + ); +} diff --git a/src/app/pages/auth/login/TokenLogin.tsx b/src/app/pages/auth/login/TokenLogin.tsx new file mode 100644 index 0000000..761d5dc --- /dev/null +++ b/src/app/pages/auth/login/TokenLogin.tsx @@ -0,0 +1,94 @@ +import { + Box, + Icon, + Icons, + Overlay, + OverlayBackdrop, + OverlayCenter, + Spinner, + Text, + color, + config, +} 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 { CustomLoginResponse, LoginError, login, useLoginComplete } from './loginUtil'; + +function LoginTokenError({ message }: { message: string }) { + 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/login/index.ts b/src/app/pages/auth/login/index.ts new file mode 100644 index 0000000..a10c3a8 --- /dev/null +++ b/src/app/pages/auth/login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts new file mode 100644 index 0000000..b2fd387 --- /dev/null +++ b/src/app/pages/auth/login/loginUtil.ts @@ -0,0 +1,118 @@ +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 { ErrorCode } from '../../../cs-errorcode'; + +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: LoginRequest +): 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(data.type, 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 === ErrorCode.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]); +}; diff --git a/src/app/pages/auth/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx new file mode 100644 index 0000000..f4439dd --- /dev/null +++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx @@ -0,0 +1,420 @@ +import { + Box, + Button, + Checkbox, + Input, + Overlay, + OverlayBackdrop, + OverlayCenter, + Spinner, + Text, + color, +} from 'folds'; +import React, { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; +import { + AuthDict, + AuthType, + IAuthData, + MatrixError, + RegisterRequest, + UIAFlow, + createClient, +} from 'matrix-js-sdk'; +import { PasswordInput } from '../../../components/password-input/PasswordInput'; +import { + getLoginTermUrl, + getUIAFlowForStages, + hasStageInFlows, + requiredStageInFlows, +} from '../../../utils/matrix-uia'; +import { useUIACompleted, 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 { + AutoDummyStageDialog, + AutoTermsStageDialog, + EmailStageDialog, + ReCaptchaStageDialog, + RegistrationTokenStageDialog, +} from '../../../components/uia-stages'; +import { useRegisterEmail } from '../../../hooks/useRegisterEmail'; +import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch'; +import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay'; +import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../../hooks/types'; + +export const SUPPORTED_REGISTER_STAGES = [ + AuthType.RegistrationToken, + AuthType.Terms, + AuthType.Recaptcha, + 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; + clientSecret: string; +}; + +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 = { + formData: FormData; + flow: UIAFlow; + authData: IAuthData; + registerEmailState: AsyncState; + registerEmail: RequestEmailTokenCallback; + onRegister: (registerReqData: RegisterRequest) => void; +}; +function RegisterUIAFlow({ + formData, + flow, + authData, + registerEmailState, + registerEmail, + onRegister, +}: RegisterUIAFlowProps) { + const completed = useUIACompleted(authData); + const { getStageToComplete } = useUIAFlow(authData, flow); + + const stageToComplete = getStageToComplete(); + + const handleAuthDict = useCallback( + (authDict: AuthDict) => { + const { password, username } = formData; + onRegister({ + auth: authDict, + password, + username, + initial_device_display_name: 'Cinny Web', + }); + }, + [onRegister, formData] + ); + + const handleCancel = 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 && ( + + )} + + ); +} + +type PasswordRegisterFormProps = { + authData: IAuthData; + uiaFlows: UIAFlow[]; + defaultUsername?: string; + defaultEmail?: string; + defaultRegisterToken?: string; +}; +export function PasswordRegisterForm({ + authData, + uiaFlows, + defaultUsername, + defaultEmail, + defaultRegisterToken, +}: PasswordRegisterFormProps) { + const serverDiscovery = useAutoDiscoveryInfo(); + const baseUrl = serverDiscovery['m.homeserver'].base_url; + const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); + const params = useUIAParams(authData); + const termUrl = getLoginTermUrl(params); + const [formData, setFormData] = useState(); + + const [ongoingFlow, setOngoingFlow] = useState(); + + const [registerEmailState, registerEmail] = useRegisterEmail(mx); + + 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; + + useRegisterComplete(customRegisterResp); + + const handleSubmit: ChangeEventHandler = (evt) => { + evt.preventDefault(); + const { + usernameInput, + passwordInput, + confirmPasswordInput, + emailInput, + tokenInput, + termsInput, + } = evt.target as HTMLFormElement & RegisterFormInputs; + const token = tokenInput?.value.trim(); + const username = usernameInput.value.trim(); + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + if (password !== confirmPassword) { + return; + } + const email = emailInput?.value.trim(); + const terms = termsInput?.value === 'on'; + + if (!username) { + usernameInput.focus(); + return; + } + + const fData: FormData = { + username, + password, + token, + email, + terms, + clientSecret: mx.generateClientSecret(), + }; + 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 + + + {registerError?.errcode === RegisterError.UserTaken && ( + + )} + {registerError?.errcode === RegisterError.UserInvalid && ( + + )} + {registerError?.errcode === RegisterError.UserExclusive && ( + + )} + + + {(match, doMatch, passRef, confPassRef) => ( + <> + + + Password + + + {registerError?.errcode === RegisterError.PasswordWeak && ( + + )} + {registerError?.errcode === RegisterError.PasswordShort && ( + + )} + + + + 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 + + . + + + )} + {registerError?.errcode === RegisterError.RateLimited && ( + + )} + {registerError?.errcode === RegisterError.Forbidden && ( + + )} + {registerError?.errcode === RegisterError.InvalidRequest && ( + + )} + {registerError?.errcode === RegisterError.Unknown && ( + + )} + + + + {registerState.status === AsyncStatus.Success && + formData && + ongoingFlow && + ongoingAuthData && ( + + )} + {registerState.status === AsyncStatus.Loading && ( + }> + + + + + )} + + ); +} diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx new file mode 100644 index 0000000..756b13b --- /dev/null +++ b/src/app/pages/auth/register/Register.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { Box, Text, color } from 'folds'; +import { Link, useSearchParams } from 'react-router-dom'; +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'; +import { getLoginPath } from '../../pathUtils'; +import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin'; +import { RegisterPathSearchParams } from '../../paths'; + +const getRegisterSearchParams = (searchParams: URLSearchParams): RegisterPathSearchParams => ({ + username: searchParams.get('username') ?? undefined, + email: searchParams.get('email') ?? undefined, + token: searchParams.get('token') ?? 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 = usePathWithOrigin(getLoginPath(server)); + + return ( + + + Register + + {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! Failed to get any registration options. + + )} + {registerFlows.status === RegisterFlowStatus.FlowRequired && ( + <> + + {(supportedFlows) => + supportedFlows.length === 0 ? ( + + This application does not support registration on this homeserver. + + ) : ( + + ) + } + + + {sso && } + + )} + {sso && ( + <> + 2 + } + /> + + + )} + + Already have an account? Login + + + ); +} diff --git a/src/app/pages/auth/register/index.ts b/src/app/pages/auth/register/index.ts new file mode 100644 index 0000000..7eb55fd --- /dev/null +++ b/src/app/pages/auth/register/index.ts @@ -0,0 +1 @@ +export * from './Register'; diff --git a/src/app/pages/auth/register/registerUtil.ts b/src/app/pages/auth/register/registerUtil.ts new file mode 100644 index 0000000..23c3d6a --- /dev/null +++ b/src/app/pages/auth/register/registerUtil.ts @@ -0,0 +1,125 @@ +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', + PasswordShort = 'PasswordShort', + 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, + error: err.data.error, + }); + } + if (err.errcode === ErrorCode.M_PASSWORD_TOO_SHORT) { + throw new MatrixError({ + errcode: RegisterError.PasswordShort, + error: err.data.error, + }); + } + + 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, + error: err.data.error, + }); + } + 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]); +}; diff --git a/src/app/pages/auth/reset-password/PasswordResetForm.tsx b/src/app/pages/auth/reset-password/PasswordResetForm.tsx new file mode 100644 index 0000000..7c71de0 --- /dev/null +++ b/src/app/pages/auth/reset-password/PasswordResetForm.tsx @@ -0,0 +1,274 @@ +import React, { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; +import { + Box, + Button, + Dialog, + Input, + Overlay, + OverlayBackdrop, + OverlayCenter, + Spinner, + Text, + color, + config, +} from 'folds'; +import { useNavigate } from 'react-router-dom'; +import FocusTrap from 'focus-trap-react'; +import { AuthDict, AuthType, MatrixError, createClient } from 'matrix-js-sdk'; +import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { useAuthServer } from '../../../hooks/useAuthServer'; +import { usePasswordEmail } from '../../../hooks/usePasswordEmail'; +import { PasswordInput } from '../../../components/password-input/PasswordInput'; +import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch'; +import { FieldError } from '../FiledError'; +import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay'; +import { EmailStageDialog } from '../../../components/uia-stages'; +import { ResetPasswordResult, resetPassword } from './resetPasswordUtil'; +import { getLoginPath, withSearchParam } from '../../pathUtils'; +import { LoginPathSearchParams } from '../../paths'; +import { getUIAError, getUIAErrorCode } from '../../../utils/matrix-uia'; + +type FormData = { + email: string; + password: string; + clientSecret: string; +}; + +function ResetPasswordComplete({ email }: { email?: string }) { + const server = useAuthServer(); + + const navigate = useNavigate(); + + const handleClick = () => { + const path = getLoginPath(server); + if (email) { + navigate(withSearchParam(path, { email })); + return; + } + navigate(path); + }; + + return ( + }> + + + + + + Password has been reset successfully. Please login with your new password. + + + + + + + + ); +} + +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 new file mode 100644 index 0000000..1ada9af --- /dev/null +++ b/src/app/pages/auth/reset-password/ResetPassword.tsx @@ -0,0 +1,36 @@ +import { Box, Text } from 'folds'; +import React from 'react'; +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 + + + + + + Remember your password? Login + + + ); +} diff --git a/src/app/pages/auth/reset-password/index.ts b/src/app/pages/auth/reset-password/index.ts new file mode 100644 index 0000000..0e85ecf --- /dev/null +++ b/src/app/pages/auth/reset-password/index.ts @@ -0,0 +1 @@ +export * from './ResetPassword'; 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 0000000..5eb436f --- /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/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts new file mode 100644 index 0000000..5834ad8 --- /dev/null +++ b/src/app/pages/auth/styles.css.ts @@ -0,0 +1,53 @@ +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: '1vh', + maxWidth: toRem(460), + 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, + { + width: toRem(26), + height: toRem(26), + + borderRadius: '50%', + }, +]); + +export const AuthHeader = style({ + padding: `0 ${config.space.S400}`, + borderBottomWidth: config.borderWidth.B300, +}); + +export const AuthCardContent = style({ + maxWidth: toRem(402), + width: '100%', + margin: 'auto', + padding: config.space.S400, + paddingTop: config.space.S700, + paddingBottom: toRem(44), + gap: toRem(44), +}); + +export const AuthFooter = style({ + padding: config.space.S200, +}); diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts new file mode 100644 index 0000000..db39ce3 --- /dev/null +++ b/src/app/pages/pathUtils.ts @@ -0,0 +1,28 @@ +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 => { + const params = server ? { server: encodeURIComponent(server) } : undefined; + return generatePath(LOGIN_PATH, params); +}; + +export const getRegisterPath = (server?: string): string => { + const params = server ? { server: encodeURIComponent(server) } : undefined; + return generatePath(REGISTER_PATH, params); +}; + +export const getResetPasswordPath = (server?: string): string => { + const params = server ? { server: encodeURIComponent(server) } : undefined; + return generatePath(RESET_PASSWORD_PATH, params); +}; diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts new file mode 100644 index 0000000..cd62264 --- /dev/null +++ b/src/app/pages/paths.ts @@ -0,0 +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?/'; diff --git a/src/app/state/hooks/inviteList.ts b/src/app/state/hooks/inviteList.ts index f8b7e05..ffe4444 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 5d0890b..c0a7bfb 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 43b5655..d90c766 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 463fd35..a6dc796 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 4f5a619..670e6db 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 96e2f0d..1fa8311 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 d456f85..f818450 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 7a793d8..e0fa170 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 374ddd5..1e2ef18 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 0c7b6bd..ad38876 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 0000000..85bcd10 --- /dev/null +++ b/src/app/state/sessions.ts @@ -0,0 +1,129 @@ +import { atom } from 'jotai'; +import { + atomWithLocalStorage, + getLocalStorageItem, + setLocalStorageItem, +} from './utils/atomWithLocalStorage'; + +export type Session = { + baseUrl: string; + userId: string; + deviceId: string; + accessToken: string; + expiresInMs?: number; + refreshToken?: string; + fallbackSdkStores?: boolean; +}; + +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( + (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); + } + } +); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 92d40ff..061931e 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 2f4ee92..b9472d9 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 b87817d..c77c91b 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 d92b93d..13869af 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 355c941..4c4caa5 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); diff --git a/src/app/state/utils/atomWithLocalStorage.ts b/src/app/state/utils/atomWithLocalStorage.ts new file mode 100644 index 0000000..f17d3a3 --- /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; +}; diff --git a/src/app/styles/Patterns.css.ts b/src/app/styles/Patterns.css.ts new file mode 100644 index 0000000..e455941 --- /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)}`, +}); diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index e007f22..5cbe380 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; @@ -77,3 +88,10 @@ export const parseGeoUri = (location: string) => { longitude, }; }; + +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/app/utils/matrix-uia.ts b/src/app/utils/matrix-uia.ts new file mode 100644 index 0000000..15c5799 --- /dev/null +++ b/src/app/utils/matrix-uia.ts @@ -0,0 +1,84 @@ +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 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) => { + 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; +}; diff --git a/src/app/utils/regex.ts b/src/app/utils/regex.ts index 5188bef..281f120 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 = '(? 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 0000000..9536a92 --- /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 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, getSecret }; diff --git a/src/ext.d.ts b/src/ext.d.ts index 5593b6e..72acc58 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; @@ -26,3 +28,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 57% rename from src/index.jsx rename to src/index.tsx index a8a7657..1d86420 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -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'; @@ -15,7 +15,18 @@ import settings from './client/state/settings'; import App from './app/pages/App'; document.body.classList.add(configClass, varsClass); - settings.applyTheme(); -ReactDom.render(, document.getElementById('root')); +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(); diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000..353ace6 --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,3 @@ +export type WithRequiredProp = Type & { + [Property in Key]-?: Type[Property]; +}; diff --git a/tsconfig.json b/tsconfig.json index d2f1e8a..60ff185 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 8357339..20c7765 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,7 +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 { svgLoader } from './viteSvgLoader' +import buildConfig from "./build.config" const copyFiles = { targets: [ @@ -18,7 +19,7 @@ const copyFiles = { dest: '', }, { - src: '_redirects', + src: 'netlify.toml', dest: '', }, { @@ -39,7 +40,7 @@ const copyFiles = { export default defineConfig({ appType: 'spa', publicDir: false, - base: "", + base: buildConfig.base, server: { port: 8080, host: true,