+++ /dev/null
-# Redirects from what the browser requests to what we serve
-/login /
-/register /
--- /dev/null
+export default {
+ base: '/',
+};
"mozilla.org",
"xmr.se"
],
- "allowCustomHomeservers": true
+ "allowCustomHomeservers": true,
+
+ "hashRouter": {
+ "enabled": false,
+ "basename": "/"
+ }
}
<audio id="inviteSound">
<source src="./public/sound/invite.ogg" type="audio/ogg" />
</audio>
- <script type="module" src="./src/index.jsx"></script>
+ <script type="module" src="./src/index.tsx"></script>
</body>
</html>
--- /dev/null
+[[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
"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",
"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",
"@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",
"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": {
}
},
"node_modules/@babel/code-frame": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
+ "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
"dependencies": {
- "@babel/highlight": "^7.18.6"
+ "@babel/highlight": "^7.23.4",
+ "chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
- "version": "7.20.10",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz",
- "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz",
+ "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.20.12",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz",
- "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==",
- "dependencies": {
- "@ampproject/remapping": "^2.1.0",
- "@babel/code-frame": "^7.18.6",
- "@babel/generator": "^7.20.7",
- "@babel/helper-compilation-targets": "^7.20.7",
- "@babel/helper-module-transforms": "^7.20.11",
- "@babel/helpers": "^7.20.7",
- "@babel/parser": "^7.20.7",
- "@babel/template": "^7.20.7",
- "@babel/traverse": "^7.20.12",
- "@babel/types": "^7.20.7",
- "convert-source-map": "^1.7.0",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz",
+ "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.3",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-module-transforms": "^7.23.3",
+ "@babel/helpers": "^7.23.2",
+ "@babel/parser": "^7.23.3",
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.3",
+ "@babel/types": "^7.23.3",
+ "convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
- "json5": "^2.2.2",
- "semver": "^6.3.0"
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"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": {
}
},
"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",
}
},
"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": {
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.18.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.19.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
- "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dependencies": {
- "@babel/template": "^7.18.10",
- "@babel/types": "^7.19.0"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dependencies": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
- "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+ "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dependencies": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.20.11",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz",
- "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==",
- "dependencies": {
- "@babel/helper-environment-visitor": "^7.18.9",
- "@babel/helper-module-imports": "^7.18.6",
- "@babel/helper-simple-access": "^7.20.2",
- "@babel/helper-split-export-declaration": "^7.18.6",
- "@babel/helper-validator-identifier": "^7.19.1",
- "@babel/template": "^7.20.7",
- "@babel/traverse": "^7.20.10",
- "@babel/types": "^7.20.7"
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+ "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/helper-simple-access": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.20.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
- "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
+ "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-simple-access": {
- "version": "7.20.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
- "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
+ "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
"dependencies": {
- "@babel/types": "^7.20.2"
+ "@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dependencies": {
- "@babel/types": "^7.18.6"
+ "@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.19.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.19.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
- "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
+ "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
- "version": "7.20.13",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz",
- "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz",
+ "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==",
"dependencies": {
- "@babel/template": "^7.20.7",
- "@babel/traverse": "^7.20.13",
- "@babel/types": "^7.20.7"
+ "@babel/template": "^7.22.15",
+ "@babel/traverse": "^7.23.4",
+ "@babel/types": "^7.23.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.18.6",
- "chalk": "^2.0.0",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
}
},
"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"
},
}
},
"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"
}
},
"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"
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/@babel/template": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
- "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dependencies": {
- "@babel/code-frame": "^7.18.6",
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.20.13",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz",
- "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==",
- "dependencies": {
- "@babel/code-frame": "^7.18.6",
- "@babel/generator": "^7.20.7",
- "@babel/helper-environment-visitor": "^7.18.9",
- "@babel/helper-function-name": "^7.19.0",
- "@babel/helper-hoist-variables": "^7.18.6",
- "@babel/helper-split-export-declaration": "^7.18.6",
- "@babel/parser": "^7.20.13",
- "@babel/types": "^7.20.7",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz",
+ "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==",
+ "dependencies": {
+ "@babel/code-frame": "^7.23.4",
+ "@babel/generator": "^7.23.4",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.4",
+ "@babel/types": "^7.23.4",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
}
},
"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": {
}
},
"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",
"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",
"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",
"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",
"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": "*",
}
},
"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": "*"
}
},
"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": {
"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",
{
"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"
}
},
"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",
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
]
},
}
},
"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",
}
},
"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": {
}
},
"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",
}
},
"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",
"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,
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
},
"node_modules/jotai": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/jotai/-/jotai-1.12.0.tgz",
- "integrity": "sha512-IhyBmjxU1sE2Ni/MUK7gQAb8QvCM6yd1/K5jtQzgQBmmjCjgfXZkkk1rYlQAIRp2KoQk0Y+yzhm1f5cZ7kegnw==",
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.6.0.tgz",
+ "integrity": "sha512-Vt6hsc04Km4j03l+Ax+Sc+FVft5cRJhqgxt6GTz6GM2eM3DyX3CdBdzcG0z2FrlZToL1/0OAkqDghIyARWnSuQ==",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
- "@babel/core": "*",
- "@babel/template": "*",
- "jotai-immer": "*",
- "jotai-optics": "*",
- "jotai-redux": "*",
- "jotai-tanstack-query": "*",
- "jotai-urql": "*",
- "jotai-valtio": "*",
- "jotai-xstate": "*",
- "jotai-zustand": "*",
- "react": ">=16.8"
+ "@types/react": ">=17.0.0",
+ "react": ">=17.0.0"
},
"peerDependenciesMeta": {
- "@babel/core": {
- "optional": true
- },
- "@babel/template": {
- "optional": true
- },
- "jotai-immer": {
- "optional": true
- },
- "jotai-optics": {
- "optional": true
- },
- "jotai-redux": {
- "optional": true
- },
- "jotai-tanstack-query": {
- "optional": true
- },
- "jotai-urql": {
- "optional": true
- },
- "jotai-valtio": {
- "optional": true
- },
- "jotai-xstate": {
+ "@types/react": {
"optional": true
},
- "jotai-zustand": {
+ "react": {
"optional": true
}
}
"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",
}
},
"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",
}
},
"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",
}
],
"dependencies": {
- "nanoid": "^3.3.6",
+ "nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
]
},
"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"
}
},
"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"
},
}
},
"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": {
"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",
}
},
"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"
}
}
},
"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"
}
},
}
},
"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": {
}
},
"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"
}
}
},
"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",
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"picocolors": "^1.0.0"
},
"bin": {
- "browserslist-lint": "cli.js"
+ "update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"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": "*",
"less": {
"optional": true
},
+ "lightningcss": {
+ "optional": true
+ },
"sass": {
"optional": true
},
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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"
],
}
},
"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": {
"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": {
"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",
"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",
"@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",
"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"
}
}
--- /dev/null
+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);
+}
--- /dev/null
+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<ClientConfig> => {
+ 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);
+}
--- /dev/null
+import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
+import { useDebounce } from '../hooks/useDebounce';
+
+type ConfirmPasswordMatchProps = {
+ initialValue: boolean;
+ children: (
+ match: boolean,
+ doMatch: () => void,
+ passRef: RefObject<HTMLInputElement>,
+ confPassRef: RefObject<HTMLInputElement>
+ ) => ReactNode;
+};
+export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
+ const [match, setMatch] = useState(initialValue);
+ const passRef = useRef<HTMLInputElement>(null);
+ const confPassRef = useRef<HTMLInputElement>(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);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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 (
+ <Overlay open backdrop={<OverlayBackdrop />}>
+ <FocusTrap focusTrapOptions={{ initialFocus: false }}>
+ <Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
+ <Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
+ {children}
+ </Box>
+ <Box
+ style={{ padding: config.space.S200 }}
+ shrink="No"
+ justifyContent="Center"
+ alignItems="Center"
+ gap="200"
+ >
+ <Chip as="div" radii="Pill" outlined>
+ <Text as="span" size="T300">{`Step ${currentStep}/${stepCount}`}</Text>
+ </Chip>
+ <TooltipProvider
+ tooltip={
+ <Tooltip variant="Critical">
+ <Text>Exit</Text>
+ </Tooltip>
+ }
+ position="Top"
+ >
+ {(anchorRef) => (
+ <IconButton
+ ref={anchorRef}
+ variant="Critical"
+ size="300"
+ onClick={onCancel}
+ radii="Pill"
+ outlined
+ >
+ <Icon size="50" src={Icons.Cross} />
+ </IconButton>
+ )}
+ </TooltipProvider>
+ </Box>
+ </Box>
+ </FocusTrap>
+ </Overlay>
+ );
+}
--- /dev/null
+import React, { ComponentProps, forwardRef } from 'react';
+import { Icon, IconButton, Input, config, Icons } from 'folds';
+import { UseStateProvider } from '../UseStateProvider';
+
+type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> & {
+ size: '400' | '500';
+};
+export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
+ ({ variant, size, style, after, ...props }, ref) => {
+ const paddingRight: string = size === '500' ? config.space.S300 : config.space.S200;
+
+ return (
+ <UseStateProvider initial={false}>
+ {(visible, setVisible) => (
+ <Input
+ {...props}
+ ref={ref}
+ style={{ paddingRight, ...style }}
+ type={visible ? 'text' : 'password'}
+ size={size}
+ variant={variant}
+ after={
+ <>
+ {after}
+ <IconButton
+ onClick={() => setVisible(!visible)}
+ type="button"
+ variant={visible ? 'Warning' : variant}
+ size="300"
+ radii="300"
+ >
+ <Icon
+ style={{ opacity: config.opacity.P300 }}
+ size="100"
+ src={visible ? Icons.Eye : Icons.EyeBlind}
+ />
+ </IconButton>
+ </>
+ }
+ />
+ )}
+ </UseStateProvider>
+ );
+ }
+);
--- /dev/null
+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,
+});
--- /dev/null
+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 (
+ <Box
+ className={classNames(css.SplashScreen, patternsCSS.BackgroundDotPattern)}
+ direction="Column"
+ >
+ {children}
+ <Box
+ className={css.SplashScreenFooter}
+ shrink="No"
+ alignItems="Center"
+ justifyContent="Center"
+ >
+ <Text size="H2" align="Center">
+ Cinny
+ </Text>
+ </Box>
+ </Box>
+ );
+}
--- /dev/null
+export * from './SplashScreen';
--- /dev/null
+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 (
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Box direction="Column" gap="100">
+ <Text size="H4">{title}</Text>
+ <Text>{message}</Text>
+ </Box>
+ <Button variant="Critical" onClick={onRetry}>
+ <Text as="span" size="B400">
+ Retry
+ </Text>
+ </Button>
+ <Button variant="Critical" fill="None" outlined onClick={onCancel}>
+ <Text as="span" size="B400">
+ Cancel
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+}
+
+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 (
+ <DummyErrorDialog
+ title={errorCode}
+ message={error ?? 'Failed to register.'}
+ onRetry={handleSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ return null;
+}
--- /dev/null
+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<HTMLFormElement> = (evt) => {
+ evt.preventDefault();
+ const { retryEmailInput } = evt.target as HTMLFormElement & {
+ retryEmailInput: HTMLInputElement;
+ };
+ const t = retryEmailInput.value;
+ onRetry(t);
+ };
+
+ return (
+ <Dialog>
+ <Box
+ as="form"
+ onSubmit={handleFormSubmit}
+ style={{ padding: config.space.S400 }}
+ direction="Column"
+ gap="400"
+ >
+ <Box direction="Column" gap="100">
+ <Text size="H4">{title}</Text>
+ <Text>{message}</Text>
+ <Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
+ Email
+ </Text>
+ <Input
+ name="retryEmailInput"
+ variant="Background"
+ size="500"
+ outlined
+ defaultValue={defaultEmail}
+ required
+ />
+ </Box>
+ <Button variant="Primary" type="submit">
+ <Text as="span" size="B400">
+ Send Verification Email
+ </Text>
+ </Button>
+ <Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
+ <Text as="span" size="B400">
+ Cancel
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+}
+
+export function EmailStageDialog({
+ email,
+ clientSecret,
+ stageData,
+ emailTokenState,
+ requestEmailToken,
+ submitAuthDict,
+ onCancel,
+}: StageComponentProps & {
+ email?: string;
+ clientSecret: string;
+ emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
+ 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 (
+ <Box direction="Column" alignItems="Center" gap="400">
+ <Spinner variant="Secondary" size="600" />
+ <Text style={{ color: color.Secondary.Main }}>Sending verification email...</Text>
+ </Box>
+ );
+ }
+
+ if (emailTokenState.status === AsyncStatus.Error) {
+ return (
+ <EmailErrorDialog
+ title={emailTokenState.error.errcode ?? 'Verify Email'}
+ message={
+ emailTokenState.error?.data?.error ??
+ emailTokenState.error.message ??
+ 'Failed to send verification Email request.'
+ }
+ onRetry={handleEmailSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ if (emailTokenState.status === AsyncStatus.Success) {
+ return (
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Box direction="Column" gap="100">
+ <Text size="H4">Verification Request Sent</Text>
+ <Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
+
+ {errorCode && (
+ <Text style={{ color: color.Critical.Main }}>{`${errorCode}: ${error}`}</Text>
+ )}
+ </Box>
+ <Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
+ <Text as="span" size="B400">
+ Continue
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+ }
+
+ if (!email) {
+ return (
+ <EmailErrorDialog
+ title="Provide Email"
+ message="Please provide email to send verification request."
+ onRetry={handleEmailSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ return null;
+}
--- /dev/null
+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 (
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Box direction="Column" gap="100">
+ <Text size="H4">{title}</Text>
+ <Text>{message}</Text>
+ </Box>
+ <Button variant="Critical" fill="None" outlined onClick={onCancel}>
+ <Text as="span" size="B400">
+ Cancel
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+}
+
+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 (
+ <ReCaptchaErrorDialog
+ title="Invalid Data"
+ message="No valid data found to proceed with ReCAPTCHA."
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ return (
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Text>Please check the box below to proceed.</Text>
+ <ReCAPTCHA sitekey={publicKey} onChange={handleChange} />
+ </Box>
+ </Dialog>
+ );
+}
--- /dev/null
+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<HTMLFormElement> = (evt) => {
+ evt.preventDefault();
+ const { retryTokenInput } = evt.target as HTMLFormElement & {
+ retryTokenInput: HTMLInputElement;
+ };
+ const t = retryTokenInput.value;
+ onRetry(t);
+ };
+
+ return (
+ <Dialog>
+ <Box
+ as="form"
+ onSubmit={handleFormSubmit}
+ style={{ padding: config.space.S400 }}
+ direction="Column"
+ gap="400"
+ >
+ <Box direction="Column" gap="100">
+ <Text size="H4">{title}</Text>
+ <Text>{message}</Text>
+ <Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
+ Registration Token
+ </Text>
+ <Input
+ name="retryTokenInput"
+ variant="Background"
+ size="500"
+ outlined
+ defaultValue={defaultToken}
+ required
+ />
+ </Box>
+ <Button variant="Critical" type="submit">
+ <Text as="span" size="B400">
+ Retry
+ </Text>
+ </Button>
+ <Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
+ <Text as="span" size="B400">
+ Cancel
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+}
+
+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 (
+ <RegistrationTokenErrorDialog
+ defaultToken={token}
+ title={errorCode}
+ message={error ?? 'Invalid registration token provided.'}
+ onRetry={handleSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ if (!token) {
+ return (
+ <RegistrationTokenErrorDialog
+ defaultToken={token}
+ title="Registration Token"
+ message="Please submit registration token provided by you homeserver admin."
+ onRetry={handleSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ return null;
+}
--- /dev/null
+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 (
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Box direction="Column" gap="100">
+ <Text size="H4">{title}</Text>
+ <Text>{message}</Text>
+ </Box>
+ <Button variant="Critical" onClick={onRetry}>
+ <Text as="span" size="B400">
+ Retry
+ </Text>
+ </Button>
+ <Button variant="Critical" fill="None" outlined onClick={onCancel}>
+ <Text as="span" size="B400">
+ Cancel
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ );
+}
+
+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 (
+ <TermsErrorDialog
+ title={errorCode}
+ message={error ?? 'Failed to submit Terms and Condition Acceptance.'}
+ onRetry={handleSubmit}
+ onCancel={onCancel}
+ />
+ );
+ }
+
+ return null;
+}
--- /dev/null
+export * from './types';
+export * from './DummyStage';
+export * from './EmailStage';
+export * from './ReCaptchaStage';
+export * from './RegistrationTokenStage';
+export * from './TermsStage';
--- /dev/null
+import { AuthDict } from 'matrix-js-sdk';
+import { AuthStageData } from '../../hooks/useUIAFlows';
+
+export type StageComponentProps = {
+ stageData: AuthStageData;
+ submitAuthDict: (authDict: AuthDict) => void;
+ onCancel: () => void;
+};
--- /dev/null
+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<string, unknown> & {
+ '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<AutoDiscoveryInfo>(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<string, boolean>;
+};
+export const specVersions = async (
+ request: typeof fetch,
+ baseUrl: string
+): Promise<SpecVersions> => {
+ 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');
+};
--- /dev/null
+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',
+}
--- /dev/null
+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<RequestEmailTokenResponse>;
-import { useCallback, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
+import { flushSync } from 'react-dom';
import { useAlive } from './useAlive';
export enum AsyncStatus {
status: AsyncStatus.Loading;
};
-export type AsyncSuccess<T> = {
+export type AsyncSuccess<D> = {
status: AsyncStatus.Success;
- data: T;
+ data: D;
};
-export type AsyncError = {
+export type AsyncError<E = unknown> = {
status: AsyncStatus.Error;
- error: unknown;
+ error: E;
};
-export type AsyncState<T> = AsyncIdle | AsyncLoading | AsyncSuccess<T> | AsyncError;
+export type AsyncState<D, E = unknown> = AsyncIdle | AsyncLoading | AsyncSuccess<D> | AsyncError<E>;
export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
-export const useAsyncCallback = <TArgs extends unknown[], TData>(
+export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
asyncCallback: AsyncCallback<TArgs, TData>
-): [AsyncState<TData>, AsyncCallback<TArgs, TData>] => {
- const [state, setState] = useState<AsyncState<TData>>({
+): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
+ const [state, setState] = useState<AsyncState<TData, TError>>({
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<TArgs, TData> = 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,
}
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;
--- /dev/null
+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<RegisterFlowStatus, RegisterFlowStatus.FlowRequired>;
+ };
+
+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<AuthFlows | null>(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;
+};
--- /dev/null
+import { createContext, useContext } from 'react';
+
+const AuthServerContext = createContext<string | null>(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;
+};
--- /dev/null
+import { createContext, useContext } from 'react';
+import { AutoDiscoveryInfo } from '../cs-api';
+
+const AutoDiscoverInfoContext = createContext<AutoDiscoveryInfo | null>(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;
+};
--- /dev/null
+import { createContext, useContext } from 'react';
+
+export type ClientConfig = {
+ defaultHomeserver?: number;
+ homeserverList?: string[];
+ allowCustomHomeservers?: boolean;
+
+ hashRouter?: {
+ enabled?: boolean;
+ basename?: string;
+ };
+};
+
+const ClientConfigContext = createContext<ClientConfig | null>(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;
+};
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);
--- /dev/null
+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<ISSOFlow, 'identity_providers'>;
+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<ParsedLoginFlows>(
+ () => ({
+ password: getPasswordFlow(loginFlows),
+ token: getTokenFlow(loginFlows),
+ sso: getSSOFlow(loginFlows),
+ }),
+ [loginFlows]
+ );
+
+ return parsedFlow;
+};
--- /dev/null
+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<RequestEmailTokenResponse, MatrixError>, 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<RequestEmailTokenCallback>
+ >(passwordEmailCallback);
+
+ return [passwordEmailState, passwordEmail];
+};
--- /dev/null
+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;
+};
--- /dev/null
+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<RequestEmailTokenResponse, MatrixError>, 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<RequestEmailTokenCallback>
+ >(registerEmailCallback);
+
+ return [registerEmailState, registerEmail];
+};
--- /dev/null
+import { createContext, useContext } from 'react';
+import { SpecVersions } from '../cs-api';
+
+const SpecVersionsContext = createContext<SpecVersions | null>(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;
+}
--- /dev/null
+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<string, unknown>;
+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,
+ };
+};
const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
- useEffect(() => isMountedStore.setItem(true), []);
+ useEffect(() => {
+ isMountedStore.setItem(true)
+ }, []);
useEffect(() => {
let isUnmounted = false;
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;
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;
const mountStore = useStore(roomId);
const mx = initMatrix.matrixClient;
- useEffect(() => mountStore.setItem(true), [roomId]);
+ useEffect(() => {
+ mountStore.setItem(true)
+ }, [roomId]);
useEffect(() => {
if (searchData?.results?.length > 0) {
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;
}
};
- if (request === null) return null;
+ if (request === null) return undefined;
const req = request;
req.on('change', handleChange);
return () => {
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;
};
}, [roomId]);
- useEffect(() => setSelected([]), [spacePath]);
+ useEffect(() => {
+ setSelected([]);
+ }, [spacePath]);
const handleSelected = (selectedRoomId) => {
const newSelected = [...selected];
+++ /dev/null
-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 (
- <StrictMode>
- <Provider>{isAuthenticated() ? <Client /> : <Auth />}</Provider>
- </StrictMode>
- );
-}
-
-export default App;
--- /dev/null
+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(
+ <Route>
+ <Route
+ path={ROOT_PATH}
+ loader={() => {
+ if (isAuthenticated()) return redirect('/home');
+ return redirect(getLoginPath());
+ }}
+ />
+ <Route loader={authLayoutLoader} element={<AuthLayout />}>
+ <Route path={LOGIN_PATH} element={<Login />} />
+ <Route path={REGISTER_PATH} element={<Register />} />
+ <Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
+ </Route>
+
+ <Route
+ loader={() => {
+ if (!isAuthenticated()) return redirect(getLoginPath());
+ return null;
+ }}
+ >
+ <Route path="/home" element={<Client />} />
+ <Route path="/direct" element={<p>direct</p>} />
+ <Route path="/:spaceIdOrAlias" element={<p>:spaceIdOrAlias</p>} />
+ <Route path="/explore" element={<p>explore</p>} />
+ </Route>
+ <Route path="/*" element={<p>Page not found</p>} />
+ </Route>
+ );
+
+ 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 (
+ <ClientConfigLoader
+ fallback={() => <ConfigConfigLoading />}
+ error={(err, retry, ignore) => (
+ <ConfigConfigError error={err} retry={retry} ignore={ignore} />
+ )}
+ >
+ {(clientConfig) => (
+ <ClientConfigProvider value={clientConfig}>
+ <JotaiProvider>
+ <RouterProvider router={createRouter(clientConfig)} />
+ </JotaiProvider>
+ </ClientConfigProvider>
+ )}
+ </ClientConfigLoader>
+ );
+}
+
+export default App;
--- /dev/null
+import { Box, Button, Dialog, Spinner, Text, color, config } from 'folds';
+import React from 'react';
+import { SplashScreen } from '../components/splash-screen';
+
+export function ConfigConfigLoading() {
+ return (
+ <SplashScreen>
+ <Box grow="Yes" direction="Column" gap="400" alignItems="Center" justifyContent="Center">
+ <Spinner variant="Secondary" size="600" />
+ <Text>Heating up</Text>
+ </Box>
+ </SplashScreen>
+ );
+}
+
+type ConfigConfigErrorProps = {
+ error: unknown;
+ retry: () => void;
+ ignore: () => void;
+};
+export function ConfigConfigError({ error, retry, ignore }: ConfigConfigErrorProps) {
+ return (
+ <SplashScreen>
+ <Box grow="Yes" direction="Column" gap="400" alignItems="Center" justifyContent="Center">
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Box direction="Column" gap="100">
+ <Text>Failed to load client configuration file.</Text>
+ {typeof error === 'object' &&
+ error &&
+ 'message' in error &&
+ typeof error.message === 'string' && (
+ <Text size="T300" style={{ color: color.Critical.Main }}>
+ {error.message}
+ </Text>
+ )}
+ </Box>
+ <Button variant="Critical" onClick={retry}>
+ <Text as="span" size="B400">
+ Retry
+ </Text>
+ </Button>
+ <Button variant="Critical" onClick={ignore} fill="Soft">
+ <Text as="span" size="B400">
+ Continue
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ </Box>
+ </SplashScreen>
+ );
+}
--- /dev/null
+import React from 'react';
+import { Box, Text } from 'folds';
+import * as css from './styles.css';
+
+export function AuthFooter() {
+ return (
+ <Box className={css.AuthFooter} justifyContent="Center" gap="400" wrap="Wrap">
+ <Text as="a" size="T300" href="https://cinny.in" target="_blank" rel="noreferrer">
+ About
+ </Text>
+ <Text
+ as="a"
+ size="T300"
+ href="https://github.com/ajbura/cinny/releases"
+ target="_blank"
+ rel="noreferrer"
+ >
+ v3.2.0
+ </Text>
+ <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
+ Twitter
+ </Text>
+ <Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
+ Powered by Matrix
+ </Text>
+ </Box>
+ );
+}
--- /dev/null
+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 (
+ <Box justifyContent="Center" alignItems="Center" gap="200">
+ <Spinner size="100" variant="Secondary" />
+ <Text align="Center" size="T300">
+ {message}
+ </Text>
+ </Box>
+ );
+}
+
+function AuthLayoutError({ message }: { message: string }) {
+ return (
+ <Box justifyContent="Center" alignItems="Center" gap="200">
+ <Text align="Center" style={{ color: color.Critical.Main }} size="T300">
+ {message}
+ </Text>
+ </Box>
+ );
+}
+
+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 (
+ <Scroll variant="Background" visibility="Hover" size="300" hideTrack>
+ <Box
+ className={classNames(css.AuthLayout, PatternsCss.BackgroundDotPattern)}
+ direction="Column"
+ alignItems="Center"
+ justifyContent="SpaceBetween"
+ gap="400"
+ >
+ <Box direction="Column" className={css.AuthCard}>
+ <Header className={css.AuthHeader} size="600" variant="Surface">
+ <Box grow="Yes" direction="Row" gap="300" alignItems="Center">
+ <img className={css.AuthLogo} src={CinnySVG} alt="Cinny Logo" />
+ <Text size="H3">Cinny</Text>
+ </Box>
+ </Header>
+ <Box className={css.AuthCardContent} direction="Column">
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Homeserver
+ </Text>
+ <ServerPicker
+ server={server}
+ serverList={clientConfig.homeserverList ?? []}
+ allowCustomServer={clientConfig.allowCustomHomeservers}
+ onServerChange={selectServer}
+ />
+ </Box>
+ {discoveryState.status === AsyncStatus.Loading && (
+ <AuthLayoutLoading message="Looking for homeserver..." />
+ )}
+ {discoveryState.status === AsyncStatus.Error && (
+ <AuthLayoutError message="Failed to find homeserver." />
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
+ <AuthLayoutError
+ message={`Failed to connect. Homeserver configuration found with ${autoDiscoveryError.host} appears unusable.`}
+ />
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
+ <AuthLayoutError message="Failed to connect. Homeserver configuration base_url appears invalid." />
+ )}
+ {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
+ <AuthServerProvider value={discoveryState.data.serverName}>
+ <AutoDiscoveryInfoProvider value={autoDiscoveryInfo}>
+ <SpecVersionsLoader
+ fallback={() => (
+ <AuthLayoutLoading
+ message={`Connecting to ${autoDiscoveryInfo['m.homeserver'].base_url}`}
+ />
+ )}
+ error={() => (
+ <AuthLayoutError message="Failed to connect. Either homeserver is unavailable at this moment or does not exist." />
+ )}
+ >
+ {(specVersions) => (
+ <SpecVersionsProvider value={specVersions}>
+ <AuthFlowsLoader
+ fallback={() => (
+ <AuthLayoutLoading message="Loading authentication flow..." />
+ )}
+ error={() => (
+ <AuthLayoutError message="Failed to get authentication flow information." />
+ )}
+ >
+ {(authFlows) => (
+ <AuthFlowsProvider value={authFlows}>
+ <Outlet />
+ </AuthFlowsProvider>
+ )}
+ </AuthFlowsLoader>
+ </SpecVersionsProvider>
+ )}
+ </SpecVersionsLoader>
+ </AutoDiscoveryInfoProvider>
+ </AuthServerProvider>
+ )}
+ </Box>
+ </Box>
+ <AuthFooter />
+ </Box>
+ </Scroll>
+ );
+}
--- /dev/null
+import React from 'react';
+import { Box, Icon, Icons, color, Text } from 'folds';
+
+export function FieldError({ message }: { message: string }) {
+ return (
+ <Box style={{ color: color.Critical.Main }} alignItems="Center" gap="100">
+ <Icon size="50" filled src={Icons.Warning} />
+ <Text size="T200">
+ <b>{message}</b>
+ </Text>
+ </Box>
+ );
+}
--- /dev/null
+import React from 'react';
+import { Box, Line, Text } from 'folds';
+
+export function OrDivider() {
+ return (
+ <Box gap="400" alignItems="Center">
+ <Line style={{ flexGrow: 1 }} direction="Horizontal" size="300" variant="Surface" />
+ <Text>OR</Text>
+ <Line style={{ flexGrow: 1 }} direction="Horizontal" size="300" variant="Surface" />
+ </Box>
+ );
+}
--- /dev/null
+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 (
+ <Box justifyContent="Center" gap="600" wrap="Wrap">
+ {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 (
+ <Avatar
+ style={{ cursor: 'pointer' }}
+ key={id}
+ as="a"
+ href={getSSOIdUrl(id)}
+ aria-label={buttonTitle}
+ size="300"
+ radii="300"
+ >
+ <AvatarImage src={iconUrl} alt={name} title={buttonTitle} />
+ </Avatar>
+ );
+ }
+
+ return (
+ <Button
+ style={{ width: '100%' }}
+ key={id}
+ as="a"
+ href={getSSOIdUrl(id)}
+ size="500"
+ variant="Secondary"
+ fill="Soft"
+ outlined
+ before={
+ iconUrl && (
+ <Avatar size="200" radii="300">
+ <AvatarImage src={iconUrl} alt={name} />
+ </Avatar>
+ )
+ }
+ >
+ <Text align="Center" size="B500" truncate>
+ {buttonTitle}
+ </Text>
+ </Button>
+ );
+ })}
+ </Box>
+ );
+}
--- /dev/null
+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<HTMLInputElement>(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<HTMLInputElement> = (evt) => {
+ const inputServer = evt.target.value.trim();
+ if (inputServer) debounceServerSelect(inputServer);
+ };
+
+ const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (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<HTMLButtonElement> = (evt) => {
+ const selectedServer = evt.currentTarget.getAttribute('data-server');
+ if (selectedServer) {
+ onServerChange(selectedServer);
+ }
+ setServerMenu(false);
+ };
+
+ return (
+ <Input
+ ref={serverInputRef}
+ style={{ paddingRight: config.space.S200 }}
+ variant={allowCustomServer ? 'Background' : 'Surface'}
+ outlined
+ defaultValue={server}
+ onChange={handleServerChange}
+ onKeyDown={handleKeyDown}
+ size="500"
+ readOnly={!allowCustomServer}
+ onClick={allowCustomServer ? undefined : () => setServerMenu(true)}
+ after={
+ serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : (
+ <PopOut
+ open={serverMenu}
+ position="Bottom"
+ align="End"
+ offset={4}
+ content={
+ <FocusTrap
+ focusTrapOptions={{
+ initialFocus: false,
+ onDeactivate: () => setServerMenu(false),
+ clickOutsideDeactivates: true,
+ isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
+ isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
+ }}
+ >
+ <Menu>
+ <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
+ <Text size="L400">Homeserver List</Text>
+ </Header>
+ <div style={{ padding: config.space.S100, paddingTop: 0 }}>
+ {serverList?.map((serverName) => (
+ <MenuItem
+ key={serverName}
+ radii="300"
+ aria-pressed={serverName === server}
+ data-server={serverName}
+ onClick={handleServerSelect}
+ >
+ <Text>{serverName}</Text>
+ </MenuItem>
+ ))}
+ </div>
+ </Menu>
+ </FocusTrap>
+ }
+ >
+ {(anchorRef) => (
+ <IconButton
+ ref={anchorRef}
+ onClick={() => setServerMenu(true)}
+ variant={allowCustomServer ? 'Background' : 'Surface'}
+ size="300"
+ aria-pressed={serverMenu}
+ radii="300"
+ >
+ <Icon src={Icons.ChevronBottom} />
+ </IconButton>
+ )}
+ </PopOut>
+ )
+ }
+ />
+ );
+}
--- /dev/null
+export * from './AuthLayout';
+export * from './login';
+export * from './register';
+export * from './reset-password';
--- /dev/null
+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 (
+ <Box direction="Column" gap="500">
+ <Text size="H2" priority="400">
+ Login
+ </Text>
+ {parsedFlows.token && loginSearchParams.loginToken && (
+ <TokenLogin token={loginSearchParams.loginToken} />
+ )}
+ {parsedFlows.password && (
+ <>
+ <PasswordLoginForm
+ defaultUsername={loginSearchParams.username}
+ defaultEmail={loginSearchParams.email}
+ />
+ <span data-spacing-node />
+ {parsedFlows.sso && <OrDivider />}
+ </>
+ )}
+ {parsedFlows.sso && (
+ <>
+ <SSOLogin
+ providers={parsedFlows.sso.identity_providers}
+ redirectUrl={ssoRedirectUrl}
+ asIcons={
+ parsedFlows.password !== undefined && parsedFlows.sso.identity_providers.length > 2
+ }
+ />
+ <span data-spacing-node />
+ </>
+ )}
+ {!parsedFlows.password && !parsedFlows.sso && (
+ <>
+ <Text style={{ color: color.Critical.Main }}>
+ {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`}
+ </Text>
+ <span data-spacing-node />
+ </>
+ )}
+ <Text align="Center">
+ Do not have an account? <Link to={getRegisterPath(server)}>Register</Link>
+ </Text>
+ </Box>
+ );
+}
--- /dev/null
+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 (
+ <PopOut
+ open={open}
+ position="Top"
+ align="End"
+ content={
+ <FocusTrap
+ focusTrapOptions={{
+ initialFocus: false,
+ onDeactivate: () => setOpen(false),
+ clickOutsideDeactivates: true,
+ }}
+ >
+ <Menu>
+ <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
+ <Text size="L400">Hint</Text>
+ </Header>
+ <Box
+ style={{ padding: config.space.S200, paddingTop: 0 }}
+ direction="Column"
+ tabIndex={0}
+ gap="100"
+ >
+ <Text size="T300">
+ <Text as="span" size="Inherit" priority="300">
+ Username:
+ </Text>{' '}
+ johndoe
+ </Text>
+ <Text size="T300">
+ <Text as="span" size="Inherit" priority="300">
+ Matrix ID:
+ </Text>
+ {` @johndoe:${server}`}
+ </Text>
+ <Text size="T300">
+ <Text as="span" size="Inherit" priority="300">
+ Email:
+ </Text>
+ {` johndoe@${server}`}
+ </Text>
+ </Box>
+ </Menu>
+ </FocusTrap>
+ }
+ >
+ {(targetRef) => (
+ <IconButton
+ tabIndex={-1}
+ onClick={() => setOpen(true)}
+ ref={targetRef}
+ type="button"
+ variant="Background"
+ size="300"
+ radii="300"
+ aria-pressed={open}
+ >
+ <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
+ </IconButton>
+ )}
+ </PopOut>
+ );
+}
+
+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<typeof login>
+ >(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<HTMLFormElement> = (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 (
+ <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Username
+ </Text>
+ <Input
+ defaultValue={defaultUsername ?? defaultEmail}
+ style={{ paddingRight: config.space.S300 }}
+ name="usernameInput"
+ variant="Background"
+ size="500"
+ required
+ outlined
+ after={<UsernameHint server={server} />}
+ />
+ {loginState.status === AsyncStatus.Error && (
+ <>
+ {loginState.error.errcode === LoginError.ServerNotAllowed && (
+ <FieldError message="Login with custom server not allowed by your client instance." />
+ )}
+ {loginState.error.errcode === LoginError.InvalidServer && (
+ <FieldError message="Failed to find your Matrix ID server." />
+ )}
+ </>
+ )}
+ </Box>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Password
+ </Text>
+ <PasswordInput name="passwordInput" variant="Background" size="500" outlined required />
+ <Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
+ {loginState.status === AsyncStatus.Error && (
+ <>
+ {loginState.error.errcode === LoginError.Forbidden && (
+ <FieldError message="Invalid Username or Password." />
+ )}
+ {loginState.error.errcode === LoginError.UserDeactivated && (
+ <FieldError message="This account has been deactivated." />
+ )}
+ {loginState.error.errcode === LoginError.InvalidRequest && (
+ <FieldError message="Failed to login. Part of your request data is invalid." />
+ )}
+ {loginState.error.errcode === LoginError.RateLimited && (
+ <FieldError message="Failed to login. Your login request has been rate-limited by server, Please try after some time." />
+ )}
+ {loginState.error.errcode === LoginError.Unknown && (
+ <FieldError message="Failed to login. Unknown reason." />
+ )}
+ </>
+ )}
+ <Box grow="Yes" shrink="No" justifyContent="End">
+ <Text as="span" size="T200" priority="400" align="Right">
+ <Link to={getResetPasswordPath(server)}>Forget Password?</Link>
+ </Text>
+ </Box>
+ </Box>
+ </Box>
+ <Button type="submit" variant="Primary" size="500">
+ <Text as="span" size="B500">
+ Login
+ </Text>
+ </Button>
+
+ <Overlay
+ open={
+ loginState.status === AsyncStatus.Loading || loginState.status === AsyncStatus.Success
+ }
+ backdrop={<OverlayBackdrop />}
+ >
+ <OverlayCenter>
+ <Spinner variant="Secondary" size="600" />
+ </OverlayCenter>
+ </Overlay>
+ </Box>
+ );
+}
--- /dev/null
+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 (
+ <Box
+ style={{
+ backgroundColor: color.Critical.Container,
+ color: color.Critical.OnContainer,
+ padding: config.space.S300,
+ borderRadius: config.radii.R400,
+ }}
+ justifyContent="Start"
+ alignItems="Start"
+ gap="300"
+ >
+ <Icon size="300" filled src={Icons.Warning} />
+ <Box direction="Column" gap="100">
+ <Text size="L400">Token Login</Text>
+ <Text size="T300">
+ <b>{message}</b>
+ </Text>
+ </Box>
+ </Box>
+ );
+}
+
+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<typeof login>
+ >(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 && (
+ <LoginTokenError message="Invalid login token." />
+ )}
+ {loginState.error.errcode === LoginError.UserDeactivated && (
+ <LoginTokenError message="This account has been deactivated." />
+ )}
+ {loginState.error.errcode === LoginError.InvalidRequest && (
+ <LoginTokenError message="Failed to login. Part of your request data is invalid." />
+ )}
+ {loginState.error.errcode === LoginError.RateLimited && (
+ <LoginTokenError message="Failed to login. Your login request has been rate-limited by server, Please try after some time." />
+ )}
+ {loginState.error.errcode === LoginError.Unknown && (
+ <LoginTokenError message="Failed to login. Unknown reason." />
+ )}
+ </>
+ )}
+ <Overlay open={loginState.status !== AsyncStatus.Error} backdrop={<OverlayBackdrop />}>
+ <OverlayCenter>
+ <Spinner size="600" variant="Secondary" />
+ </OverlayCenter>
+ </Overlay>
+ </>
+ );
+}
--- /dev/null
+export * from './Login';
--- /dev/null
+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<string> => {
+ 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<string>),
+ data: LoginRequest
+): Promise<CustomLoginResponse> => {
+ 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<LoginResponse, MatrixError>(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]);
+};
--- /dev/null
+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<RequestEmailTokenResponse, MatrixError>;
+ 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 (
+ <UIAFlowOverlay
+ currentStep={completed.length + 1}
+ stepCount={flow.stages.length}
+ onCancel={handleCancel}
+ >
+ {stageToComplete.type === AuthType.RegistrationToken && (
+ <RegistrationTokenStageDialog
+ token={formData.token}
+ stageData={stageToComplete}
+ submitAuthDict={handleAuthDict}
+ onCancel={handleCancel}
+ />
+ )}
+ {stageToComplete.type === AuthType.Terms && (
+ <AutoTermsStageDialog
+ stageData={stageToComplete}
+ submitAuthDict={handleAuthDict}
+ onCancel={handleCancel}
+ />
+ )}
+ {stageToComplete.type === AuthType.Recaptcha && (
+ <ReCaptchaStageDialog
+ stageData={stageToComplete}
+ submitAuthDict={handleAuthDict}
+ onCancel={handleCancel}
+ />
+ )}
+ {stageToComplete.type === AuthType.Email && (
+ <EmailStageDialog
+ email={formData.email}
+ clientSecret={formData.clientSecret}
+ stageData={stageToComplete}
+ requestEmailToken={registerEmail}
+ emailTokenState={registerEmailState}
+ submitAuthDict={handleAuthDict}
+ onCancel={handleCancel}
+ />
+ )}
+ {stageToComplete.type === AuthType.Dummy && (
+ <AutoDummyStageDialog
+ stageData={stageToComplete}
+ submitAuthDict={handleAuthDict}
+ onCancel={handleCancel}
+ />
+ )}
+ </UIAFlowOverlay>
+ );
+}
+
+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<FormData>();
+
+ const [ongoingFlow, setOngoingFlow] = useState<UIAFlow>();
+
+ 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<HTMLFormElement> = (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 (
+ <>
+ <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Username
+ </Text>
+ <Input
+ variant="Background"
+ defaultValue={defaultUsername}
+ name="usernameInput"
+ size="500"
+ outlined
+ required
+ />
+ {registerError?.errcode === RegisterError.UserTaken && (
+ <FieldError message="This username is already taken." />
+ )}
+ {registerError?.errcode === RegisterError.UserInvalid && (
+ <FieldError message="This username contains invalid characters." />
+ )}
+ {registerError?.errcode === RegisterError.UserExclusive && (
+ <FieldError message="This username is reserved." />
+ )}
+ </Box>
+ <ConfirmPasswordMatch initialValue>
+ {(match, doMatch, passRef, confPassRef) => (
+ <>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Password
+ </Text>
+ <PasswordInput
+ ref={passRef}
+ onChange={doMatch}
+ name="passwordInput"
+ variant="Background"
+ size="500"
+ outlined
+ required
+ />
+ {registerError?.errcode === RegisterError.PasswordWeak && (
+ <FieldError
+ message={
+ registerError.data.error ??
+ 'Weak Password. Password rejected by server please choosing more strong Password.'
+ }
+ />
+ )}
+ {registerError?.errcode === RegisterError.PasswordShort && (
+ <FieldError
+ message={
+ registerError.data.error ??
+ 'Short Password. Password rejected by server please choosing more long Password.'
+ }
+ />
+ )}
+ </Box>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Confirm Password
+ </Text>
+ <PasswordInput
+ ref={confPassRef}
+ onChange={doMatch}
+ name="confirmPasswordInput"
+ variant="Background"
+ size="500"
+ style={{ color: match ? undefined : color.Critical.Main }}
+ outlined
+ required
+ />
+ </Box>
+ </>
+ )}
+ </ConfirmPasswordMatch>
+ {hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && (
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ {requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)
+ ? 'Registration Token'
+ : 'Registration Token (Optional)'}
+ </Text>
+ <Input
+ variant="Background"
+ defaultValue={defaultRegisterToken}
+ name="tokenInput"
+ size="500"
+ required={requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)}
+ outlined
+ />
+ </Box>
+ )}
+ {hasStageInFlows(uiaFlows, AuthType.Email) && (
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ {requiredStageInFlows(uiaFlows, AuthType.Email) ? 'Email' : 'Email (Optional)'}
+ </Text>
+ <Input
+ variant="Background"
+ defaultValue={defaultEmail}
+ name="emailInput"
+ type="email"
+ size="500"
+ required={requiredStageInFlows(uiaFlows, AuthType.Email)}
+ outlined
+ />
+ </Box>
+ )}
+
+ {hasStageInFlows(uiaFlows, AuthType.Terms) && termUrl && (
+ <Box alignItems="Center" gap="200">
+ <Checkbox name="termsInput" size="300" variant="Primary" required />
+ <Text size="T300">
+ I accept server{' '}
+ <a href={termUrl} target="_blank" rel="noreferrer">
+ Terms and Conditions
+ </a>
+ .
+ </Text>
+ </Box>
+ )}
+ {registerError?.errcode === RegisterError.RateLimited && (
+ <FieldError message="Failed to register. Your register request has been rate-limited by server, Please try after some time." />
+ )}
+ {registerError?.errcode === RegisterError.Forbidden && (
+ <FieldError message="Failed to register. The homeserver does not permit registration." />
+ )}
+ {registerError?.errcode === RegisterError.InvalidRequest && (
+ <FieldError message="Failed to register. Invalid request." />
+ )}
+ {registerError?.errcode === RegisterError.Unknown && (
+ <FieldError message={registerError.data.error ?? 'Failed to register. Unknown Reason.'} />
+ )}
+ <span data-spacing-node />
+ <Button variant="Primary" size="500" type="submit">
+ <Text as="span" size="B500">
+ Register
+ </Text>
+ </Button>
+ </Box>
+ {registerState.status === AsyncStatus.Success &&
+ formData &&
+ ongoingFlow &&
+ ongoingAuthData && (
+ <RegisterUIAFlow
+ formData={formData}
+ flow={ongoingFlow}
+ authData={ongoingAuthData}
+ registerEmail={registerEmail}
+ registerEmailState={registerEmailState}
+ onRegister={handleRegister}
+ />
+ )}
+ {registerState.status === AsyncStatus.Loading && (
+ <Overlay open backdrop={<OverlayBackdrop />}>
+ <OverlayCenter>
+ <Spinner variant="Secondary" size="600" />
+ </OverlayCenter>
+ </Overlay>
+ )}
+ </>
+ );
+}
--- /dev/null
+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 (
+ <Box direction="Column" gap="500">
+ <Text size="H2" priority="400">
+ Register
+ </Text>
+ {registerFlows.status === RegisterFlowStatus.RegistrationDisabled && !sso && (
+ <Text style={{ color: color.Critical.Main }} size="T300">
+ Registration has been disabled on this homeserver.
+ </Text>
+ )}
+ {registerFlows.status === RegisterFlowStatus.RateLimited && !sso && (
+ <Text style={{ color: color.Critical.Main }} size="T300">
+ You have been rate-limited! Please try after some time.
+ </Text>
+ )}
+ {registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && (
+ <Text style={{ color: color.Critical.Main }} size="T300">
+ Invalid Request! Failed to get any registration options.
+ </Text>
+ )}
+ {registerFlows.status === RegisterFlowStatus.FlowRequired && (
+ <>
+ <SupportedUIAFlowsLoader
+ flows={registerFlows.data.flows ?? []}
+ supportedStages={SUPPORTED_REGISTER_STAGES}
+ >
+ {(supportedFlows) =>
+ supportedFlows.length === 0 ? (
+ <Text style={{ color: color.Critical.Main }} size="T300">
+ This application does not support registration on this homeserver.
+ </Text>
+ ) : (
+ <PasswordRegisterForm
+ authData={registerFlows.data}
+ uiaFlows={supportedFlows}
+ defaultUsername={registerSearchParams.username}
+ defaultEmail={registerSearchParams.email}
+ defaultRegisterToken={registerSearchParams.token}
+ />
+ )
+ }
+ </SupportedUIAFlowsLoader>
+ <span data-spacing-node />
+ {sso && <OrDivider />}
+ </>
+ )}
+ {sso && (
+ <>
+ <SSOLogin
+ providers={sso.identity_providers}
+ redirectUrl={ssoRedirectUrl}
+ asIcons={
+ registerFlows.status === RegisterFlowStatus.FlowRequired &&
+ sso.identity_providers.length > 2
+ }
+ />
+ <span data-spacing-node />
+ </>
+ )}
+ <Text align="Center">
+ Already have an account? <Link to={getLoginPath(server)}>Login</Link>
+ </Text>
+ </Box>
+ );
+}
--- /dev/null
+export * from './Register';
--- /dev/null
+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<RegisterResult> => {
+ const [err, res] = await to<RegisterResponse, MatrixError>(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]);
+};
--- /dev/null
+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<LoginPathSearchParams>(path, { email }));
+ return;
+ }
+ navigate(path);
+ };
+
+ return (
+ <Overlay open backdrop={<OverlayBackdrop />}>
+ <OverlayCenter>
+ <FocusTrap>
+ <Dialog>
+ <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+ <Text>
+ Password has been reset successfully. Please login with your new password.
+ </Text>
+ <Button variant="Primary" onClick={handleClick}>
+ <Text size="B400" as="span">
+ Login
+ </Text>
+ </Button>
+ </Box>
+ </Dialog>
+ </FocusTrap>
+ </OverlayCenter>
+ </Overlay>
+ );
+}
+
+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<FormData>();
+
+ 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<HTMLFormElement> = (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 (
+ <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
+ <Text size="T300" priority="400">
+ Homeserver <strong>{server}</strong> will send you an email to let you reset your password.
+ </Text>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Email
+ </Text>
+ <Input
+ defaultValue={defaultEmail}
+ type="email"
+ name="emailInput"
+ variant="Background"
+ size="500"
+ required
+ outlined
+ />
+ {passwordEmailState.status === AsyncStatus.Error && (
+ <FieldError
+ message={`${passwordEmailState.error.errcode}: ${passwordEmailState.error.data?.error}`}
+ />
+ )}
+ </Box>
+ <ConfirmPasswordMatch initialValue>
+ {(match, doMatch, passRef, confPassRef) => (
+ <>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ New Password
+ </Text>
+ <PasswordInput
+ ref={passRef}
+ onChange={doMatch}
+ name="passwordInput"
+ variant="Background"
+ size="500"
+ outlined
+ required
+ />
+ </Box>
+ <Box direction="Column" gap="100">
+ <Text as="label" size="L400" priority="300">
+ Confirm Password
+ </Text>
+ <PasswordInput
+ ref={confPassRef}
+ onChange={doMatch}
+ name="confirmPasswordInput"
+ variant="Background"
+ size="500"
+ style={{ color: match ? undefined : color.Critical.Main }}
+ outlined
+ required
+ />
+ </Box>
+ </>
+ )}
+ </ConfirmPasswordMatch>
+ {resetPasswordError && (
+ <FieldError
+ message={`${resetPasswordError.errcode}: ${
+ resetPasswordError.data?.error ?? 'Failed to reset password.'
+ }`}
+ />
+ )}
+ <span data-spacing-node />
+ <Button type="submit" variant="Primary" size="500">
+ <Text as="span" size="B500">
+ Reset Password
+ </Text>
+ </Button>
+
+ {resetPasswordResult && <ResetPasswordComplete email={formData?.email} />}
+
+ {passwordEmailState.status === AsyncStatus.Success && formData && waitingToVerifyEmail && (
+ <UIAFlowOverlay currentStep={1} stepCount={1} onCancel={handleCancel}>
+ <EmailStageDialog
+ stageData={{
+ type: AuthType.Email,
+ errorCode: flowErrorCode,
+ error: flowError,
+ session: ongoingAuthData?.session,
+ }}
+ submitAuthDict={handleSubmitRequest}
+ email={formData.email}
+ clientSecret={formData.clientSecret}
+ requestEmailToken={passwordEmail}
+ emailTokenState={passwordEmailState}
+ onCancel={handleCancel}
+ />
+ </UIAFlowOverlay>
+ )}
+
+ <Overlay
+ open={
+ passwordEmailState.status === AsyncStatus.Loading ||
+ resetPasswordState.status === AsyncStatus.Loading
+ }
+ backdrop={<OverlayBackdrop />}
+ >
+ <OverlayCenter>
+ <Spinner variant="Secondary" size="600" />
+ </OverlayCenter>
+ </Overlay>
+ </Box>
+ );
+}
--- /dev/null
+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 (
+ <Box direction="Column" gap="500">
+ <Text size="H2" priority="400">
+ Reset Password
+ </Text>
+ <PasswordResetForm defaultEmail={resetPasswordSearchParams.email} />
+ <span data-spacing-node />
+
+ <Text align="Center">
+ Remember your password? <Link to={getLoginPath(server)}>Login</Link>
+ </Text>
+ </Box>
+ );
+}
--- /dev/null
+export * from './ResetPassword';
--- /dev/null
+import to from 'await-to-js';
+import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk';
+
+export type ResetPasswordResponse = Record<string, never>;
+export type ResetPasswordResult = [IAuthData, undefined] | [undefined, ResetPasswordResponse];
+export const resetPassword = async (
+ mx: MatrixClient,
+ authDict: AuthDict,
+ newPassword: string
+): Promise<ResetPasswordResult> => {
+ const [err, res] = await to<ResetPasswordResponse, MatrixError>(
+ 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];
+};
--- /dev/null
+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,
+});
--- /dev/null
+import { generatePath } from 'react-router-dom';
+import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths';
+
+export const withSearchParam = <T extends Record<string, string>>(
+ 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);
+};
--- /dev/null
+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?/';
-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<string[], RoomsAction>
-) => {
+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<string[], RoomsAction>,
- mDirectAtom: WritableAtom<Set<string>, MDirectAction>
+ invitesAtom: typeof allInvitesAtom,
+ directAtom: typeof mDirectAtom
) => {
- const mDirects = useAtomValue(mDirectAtom);
+ const mDirects = useAtomValue(directAtom);
const selector = useCallback(
(rooms: string[]) =>
rooms.filter(
),
[mx, mDirects]
);
- return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
+ return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
};
export const useDirectInvites = (
mx: MatrixClient,
- allInvitesAtom: WritableAtom<string[], RoomsAction>,
- mDirectAtom: WritableAtom<Set<string>, MDirectAction>
+ invitesAtom: typeof allInvitesAtom,
+ directAtom: typeof mDirectAtom
) => {
- const mDirects = useAtomValue(mDirectAtom);
+ const mDirects = useAtomValue(directAtom);
const selector = useCallback(
(rooms: string[]) =>
rooms.filter(
),
[mx, mDirects]
);
- return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
+ return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
};
-export const useUnsupportedInvites = (
- mx: MatrixClient,
- allInvitesAtom: WritableAtom<string[], RoomsAction>
-) => {
+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));
};
-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<string[], RoomsAction>) => {
+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<string[], RoomsAction>,
- mDirectAtom: WritableAtom<Set<string>, 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<string[], RoomsAction>,
- mDirectAtom: WritableAtom<Set<string>, 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<string[], RoomsAction>
-) => {
+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));
};
-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 = <K extends keyof Settings>(
- settingsAtom: WritableAtom<Settings, Settings>,
- key: K
-) => {
+export type SettingSetter<K extends keyof Settings> =
+ | Settings[K]
+ | ((s: Settings[K]) => Settings[K]);
+
+export const useSetSetting = <K extends keyof Settings>(settingsAtom: typeof sAtom, key: K) => {
const setterAtom = useMemo(
() =>
- atom<null, Settings[K] | ((s: Settings[K]) => Settings[K])>(null, (get, set, value) => {
+ atom<null, [SettingSetter<K>], undefined>(null, (get, set, value) => {
const s = { ...get(settingsAtom) };
s[key] = typeof value === 'function' ? value(s[key]) : value;
set(settingsAtom, s);
};
export const useSetting = <K extends keyof Settings>(
- settingsAtom: WritableAtom<Settings, Settings>,
+ settingsAtom: typeof sAtom,
key: K
-): [Settings[K], SetAtom<Settings[K] | ((s: Settings[K]) => Settings[K]), void>] => {
+): [Settings[K], ReturnType<typeof useSetSetting<K>>] => {
const selector = useMemo(() => (s: Settings) => s[key], [key]);
const setting = useAtomValue(selectAtom(settingsAtom, selector));
import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
const baseRoomsAtom = atom<string[]>([]);
-export const allInvitesAtom = atom<string[], RoomsAction>(
+export const allInvitesAtom = atom<string[], [RoomsAction], undefined>(
(get) => get(baseRoomsAtom),
(get, set, action) => {
if (action.type === 'INITIALIZE') {
export const useBindAllInvitesAtom = (
mx: MatrixClient,
- allRooms: WritableAtom<string[], RoomsAction>
+ allRooms: WritableAtom<string[], [RoomsAction], undefined>
) => {
useBindRoomsWithMembershipsAtom(
mx,
export const createListAtom = <T>() => {
const baseListAtom = atom<T[]>([]);
- return atom<T[], ListAction<T>>(
+ return atom<T[], [ListAction<T>], undefined>(
(get) => get(baseListAtom),
(get, set, action) => {
const items = get(baseListAtom);
-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';
};
const baseMDirectAtom = atom(new Set<string>());
-export const mDirectAtom = atom<Set<string>, MDirectAction>(
+export const mDirectAtom = atom<Set<string>, [MDirectAction], undefined>(
(get) => get(baseMDirectAtom),
(get, set, action) => {
set(baseMDirectAtom, action.rooms);
}
);
-export const useBindMDirectAtom = (
- mx: MatrixClient,
- mDirect: WritableAtom<Set<string>, MDirectAction>
-) => {
+export const useBindMDirectAtom = (mx: MatrixClient, mDirect: typeof mDirectAtom) => {
const setMDirect = useSetAtom(mDirect);
useEffect(() => {
-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';
});
const baseMutedRoomsAtom = atom(new Set<string>());
-export const mutedRoomsAtom = atom<Set<string>, MutedRoomsUpdate>(
+export const mutedRoomsAtom = atom<Set<string>, [MutedRoomsUpdate], undefined>(
(get) => get(baseMutedRoomsAtom),
(get, set, action) => {
const mutedRooms = new Set([...get(mutedRoomsAtom)]);
}
);
-export const useBindMutedRoomsAtom = (
- mx: MatrixClient,
- mutedAtom: WritableAtom<Set<string>, MutedRoomsUpdate>
-) => {
+export const useBindMutedRoomsAtom = (mx: MatrixClient, mutedAtom: typeof mutedRoomsAtom) => {
const setMuted = useSetAtom(mutedAtom);
useEffect(() => {
-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<string[]>([]);
-export const allRoomsAtom = atom<string[], RoomsAction>(
+export const allRoomsAtom = atom<string[], [RoomsAction], undefined>(
(get) => get(baseRoomsAtom),
(get, set, action) => {
if (action.type === 'INITIALIZE') {
});
}
);
-export const useBindAllRoomsAtom = (
- mx: MatrixClient,
- allRooms: WritableAtom<string[], RoomsAction>
-) => {
+export const useBindAllRoomsAtom = (mx: MatrixClient, allRooms: typeof allRoomsAtom) => {
useBindRoomsWithMembershipsAtom(
mx,
allRooms,
import produce from 'immer';
-import { atom, useSetAtom, WritableAtom } from 'jotai';
+import { atom, useSetAtom } from 'jotai';
import {
ClientEvent,
MatrixClient,
};
const baseRoomToParents = atom<RoomToParents>(new Map());
-export const roomToParentsAtom = atom<RoomToParents, RoomToParentsAction>(
+export const roomToParentsAtom = atom<RoomToParents, [RoomToParentsAction], undefined>(
(get) => get(baseRoomToParents),
(get, set, action) => {
if (action.type === 'INITIALIZE') {
export const useBindRoomToParentsAtom = (
mx: MatrixClient,
- roomToParents: WritableAtom<RoomToParents, RoomToParentsAction>
+ roomToParents: typeof roomToParentsAtom
) => {
const setRoomToParents = useSetAtom(roomToParents);
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';
};
const baseRoomToUnread = atom<RoomToUnread>(new Map());
-export const roomToUnreadAtom = atom<RoomToUnread, RoomToUnreadAction>(
+export const roomToUnreadAtom = atom<RoomToUnread, [RoomToUnreadAction], undefined>(
(get) => get(baseRoomToUnread),
(get, set, action) => {
if (action.type === 'RESET') {
export const useBindRoomToUnreadAtom = (
mx: MatrixClient,
- unreadAtom: WritableAtom<RoomToUnread, RoomToUnreadAction>,
+ unreadAtom: typeof roomToUnreadAtom,
muteChangesAtom: PrimitiveAtom<MuteChanges>
) => {
const setUnreadAtom = useSetAtom(unreadAtom);
--- /dev/null
+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<Sessions>(
+ 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<Sessions, [SessionsAction], undefined>(
+ (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);
+ }
+ }
+);
};
const baseSettings = atom<Settings>(getSettings());
-export const settingsAtom = atom<Settings, Settings>(
+export const settingsAtom = atom<Settings, [Settings], undefined>(
(get) => get(baseSettings),
(get, set, update) => {
set(baseSettings, update);
};
const baseTabToRoom = atom<TabToRoom>(new Map());
-export const tabToRoomAtom = atom<TabToRoom, TabToRoomAction>(
+export const tabToRoomAtom = atom<TabToRoom, [TabToRoomAction], undefined>(
(get) => get(baseTabToRoom),
(get, set, action) => {
if (action.type === 'PUT') {
};
const baseRoomIdToTypingMembersAtom = atom<IRoomIdToTypingMembers>(new Map());
-export const roomIdToTypingMembersAtom = atom<IRoomIdToTypingMembers, IRoomIdToTypingMembersAction>(
+export const roomIdToTypingMembersAtom = atom<
+ IRoomIdToTypingMembers,
+ [IRoomIdToTypingMembersAction],
+ undefined
+>(
(get) => get(baseRoomIdToTypingMembersAtom),
(get, set, action) => {
const roomIdToTypingMembers = get(baseRoomIdToTypingMembersAtom);
file,
status: UploadStatus.Idle,
});
- return atom<Upload, UploadAtomAction>(
+ return atom<Upload, [UploadAtomAction], undefined>(
(get) => get(baseUploadAtom),
(get, set, update) => {
const uploadState = get(baseUploadAtom);
export const useBindRoomsWithMembershipsAtom = (
mx: MatrixClient,
- roomsAtom: WritableAtom<string[], RoomsAction>,
+ roomsAtom: WritableAtom<string[], [RoomsAction], undefined>,
memberships: Membership[]
) => {
const setRoomsAtom = useSetAtom(roomsAtom);
--- /dev/null
+import { atom } from 'jotai';
+
+export const getLocalStorageItem = <T>(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 = <T>(key: string, value: T) => {
+ localStorage.setItem(key, JSON.stringify(value));
+};
+
+export type GetLocalStorageItem<T> = (key: string) => T;
+export type SetLocalStorageItem<T> = (key: string, value: T) => void;
+
+export const atomWithLocalStorage = <T>(
+ key: string,
+ getItem: GetLocalStorageItem<T>,
+ setItem: SetLocalStorageItem<T>
+) => {
+ const value = getItem(key);
+
+ const baseAtom = atom<T>(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<T, [T], undefined>(
+ (get) => get(baseAtom),
+ (get, set, newValue) => {
+ set(baseAtom, newValue);
+ setItem(key, newValue);
+ }
+ );
+
+ return localStorageAtom;
+};
--- /dev/null
+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)}`,
+});
return values;
}, []);
+export const promiseFulfilledResult = <T>(
+ settledResult: PromiseSettledResult<T>
+): T | undefined => {
+ if (settledResult.status === 'fulfilled') return settledResult.value;
+ return undefined;
+};
+export const promiseRejectedResult = <T>(settledResult: PromiseSettledResult<T>): any => {
+ if (settledResult.status === 'rejected') return settledResult.reason;
+ return undefined;
+};
+
export const binarySearch = <T>(items: T[], match: (item: T) => -1 | 0 | 1): T | undefined => {
const search = (start: number, end: number): T | undefined => {
if (start > end) return undefined;
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));
--- /dev/null
+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<string, Record<string, unknown>>;
+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<string, any>;
+ const url = langToPolicy.en?.url;
+ if (typeof url === 'string') return url;
+
+ const firstKey = Object.keys(langToPolicy)[0];
+ return langToPolicy[firstKey]?.url;
+ }
+ }
+ return undefined;
+};
export const HTTP_URL_PATTERN = `https?:\\/\\/(?:www\\.)?(?:[^\\s)]*)(?<![.,:;!/?()[\\]\\s]+)`;
+export const EMAIL_REGEX =
+ /^(([^<>()[\]\\.,;:\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 = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
// https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
}
export {
- createTemporaryClient, login, verifyEmail,
+ updateLocalStore, createTemporaryClient, login, verifyEmail,
loginWithToken, startSsoLogin,
completeRegisterStage,
};
import Olm from '@matrix-org/olm';
// import { logger } from 'matrix-js-sdk/lib/logger';
-import { secret } from './state/auth';
+import { getSecret } from './state/auth';
import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
dbName: 'web-sync-store',
});
await indexedDBStore.startup();
+ const secret = getSecret();
this.matrixClient = sdk.createClient({
baseUrl: secret.baseUrl,
+++ /dev/null
-import cons from './cons';
-
-function getSecret(key) {
- return localStorage.getItem(key);
-}
-
-const isAuthenticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null;
-
-const secret = {
- accessToken: getSecret(cons.secretKey.ACCESS_TOKEN),
- deviceId: getSecret(cons.secretKey.DEVICE_ID),
- userId: getSecret(cons.secretKey.USER_ID),
- baseUrl: getSecret(cons.secretKey.BASE_URL),
-};
-
-export {
- isAuthenticated,
- secret,
-};
--- /dev/null
+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 };
+/// <reference types="vite/client" />
+
declare module 'browser-encrypt-attachment' {
export interface EncryptedAttachmentInfo {
v: string;
info: EncryptedAttachmentInfo
): Promise<ArrayBuffer>;
}
+
+declare module '*.svg' {
+ const content: string;
+ export default content;
+}
+++ /dev/null
-/* eslint-disable import/first */
-import React from 'react';
-import ReactDom from 'react-dom';
-import { enableMapSet } from 'immer';
-import '@fontsource/inter/variable.css';
-import 'folds/dist/style.css';
-import { configClass, varsClass } from 'folds';
-
-enableMapSet();
-
-import './index.scss';
-
-import settings from './client/state/settings';
-
-import App from './app/pages/App';
-
-document.body.classList.add(configClass, varsClass);
-
-settings.applyTheme();
-
-ReactDom.render(<App />, document.getElementById('root'));
--- /dev/null
+/* eslint-disable import/first */
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { enableMapSet } from 'immer';
+import '@fontsource/inter/variable.css';
+import 'folds/dist/style.css';
+import { configClass, varsClass } from 'folds';
+
+enableMapSet();
+
+import './index.scss';
+
+import settings from './client/state/settings';
+
+import App from './app/pages/App';
+
+document.body.classList.add(configClass, varsClass);
+settings.applyTheme();
+
+const mountApp = () => {
+ const rootContainer = document.getElementById('root');
+
+ if (rootContainer === null) {
+ console.error('Root container element not found!');
+ return;
+ }
+
+ const root = createRoot(rootContainer);
+ root.render(<App />);
+};
+
+mountApp();
--- /dev/null
+export type WithRequiredProp<Type extends object, Key extends keyof Type> = Type & {
+ [Property in Key]-?: Type[Property];
+};
"strict": true,
"esModuleInterop": true,
"moduleResolution": "Node",
+ "resolveJsonModule": true,
"outDir": "dist",
"skipLibCheck": true
},
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: [
dest: '',
},
{
- src: '_redirects',
+ src: 'netlify.toml',
dest: '',
},
{
export default defineConfig({
appType: 'spa',
publicDir: false,
- base: "",
+ base: buildConfig.base,
server: {
port: 8080,
host: true,