cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit 3f144dbf7e4a4b2e0cc7c27e26d7545f1a9d43d2
parent 0243be4babbc31b7e4d3f3f50edf339bce6e085b
Author: MTRNord <mtrnord1@gmail.com>
Date:   Fri, 28 Apr 2023 18:26:59 +0200

feat: Ditch js-sdk and hookup login and mainpage to redux using rtk-query

Diffstat:
Mindex.html | 5++---
Mpackage-lock.json | 205++-----------------------------------------------------------------------------
Mpackage.json | 2--
Asrc/@types/global.d.ts | 8++++++++
Msrc/App.tsx | 34++++++++++++++++++++++++++++++++++
Msrc/app/api/api.ts | 189+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Asrc/app/api/apiTypes.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/app/api/reducers.ts | 42------------------------------------------
Msrc/app/protectedRoute.tsx | 2+-
Dsrc/app/sagas/rootSaga.ts | 6------
Msrc/app/store.ts | 24+++++++++++-------------
Msrc/components/login/login.tsx | 43++++++++++++++++++++++++++-----------------
Msrc/index.scss | 1+
Msrc/main.tsx | 4+---
Msrc/pages/MainPage.tsx | 4++--
15 files changed, 250 insertions(+), 369 deletions(-)

diff --git a/index.html b/index.html @@ -9,9 +9,8 @@ /> <title>Matrix</title> <script> - if (global === undefined) { - var global = window; - } + global = globalThis // fix for "global not defined" error + globalThis.OLM_OPTIONS = undefined; // Fix missing var for olm </script> </head> <body> diff --git a/package-lock.json b/package-lock.json @@ -20,14 +20,12 @@ "dompurify": "^3.0.2", "lexical": "^0.10.0", "lucide-react": "^0.171.0", - "matrix-js-sdk": "^24.1.0", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.4.3", "react-redux": "^8.0.5", "react-router-dom": "^6.10.0", - "redux-saga": "^1.2.3", "typescript": "^5.0.2", "web-vitals": "^2.1.4" }, @@ -37,7 +35,7 @@ "@storybook/addon-essentials": "^7.0.7", "@storybook/addon-interactions": "^7.0.7", "@storybook/addon-links": "^7.0.7", - "@storybook/addon-styling": "^1.0.2", + "@storybook/addon-styling": "^1.0.3", "@storybook/blocks": "^7.0.7", "@storybook/react": "^7.0.7", "@storybook/react-vite": "^7.0.7", @@ -3021,14 +3019,6 @@ "yjs": ">=13.5.22" } }, - "node_modules/@matrix-org/matrix-sdk-crypto-js": { - "version": "0.1.0-alpha.7", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz", - "integrity": "sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg==", - "engines": { - "node": ">= 10" - } - }, "node_modules/@mdx-js/react": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", @@ -3092,57 +3082,6 @@ "node": ">= 8" } }, - "node_modules/@redux-saga/core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz", - "integrity": "sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.2.1", - "@redux-saga/delay-p": "^1.2.1", - "@redux-saga/is": "^1.1.3", - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/redux-saga" - } - }, - "node_modules/@redux-saga/deferred": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", - "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" - }, - "node_modules/@redux-saga/delay-p": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", - "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", - "dependencies": { - "@redux-saga/symbols": "^1.1.3" - } - }, - "node_modules/@redux-saga/is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", - "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", - "dependencies": { - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1" - } - }, - "node_modules/@redux-saga/symbols": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", - "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" - }, - "node_modules/@redux-saga/types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", - "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" - }, "node_modules/@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -5515,11 +5454,6 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, - "node_modules/@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, "node_modules/@types/express": { "version": "4.17.17", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", @@ -5715,11 +5649,6 @@ "@types/react": "*" } }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -6113,11 +6042,6 @@ "ajv": "^6.9.1" } }, - "node_modules/another-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==" - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -6406,11 +6330,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6725,14 +6644,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "dependencies": { - "base-x": "^4.0.0" - } - }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -7216,6 +7127,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -8103,6 +8015,8 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, "engines": { "node": ">=0.8.x" } @@ -10599,18 +10513,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, - "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10715,42 +10617,6 @@ "react": ">= 0.14.0" } }, - "node_modules/matrix-events-sdk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", - "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" - }, - "node_modules/matrix-js-sdk": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-24.1.0.tgz", - "integrity": "sha512-xEx2ZoNsS56dwgqLJ3rIv2SUpFxdQLrLKmJCpMatMUKCAg+NGuZfpQ3QXblIbGaqFNQZCH7fC7S48AeTMZp1Jw==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.5", - "another-json": "^0.2.0", - "bs58": "^5.0.0", - "content-type": "^1.0.4", - "loglevel": "^1.7.1", - "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.3.1", - "p-retry": "4", - "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6", - "uuid": "9" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/matrix-widget-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz", - "integrity": "sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q==", - "dependencies": { - "@types/events": "^3.0.0", - "events": "^3.2.0" - } - }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -11407,18 +11273,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -12691,14 +12545,6 @@ "@babel/runtime": "^7.9.2" } }, - "node_modules/redux-saga": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.3.tgz", - "integrity": "sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ==", - "dependencies": { - "@redux-saga/core": "^1.2.3" - } - }, "node_modules/redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", @@ -12887,14 +12733,6 @@ "node": ">=12" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -13117,14 +12955,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/sdp-transform": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", - "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", - "bin": { - "sdp-verify": "checker.js" - } - }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -14395,27 +14225,6 @@ "node": ">=12.20" } }, - "node_modules/typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "dependencies": { - "typescript-logic": "^0.0.0" - } - }, - "node_modules/typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "node_modules/typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "dependencies": { - "typescript-compare": "^0.0.2" - } - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -14435,11 +14244,6 @@ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", "dev": true }, - "node_modules/unhomoglyph": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", - "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -14675,6 +14479,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json @@ -16,14 +16,12 @@ "dompurify": "^3.0.2", "lexical": "^0.10.0", "lucide-react": "^0.171.0", - "matrix-js-sdk": "^24.1.0", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.4.3", "react-redux": "^8.0.5", "react-router-dom": "^6.10.0", - "redux-saga": "^1.2.3", "typescript": "^5.0.2", "web-vitals": "^2.1.4" }, diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts @@ -0,0 +1,7 @@ + +import "matrix-react-sdk/src/@types/global"; // load matrix-react-sdk's type extensions first +declare global { + interface Window { + OLM_OPTIONS: any; + } +} +\ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx @@ -3,10 +3,44 @@ import { Route, Routes, } from "react-router-dom"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import olmWasmPath from "olm/olm.wasm?url"; +import OlmLegacy from 'olm/olm_legacy.js?url'; +import Olm from "olm"; import LoginPage from './pages/LoginPage'; import MainPage from './pages/MainPage'; import { ProtectedRoute } from './app/protectedRoute'; +Olm + .init({ + locateFile: () => olmWasmPath, + }) + .then(() => { + console.log("Using WebAssembly Olm"); + }) + .catch((wasmLoadError: any) => { + console.log("Failed to load Olm: trying legacy version", wasmLoadError); + return new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = OlmLegacy; + s.onload = resolve; + s.onerror = reject; + document.body.appendChild(s); + }) + .then(() => { + // Init window.Olm, ie. the one just loaded by the script tag, + // not 'Olm' which is still the failed wasm version. + return window.Olm.init(); + }) + .then(() => { + console.log("Using legacy Olm"); + }) + .catch((legacyLoadError) => { + console.log("Both WebAssembly and asm.js Olm failed!", legacyLoadError); + }); + }); + function App() { return <BrowserRouter> <Routes> diff --git a/src/app/api/api.ts b/src/app/api/api.ts @@ -1,91 +1,121 @@ -import { all, call, fork, put, takeEvery } from "redux-saga/effects"; -import { LOGIN_REQUEST_ACTION, LOGIN_SUCCESS_ACTION, LOGIN, LOGIN_ACTION, LOGIN_FAILURE_ACTION } from "./reducers"; -import { IndexedDBCryptoStore, IndexedDBStore, MatrixClient, MemoryStore, createClient, setCryptoStoreFactory } from "matrix-js-sdk"; -import { AutoDiscovery } from 'matrix-js-sdk/lib/autodiscovery'; +import { BaseQueryFn, FetchArgs, FetchBaseQueryError, createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { RootState } from '../store'; +import { ILoginFlows, ILoginParams, ILoginResponse, IWellKnown } from './apiTypes'; +import { createAction, createReducer } from '@reduxjs/toolkit'; +// async function login(baseUrl: string, userId: string, password: string): Promise<MatrixClient> { +// //const client = await initMatrixClient(baseUrl, userId, undefined, undefined, password); -async function login(baseUrl: string, userId: string, password: string): Promise<MatrixClient> { - const client = await initMatrixClient(baseUrl, userId, undefined, password); +// window.localStorage.setItem("accessToken", client.getAccessToken()!); +// window.localStorage.setItem("baseUrl", client.baseUrl); +// window.localStorage.setItem("userId", client.getUserId()!); +// window.localStorage.setItem("deviceId", client.getDeviceId()!); - window.localStorage.setItem("accessToken", client.getAccessToken()!); - window.localStorage.setItem("baseUrl", client.baseUrl); - window.localStorage.setItem("userId", client.getUserId()!); +// return client; +// } - return client; +export const setHost = createAction<string, 'auth/host'>('auth/host'); +export const setAccessToken = createAction<string, 'auth/access_token'>('auth/access_token'); +export const setLoggedIn = createAction<boolean, 'auth/logged_in'>('auth/logged_in'); +interface IAuthState { + host?: string; + accessToken?: string; + logged_in: boolean; } - -function* onLoginSaga(action: LOGIN): any { - yield put(LOGIN_REQUEST_ACTION()); - if (!action.baseUrl.startsWith("https://")) { - yield put(LOGIN_FAILURE_ACTION("Homeserver url must start with https://")); - return; - } - if (!action.username) { - yield put(LOGIN_FAILURE_ACTION("Username must be a non empty string")); - return; - } - if (!action.password) { - yield put(LOGIN_FAILURE_ACTION("Password must be a non empty string")); - return; - } - try { - const client: MatrixClient = yield call(login, action.baseUrl, action.username, action.password); - yield put(LOGIN_SUCCESS_ACTION(client)); - } catch (e) { - yield put(LOGIN_FAILURE_ACTION((e as any).toString())); - return; - } -} - -function* watchLoginSaga() { - yield takeEvery<LOGIN>(LOGIN_ACTION, onLoginSaga); -} - -export function* apiSagas() { - yield all([fork(watchLoginSaga)]); +const initialAuthState: IAuthState = { + host: undefined, + accessToken: undefined, + logged_in: false, } +export const auth = createReducer(initialAuthState, (builder) => builder + .addCase(setHost, (state, action) => { state.host = action.payload }) + .addCase(setAccessToken, (state, action) => { state.accessToken = action.payload }) + .addCase(setLoggedIn, (state, action) => { state.logged_in = action.payload }) +); -export async function initMatrixClient(baseUrl: string, userId: string, accessToken?: string, password?: string): Promise<MatrixClient> { - // just *accessing* indexedDB throws an exception in firefox with indexeddb disabled. - let indexedDB: IDBFactory | undefined; - try { - indexedDB = global.indexedDB; - } catch (e) { } - - // if our browser (appears to) support indexeddb, use an indexeddb crypto store. - let store = new MemoryStore({ localStorage: window.localStorage }); - if (indexedDB) { - setCryptoStoreFactory(() => new IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto")); - store = new IndexedDBStore({ indexedDB: indexedDB, localStorage: window.localStorage }); - await store.startup(); - } +const baseQuery: BaseQueryFn<string | FetchArgs, + unknown, + FetchBaseQueryError> = async (args, api, extraOptions) => { + const baseUrl = (api.getState() as RootState).auth.host; + // gracefully handle scenarios where data to generate the URL is missing + if (!baseUrl) { + return { + error: { + status: 400, + statusText: 'Bad Request', + data: 'Invalid Matrix Host', + }, + }; + } - const clientConfig = await AutoDiscovery.findClientConfig(baseUrl.replace("https://", '')); + return rawBaseQuery(baseUrl)(args, api, extraOptions); + }; - if (clientConfig["m.homeserver"].state === AutoDiscovery.FAIL_PROMPT) { - throw Error(clientConfig["m.homeserver"].error?.toString()) - } - if (clientConfig["m.homeserver"].state !== AutoDiscovery.FAIL_ERROR) { - if (clientConfig["m.homeserver"].base_url) { - baseUrl = clientConfig["m.homeserver"].base_url; - } - } +export const matrixApi = createApi({ + reducerPath: 'matrixApi', + baseQuery: baseQuery, + tagTypes: ['Login', 'Flows', "WellKnown"], + endpoints: (builder) => ({ + getWellKnown: builder.query<IWellKnown, undefined>({ + query: () => `/.well-known/matrix/client`, + providesTags: ['WellKnown'], + async onQueryStarted(_arg, { dispatch, queryFulfilled }) { + try { + const { data, meta } = await queryFulfilled; + const response = (meta as { request: Request, response: Response }).response; + if (response.status === 200) { + if (data?.["m.homeserver"]?.base_url) { + dispatch(setHost(data["m.homeserver"].base_url)) + } + } + } catch (err) { + // `onError` side-effect + } + }, + }), + getLoginFlows: builder.query<ILoginFlows, undefined>({ + query: () => `/_matrix/client/v3/login`, + providesTags: ['Flows'], + }), + doLogin: builder.mutation<ILoginResponse, ILoginParams>({ + query: (data) => ({ + url: `/_matrix/client/r0/login`, + method: 'POST', + body: data, + }), + async onQueryStarted(_arg, { dispatch, queryFulfilled }) { + try { + const { data, meta } = await queryFulfilled; + const response = (meta as { request: Request, response: Response }).response; + if (response.status === 200) { + dispatch(setLoggedIn(true)) + dispatch(setAccessToken(data.access_token)) + if (data.well_known?.["m.homeserver"]?.base_url) { + dispatch(setHost(data.well_known["m.homeserver"].base_url)) + } + } else { + dispatch(setLoggedIn(false)) + } + } catch (err) { + // `onError` side-effect + dispatch(setLoggedIn(false)) + } + }, + invalidatesTags: ['Login'], + }), + }), +}); - const matrixClient = createClient({ - baseUrl: baseUrl, - accessToken: accessToken, - userId: accessToken ? userId : undefined, - useAuthorizationHeader: true, - store - }); +const rawBaseQuery = (baseUrl: string) => fetchBaseQuery({ + baseUrl, + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).auth.accessToken; + !!token && headers.set('Authorization', `Bearer ${token}`); - if (!accessToken && !password) { - throw Error("Missing password and accessToken. Unable to proceed") - } + return headers; + }, +}); - if (!accessToken) { - await matrixClient.loginWithPassword(userId, password!!); - } - await matrixClient.initCrypto(); - return matrixClient -} -\ No newline at end of file +// Export hooks for usage in functional components, which are +// auto-generated based on the defined endpoints +export const { useGetLoginFlowsQuery, useDoLoginMutation, useLazyGetWellKnownQuery } = matrixApi diff --git a/src/app/api/apiTypes.ts b/src/app/api/apiTypes.ts @@ -0,0 +1,49 @@ +export interface ILoginFlow { + type: string; +} +export interface ILoginFlows { + flows: ILoginFlow[]; +} +export interface IRateLimitError { + errcode: string; + error?: string; + retry_after_ms: number; +} + +export interface ILoginResponse { + access_token: string; + device_id: string; + home_server?: string; + user_id: string; + expires_in_ms?: number; + refresh_token?: string; + well_known?: IWellKnown +} + +export interface ILoginParams { + address?: string; + device_id?: string; + identifier?: { + type: "m.id.user" | "m.id.thirdparty" | "m.id.phone"; + user?: string; + medium?: string; + address?: string; + country?: string; + phone?: string; + }; + initial_device_display_name?: string; + password?: string; + user?: string; + refresh_token?: string; + token?: string; + type: "m.login.password" | "m.login.token"; +} + +export interface IWellKnown { + "m.homeserver": { + base_url: string; + } + "m.identity_server"?: { + base_url: string; + } +} +\ No newline at end of file diff --git a/src/app/api/reducers.ts b/src/app/api/reducers.ts @@ -1,41 +0,0 @@ -import { Action, createAction, createReducer } from "@reduxjs/toolkit"; -import { RootState } from "../store"; -import { MatrixClient } from "matrix-js-sdk"; - -interface ApiLoginStatus { - loginPending: boolean - error?: string - client?: MatrixClient -} - -export const LOGIN_ACTION = "api/LOGIN"; -export const LOGIN_REQUEST_ACTION = createAction('api/LOGIN_REQUEST'); -export const LOGIN_SUCCESS_ACTION = createAction<MatrixClient>('api/LOGIN_SUCCESS'); -export const LOGIN_FAILURE_ACTION = createAction<string>('api/LOGIN_FAILURE'); - -export interface LOGIN extends Action { type: typeof LOGIN_ACTION, baseUrl: string, username: string, password: string }; - - -const initialLoginState: ApiLoginStatus = { loginPending: false }; - - -export const apiLoginReducer = createReducer(initialLoginState, (builder) => { - builder - .addCase(LOGIN_REQUEST_ACTION, (state, _) => { - state.loginPending = true; - state.error = undefined; - }) - // @ts-ignore: Somehow this state is causing issues here since its written but not read. TS6133 - .addCase(LOGIN_SUCCESS_ACTION, (state, action) => { - state = initialLoginState; - state.client = action.payload - }) - .addCase(LOGIN_FAILURE_ACTION, (state, action) => { - state.loginPending = false; - state.error = action.payload - }) -}); - -export function getLoginError(state: RootState): string | undefined { - return state.login.error -} -\ No newline at end of file diff --git a/src/app/protectedRoute.tsx b/src/app/protectedRoute.tsx @@ -3,7 +3,7 @@ import { Navigate } from "react-router-dom"; import { useAppSelector } from "./hooks"; export const ProtectedRoute = ({ children }: { children: ReactElement<any, any> }) => { - const client = useAppSelector(state => state.login.client); + const client = useAppSelector(state => state.auth.host); // TODO: relaunch a client if we can and skip login if (!client) { diff --git a/src/app/sagas/rootSaga.ts b/src/app/sagas/rootSaga.ts @@ -1,6 +0,0 @@ -import { all } from "redux-saga/effects"; -import { apiSagas } from "../api/api"; - -export default function* rootSaga() { - yield all([apiSagas()]); -} diff --git a/src/app/store.ts b/src/app/store.ts @@ -1,23 +1,20 @@ import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; -import createSagaMiddleware from "redux-saga"; -import rootSaga from "./sagas/rootSaga"; -import { apiLoginReducer } from "./api/reducers"; -import { initMatrixClient } from "./api/api"; - -const sagaMiddleware = createSagaMiddleware(); -const middlewares = [sagaMiddleware]; +import { matrixApi, auth } from "./api/api"; +import { setupListeners } from "@reduxjs/toolkit/dist/query/react"; export const store = configureStore({ reducer: { - login: apiLoginReducer, + auth: auth, + [matrixApi.reducerPath]: matrixApi.reducer, }, middleware: (getDefaultMiddleware) => [ ...getDefaultMiddleware(), - ...middlewares, + matrixApi.middleware, ], - preloadedState: await getInitialState() + //preloadedState: await getInitialState() }); -sagaMiddleware.run(rootSaga); + +setupListeners(store.dispatch) export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>; @@ -33,11 +30,12 @@ async function getInitialState() { const accessToken = localStorage.getItem("accessToken"); const baseUrl = localStorage.getItem("baseUrl"); const userId = localStorage.getItem("userId"); + const deviceId = localStorage.getItem("deviceId"); if (accessToken && baseUrl && userId) { - const client = await initMatrixClient(baseUrl, userId, accessToken); + //const client = await initMatrixClient(baseUrl, userId, deviceId!, accessToken, undefined, true); return { login: { - client, + //client, loginPending: false } } diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx @@ -1,35 +1,45 @@ import { useDeferredValue, useState } from 'react'; -import { getLoginError } from '../../app/api/reducers'; -import { useAppDispatch, useAppSelector } from '../../app/hooks'; -import { RootState } from '../../app/store'; +import { useAppDispatch } from '../../app/hooks'; +import { setHost, useDoLoginMutation, useLazyGetWellKnownQuery } from '../../app/api/api'; import Button from '../button/button'; import Header from '../header/header'; import Input from '../input/basic/input'; -import { LOGIN_ACTION } from '../../app/api/reducers'; +import { Navigate } from 'react-router-dom'; export function Login() { - const loginError = useAppSelector((state: RootState) => getLoginError(state)); + const [login, { isLoading: loginPending, error: loginError }] = useDoLoginMutation(); + const [triggerWellKnown, _result, _lastPromiseInfo] = useLazyGetWellKnownQuery(); const deferredLoginError = useDeferredValue(loginError); - const loginPending = useAppSelector((state: RootState) => state.login.loginPending); - const [homeserver, setHomeserver] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [loginPendingLast, setLoginPendingLast] = useState(false); const dispatch = useAppDispatch(); + if (loginPendingLast !== loginPending) { + return <Navigate to="/" />; + } + const startLogin = async () => { + dispatch(setHost(`https://${username.split(':')[1]}`)); + await triggerWellKnown(undefined); + login({ + type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: username, + }, + user: username, + password: password + }) + setLoginPendingLast(true); + } + // TODO: We need to make sure that we do well-known before login return ( <form className="flex flex-col rounded-md shadow p-4 bg-white gap-2 min-w-[30rem]" onSubmit={(e) => { e.preventDefault(); - dispatch({ type: LOGIN_ACTION, baseUrl: homeserver, username: username, password: password }); + startLogin(); }}> <Header>Login</Header> - {deferredLoginError ? <h2 className='text-red-500 font-normal text-sm'>{deferredLoginError}</h2> : <div className='min-h-[1.25rem]'></div>} - <Input - readonly={loginPending} - value={homeserver} - autoFocus={true} - placeholder="Homeserver" - onChange={e => setHomeserver(e.target.value)} - /> + {deferredLoginError ? <h2 className='text-red-500 font-normal text-sm'>{'error' in deferredLoginError ? deferredLoginError.error : ('data' in deferredLoginError ? JSON.stringify(deferredLoginError.data) : deferredLoginError.message)}</h2> : <div className='min-h-[1.25rem]'></div>} <Input readonly={loginPending} value={username} @@ -47,7 +57,6 @@ export function Login() { readonly={loginPending} style="primary" type="submit" - onClick={() => dispatch({ type: LOGIN_ACTION, baseUrl: homeserver, username: username, password: password })} > Login </Button> diff --git a/src/index.scss b/src/index.scss @@ -1,3 +1,4 @@ body, html { margin: 0; + height: 100%; } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx @@ -1,3 +1,4 @@ +/// <reference types="./@types/global.d.ts" /> import React from 'react' import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; @@ -8,9 +9,6 @@ import './index.scss'; const container = document.getElementById('root')!; const root = createRoot(container); -// js-sdk helper -window.Olm = await import("olm"); - root.render( <React.StrictMode> <Provider store={store}> diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx @@ -64,8 +64,8 @@ export default function MainPage() { online: false, }, ]; - return <div className='flex flex-row w-full gap-2 h-full'> - <div className='flex flex-col h-full bg-gradient-to-br from-slate-100 via-gray-200 to-orange-200 border-r-[1px] border-slate-300'> + return <div className='flex flex-row w-full gap-2 min-h-screen'> + <div className='flex flex-col bg-gradient-to-br from-slate-100 via-gray-200 to-orange-200 border-r-[1px] border-slate-300'> <div className='flex flex-row gap-2 m-2 p-1 items-center border-b-2'> <Avatar displayname='Test' avatarUrl='https://randomuser.me/api/portraits/men/62.jpg' dm={false} online={false} /> <div className='flex flex-row justify-between items-center w-full'>