commit 0387a54665645c79c60ca7ea9902ab08c0645a41
parent 073708091d8f38420412a7bfe24b30a0d31032a7
Author: MTRNord <mtrnord1@gmail.com>
Date: Mon, 1 May 2023 21:32:47 +0200
Initial impl of custom sdk without redux and with sliding sync
Diffstat:
16 files changed, 719 insertions(+), 486 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -9,8 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@lexical/react": "^0.10.0",
- "@piotr-cz/redux-persist-idb-storage": "^1.1.3",
- "@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
@@ -19,15 +17,15 @@
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"dompurify": "^3.0.2",
+ "idb": "^7.1.1",
+ "immer": "^10.0.1",
"lexical": "^0.10.0",
"lucide-react": "^0.171.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",
- "reduxjs-toolkit-persist": "^7.2.1",
"typescript": "^5.0.2",
"web-vitals": "^2.1.4"
},
@@ -3084,40 +3082,6 @@
"node": ">= 8"
}
},
- "node_modules/@piotr-cz/redux-persist-idb-storage": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@piotr-cz/redux-persist-idb-storage/-/redux-persist-idb-storage-1.1.3.tgz",
- "integrity": "sha512-aAx40LFsPmg93SHKHUUHtSM50iEFp4uZJrvrkG0oT4yOLyjUAPEqLIcPrsYDuWJpyJrYt8T7SNieinQxp+boEQ==",
- "dependencies": {
- "idb": "^3.0.0"
- },
- "peerDependencies": {
- "redux-persist": ">=3.5.0"
- }
- },
- "node_modules/@reduxjs/toolkit": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz",
- "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==",
- "dependencies": {
- "immer": "^9.0.21",
- "redux": "^4.2.1",
- "redux-thunk": "^2.4.2",
- "reselect": "^4.1.8"
- },
- "peerDependencies": {
- "react": "^16.9.0 || ^17.0.0 || ^18",
- "react-redux": "^7.2.1 || ^8.0.2"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-redux": {
- "optional": true
- }
- }
- },
"node_modules/@remix-run/router": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz",
@@ -5515,15 +5479,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/hoist-non-react-statics": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
- "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
- "dependencies": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -5697,11 +5652,6 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
- "node_modules/@types/use-sync-external-store": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
- "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
- },
"node_modules/@types/yargs": {
"version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
@@ -8893,19 +8843,6 @@
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true
},
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/hoist-non-react-statics/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
- },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -8993,9 +8930,9 @@
}
},
"node_modules/idb": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz",
- "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw=="
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
},
"node_modules/ieee754": {
"version": "1.2.1",
@@ -9041,9 +8978,9 @@
}
},
"node_modules/immer": {
- "version": "9.0.21",
- "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
- "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.1.tgz",
+ "integrity": "sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -12262,49 +12199,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
- "node_modules/react-redux": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
- "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
- "dependencies": {
- "@babel/runtime": "^7.12.1",
- "@types/hoist-non-react-statics": "^3.3.1",
- "@types/use-sync-external-store": "^0.0.3",
- "hoist-non-react-statics": "^3.3.2",
- "react-is": "^18.0.0",
- "use-sync-external-store": "^1.0.0"
- },
- "peerDependencies": {
- "@types/react": "^16.8 || ^17.0 || ^18.0",
- "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
- "react": "^16.8 || ^17.0 || ^18.0",
- "react-dom": "^16.8 || ^17.0 || ^18.0",
- "react-native": ">=0.59",
- "redux": "^4"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- },
- "redux": {
- "optional": true
- }
- }
- },
- "node_modules/react-redux/node_modules/react-is": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
- },
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@@ -12555,39 +12449,6 @@
"node": ">=8"
}
},
- "node_modules/redux": {
- "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/redux-persist": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
- "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
- "peer": true,
- "peerDependencies": {
- "redux": ">4.0.0"
- }
- },
- "node_modules/redux-thunk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
- "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
- "peerDependencies": {
- "redux": "^4"
- }
- },
- "node_modules/reduxjs-toolkit-persist": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/reduxjs-toolkit-persist/-/reduxjs-toolkit-persist-7.2.1.tgz",
- "integrity": "sha512-1gu9Qid0qXlsM4a2D8eHx/YOwa7I0lgAXqM0KYQoX2RA+KMezkq5wkJKEMLoUkpqVuWYHRdU7auS2/jFVsR0gQ==",
- "peerDependencies": {
- "@reduxjs/toolkit": "^1.6.1"
- }
- },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -12721,11 +12582,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/reselect": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
- "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
- },
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -14474,14 +14330,6 @@
"react-dom": "16.8.0 - 18"
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
- "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
diff --git a/package.json b/package.json
@@ -5,8 +5,6 @@
"type": "module",
"dependencies": {
"@lexical/react": "^0.10.0",
- "@piotr-cz/redux-persist-idb-storage": "^1.1.3",
- "@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
@@ -15,15 +13,15 @@
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"dompurify": "^3.0.2",
+ "idb": "^7.1.1",
+ "immer": "^10.0.1",
"lexical": "^0.10.0",
"lucide-react": "^0.171.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",
- "reduxjs-toolkit-persist": "^7.2.1",
"typescript": "^5.0.2",
"web-vitals": "^2.1.4"
},
diff --git a/src/app/api/api.ts b/src/app/api/api.ts
@@ -1,151 +0,0 @@
-import { BaseQueryFn, FetchArgs, FetchBaseQueryError, createApi, fetchBaseQuery, retry, } from '@reduxjs/toolkit/query/react'
-import { RootState } from '../store';
-
-import { ILoginFlows, ILoginParams, ILoginResponse, IRateLimitError, 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);
-
-// window.localStorage.setItem("accessToken", client.getAccessToken()!);
-// window.localStorage.setItem("baseUrl", client.baseUrl);
-// window.localStorage.setItem("userId", client.getUserId()!);
-// window.localStorage.setItem("deviceId", client.getDeviceId()!);
-
-// 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;
-}
-const initialAuthState: IAuthState = {
- 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 })
-);
-
-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',
- },
- };
- }
-
- return rawBaseQuery(baseUrl)(args, api, extraOptions);
- };
-
-const baseQueryWithRetry = retry(
- async (args: string | FetchArgs, api, extraOptions) => {
- const result = await baseQuery(args, api, extraOptions);
- if (result.error?.status === 429) {
- const retryAfterMs = (result.error.data as IRateLimitError).retry_after_ms;
- if (retryAfterMs) {
- await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
- }
- }
- return result;
- }, { maxRetries: 3 });
-
-export const matrixApi = createApi({
- reducerPath: 'matrixApi',
- baseQuery: baseQueryWithRetry,
- refetchOnReconnect: true,
- 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'],
- }),
- sync: builder.query<any[], void>({
- keepUnusedDataFor: 2147483647,
- // The query is not relevant here as the data will be provided via streaming updates.
- // A queryFn returning an empty array is used, with contents being populated via
- // streaming updates below as they are received.
- queryFn: () => ({ data: [] }),
- async onCacheEntryAdded(_arg, { updateCachedData, cacheEntryRemoved, getState }) {
- // TODO: /sync loop
- /*
- updateCachedData((draft) => {
- draft.push(JSON.parse(event.data))
- })
- */
- await cacheEntryRemoved
- // Cleanup?
- },
- }),
- }),
-});
-
-const rawBaseQuery = (baseUrl: string) => fetchBaseQuery({
- baseUrl,
- prepareHeaders: (headers, { getState }) => {
- const token = (getState() as RootState).auth.accessToken;
- !!token && headers.set('Authorization', `Bearer ${token}`);
-
- return headers;
- },
-});
-
-
-// Export hooks for usage in functional components, which are
-// auto-generated based on the defined endpoints
-export const { useLazyGetLoginFlowsQuery, useDoLoginMutation, useLazyGetWellKnownQuery, useSyncQuery } = matrixApi
diff --git a/src/app/api/apiTypes.ts b/src/app/api/apiTypes.ts
@@ -1,49 +0,0 @@
-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/hooks.ts b/src/app/hooks.ts
@@ -1,6 +0,0 @@
-import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
-import type { RootState, AppDispatch } from './store';
-
-// Use throughout your app instead of plain `useDispatch` and `useSelector`
-export const useAppDispatch = () => useDispatch<AppDispatch>();
-export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
-\ No newline at end of file
diff --git a/src/app/protectedRoute.tsx b/src/app/protectedRoute.tsx
@@ -1,12 +1,10 @@
-import { ReactElement } from "react";
+import { ReactElement, useContext } from "react";
import { Navigate } from "react-router-dom";
-import { useAppSelector } from "./hooks";
+import { MatrixContext } from "./sdk/client";
export const ProtectedRoute = ({ children }: { children: ReactElement<any, any> }) => {
- const client = useAppSelector(state => state.auth.host);
-
- // TODO: relaunch a client if we can and skip login
- if (!client) {
+ const matrixClient = useContext(MatrixContext);
+ if (!matrixClient.isLoggedIn) {
// user is not authenticated
return <Navigate to="login" />;
}
diff --git a/src/app/sdk/api/apiTypes.ts b/src/app/sdk/api/apiTypes.ts
@@ -0,0 +1,235 @@
+export interface ILoginFlow {
+ type: string;
+}
+export interface ILoginFlows {
+ flows: ILoginFlow[];
+}
+
+export interface IErrorResp {
+ errcode: string;
+ error?: string;
+}
+
+export interface IRateLimitError extends IErrorResp {
+ 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;
+ }
+ "org.matrix.msc3575.proxy"?: {
+ url: string;
+ }
+}
+
+export interface IAccountData {
+ events?: any[];
+}
+
+export interface IClientEventWithoutRoomId {
+ content: any;
+ event_id: string;
+ origin_server_ts: number;
+ sender: string;
+ // FIXME: Is this really meant to be optional?
+ state_key?: string;
+ type: string;
+ unsigned?: {
+ age?: number;
+ prev_content?: any;
+ redacted_because?: IClientEventWithoutRoomId;
+ transaction_id?: string;
+ }
+}
+
+export interface ITimeline {
+ events: IClientEventWithoutRoomId[];
+ limited: boolean;
+ prev_batch: string;
+}
+
+// Old sync
+export interface ISyncResponse {
+ account_data?: IAccountData;
+ device_lists?: {
+ changed?: string[];
+ left?: string[];
+ };
+ device_one_time_keys_count?: {
+ [key: string]: number;
+ };
+ next_batch: string;
+ presence?: {
+ events?: any[];
+ };
+ rooms?: {
+ invite?: {
+ [key: string]: {
+ invite_state?: {
+ events?: {
+ content: any;
+ sender: string;
+ state_key: string;
+ type: string;
+ // Future proofing
+ [key: string]: any;
+ }[];
+ };
+ };
+ };
+ join?: {
+ [key: string]: {
+ account_data?: IAccountData;
+ ephemeral?: {
+ events?: any[];
+ };
+ state?: {
+ events: IClientEventWithoutRoomId[];
+ }
+ summary?: {
+ "m.heroes": string[];
+ "m.joined_member_count": number;
+ "m.invited_member_count": number;
+ };
+ timeline?: ITimeline;
+ unread_notifications?: {
+ highlight_count: number;
+ notification_count: number;
+ };
+ unread_thread_notifications?: {
+ [key: string]: {
+ highlight_count: number;
+ notification_count: number;
+ };
+ };
+ };
+ };
+ leave?: {
+ [key: string]: {
+ state?: {
+ events: IClientEventWithoutRoomId[];
+ };
+ account_data?: IAccountData;
+ timeline?: ITimeline;
+ };
+ };
+ knock?: {
+ [key: string]: {
+ knock_state?: {
+ events?: {
+ content: any;
+ sender: string;
+ state_key: string;
+ type: string;
+ }[];
+ };
+ };
+ };
+ };
+ to_device?: {
+ events?: any[];
+ };
+}
+
+// Sliding sync
+export interface ISlidingSyncResp {
+ lists?: {
+ [key: string]: List;
+ };
+ rooms?: {
+ [key: string]: RoomJson;
+ };
+ extensions?: Extensions;
+ pos: string;
+ txn_id: string;
+}
+
+export interface Extensions { }
+
+export interface List {
+ ops?: (SYNC_OP | INSERT_OP | INVALIDATE_OP | DELETE_OP)[];
+ count: number;
+}
+
+export function isSyncOp(op: any): op is SYNC_OP {
+ return op.op === "SYNC";
+}
+
+export function isInsertOp(op: any): op is INSERT_OP {
+ return op.op === "INSERT";
+}
+
+export function isInvalidateOp(op: any): op is INVALIDATE_OP {
+ return op.op === "INVALIDATE";
+}
+
+export function isDeleteOp(op: any): op is DELETE_OP {
+ return op.op === "DELETE";
+}
+
+export interface DELETE_OP {
+ op: string;
+ index: number;
+}
+
+export interface INVALIDATE_OP {
+ op: string;
+ range: number[];
+}
+
+export interface INSERT_OP {
+ op: string;
+ index: number;
+ room_id: string;
+}
+
+export interface SYNC_OP {
+ op: string;
+ range: number[];
+ room_ids: string[];
+}
+
+export interface RoomJson {
+ name: string,
+ // List of events
+ timeline: any[],
+ notification_count: number,
+ highlight_count: number,
+ initial: boolean,
+ joined_count: number,
+ invited_count: number,
+ prev_batch: string,
+}
+\ No newline at end of file
diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts
@@ -0,0 +1,370 @@
+import { createContext, useEffect, useState } from "react";
+import { IErrorResp, ILoginFlows, ILoginResponse, IRateLimitError, ISlidingSyncResp, IWellKnown, isDeleteOp, isInsertOp, isInvalidateOp, isSyncOp } from "./api/apiTypes";
+import { Room } from "./room";
+import EventEmitter from "events";
+
+export interface MatrixClientEvents {
+ // Used to notify about changes to the room list
+ 'rooms': (rooms: Room[]) => void;
+ //'delete': (changedCount: number) => void;
+}
+export declare interface MatrixClient {
+ on<U extends keyof MatrixClientEvents>(
+ event: U, listener: MatrixClientEvents[U]
+ ): this;
+
+ emit<U extends keyof MatrixClientEvents>(
+ event: U, ...args: Parameters<MatrixClientEvents[U]>
+ ): boolean;
+}
+
+
+export class MatrixClient extends EventEmitter {
+ private static _instance: MatrixClient;
+ // TODO: This needs to be stored
+ private access_token?: string;
+ // TODO: This needs to be stored
+ private device_id?: string;
+ // TODO: This needs to be stored
+ private mxid?: string;
+ // TODO: This needs to be stored
+ // Hostname including "https://"
+ private hostname?: string;
+ // TODO: This needs to be stored
+ private slidingSyncHostname?: string;
+ private syncing = false;
+ private roomsInView: string[] = [];
+ // TODO: This needs to be stored
+ private roomToRoom: Map<number, Room> = new Map();
+ // TODO: This needs to be stored
+ private syncPos?: string;
+ // TODO: This needs to be stored
+ private initialSync = true;
+
+ public get isLoggedIn(): boolean {
+ return this.access_token !== undefined;
+ }
+
+ public static get Instance() {
+ // Do you need arguments? Make it a regular static method instead.
+ return this._instance || (this._instance = new this());
+ }
+
+ private set setHostname(hostname: string) {
+ if (!hostname.startsWith("https://")) {
+ throw Error("Hostname must start with 'https://'");
+ }
+ this.hostname = hostname;
+ }
+
+ public async startSync() {
+ if (!this.isLoggedIn) {
+ throw Error("Not logged in");
+ }
+ if (this.syncing) {
+ return;
+ }
+ this.syncing = true;
+ while (this.syncing) {
+ try {
+ await this.sync();
+ } catch (e) {
+ console.error(e);
+ return;
+ }
+ }
+ }
+
+ public stopSync() {
+ this.syncing = false;
+ }
+
+ private async sync() {
+ if (!this.isLoggedIn) {
+ throw Error("Not logged in");
+ }
+ if (!this.slidingSyncHostname) {
+ throw Error("Hostname must be set first");
+ }
+
+ // This is the initial sync case for each list
+ let lists_ranges = {
+ "overview": [[0, 10]]
+ };
+ let timeline_limit = 1;
+ if (!this.initialSync) {
+ // Set higher timeline limit for subsequent syncs
+ timeline_limit = 10;
+ // Calculate overlap between this.roomsInView and this.roomToRoomID and then
+ // calculate the ranges for each list
+ const rawRangeInView = [...this.roomToRoom.entries()]
+ .filter(([_, room]) => this.roomsInView.includes(room.roomID))
+ .map(([rangeID]) => rangeID)
+ .sort();
+
+ // Increment range by 1 to make sure we always get a little more than we need
+ // [1,2,3,4,7,8,9,10,11] -> [2,3,4,5,8,9,10,11,12]
+ rawRangeInView.push(rawRangeInView[rawRangeInView.length - 1] + 1);
+
+ // Turn an input like [1,2,3,4,7,8,9,10,11] to [[1,4], [7,11]]
+ const rangesInView = rawRangeInView.reduce((acc, cur, i, arr) => {
+ if (i === 0) {
+ // [1,2,3,4] -> [[1,1]]
+ acc.push([cur, cur]);
+ return acc;
+ }
+ // Cur = 2, arr = [1,2,3,4], arr[i - 1] + 1 = 2 then
+ if (cur === arr[i - 1] + 1) {
+ // [1,2,3,4,7] -> [[1,2]]
+ acc[acc.length - 1][1] = cur;
+ return acc;
+ }
+ // Else [1,2,3,4,7] -> [[1,2], [7,7]]
+ acc.push([cur, cur]);
+ return acc;
+ }, [] as [number, number][]);
+
+ lists_ranges = {
+ "overview": rangesInView
+ };
+ }
+
+
+ let url = `${this.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=30000`;
+ if (this.syncPos) {
+ url = `${this.slidingSyncHostname}/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=30000&pos=${this.syncPos}`
+ }
+
+
+ console.log(url)
+ const resp = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${this.access_token}`
+ },
+ body: JSON.stringify({
+ // allows clients to know what request params reached the server,
+ // functionally similar to txn IDs on /send for events.
+ // TODO: check resp
+ txn_id: Date.now().toString(),
+
+ // a delta token to remember information between sessions.
+ // See "Bandwidth optimisations for persistent clients" for more information.
+ // TODO: This isnt implemented anywhere yet
+ //delta_token: "opaque-server-provided-string",
+
+ // Sliding Window API
+ lists: {
+ "overview": {
+ ranges: lists_ranges["overview"],
+ sort: ["by_notification_level", "by_recency", "by_name"],
+ required_state: [
+ // needed to build sections
+ ["m.space.child", "*"],
+ ["m.space.parent", "*"],
+ // Room Avatar
+ ["m.room.avatar", "*"],
+ ],
+ timeline_limit: timeline_limit,
+ filters: {}
+ },
+ },
+ bump_event_types: ["m.room.message", "m.room.encrypted"],
+
+ // Room Subscriptions API
+ room_subscriptions: {},
+ unsubscribe_rooms: []
+ })
+ });
+ if (!resp.ok) {
+ console.error(resp);
+ throw Error("Error syncing. See console for error.");
+ }
+ const json = await resp.json() as ISlidingSyncResp;
+ this.syncPos = json.pos;
+ for (const listKey in json.lists) {
+ const list = json.lists[listKey];
+ if (list.ops) {
+ for (const op of list.ops) {
+ if (isSyncOp(op)) {
+ for (let i = op.range[0]; i <= op.range[1]; i++) {
+ // We shall first forget about these and "startover"
+ this.roomToRoom.delete(i);
+
+ // We start to remember the Room now.
+ this.roomToRoom.set(i, new Room(op.room_ids[i]));
+ }
+ } else if (isInsertOp(op)) {
+ console.log("Got INSERT OP", op);
+ } else if (isDeleteOp(op)) {
+ this.roomToRoom.delete(op.index);
+ } else if (isInvalidateOp(op)) {
+ for (let i = op.range[0]; i <= op.range[1]; i++) {
+ // We shall first forget about these and "startover"
+ this.roomToRoom.delete(i);
+ }
+ }
+ }
+ }
+ }
+ for (const roomID in json.rooms) {
+ const room = json.rooms[roomID];
+ const name = room.name;
+ const notification_count = room.notification_count;
+ const notification_highlight_count = room.highlight_count;
+ const joined_count = room.joined_count;
+ const invited_count = room.invited_count;
+
+ // Add the room data to the Room from this.roomToRoomID
+ // Note that the map key does not mean the roomID but the sync position
+ // RoomID is part of the value object of the map
+ // value.roomID is the roomID
+ const roomObj = [...this.roomToRoom.values()].find(room => room.roomID === roomID);
+
+ if (!roomObj) {
+ console.error("Could not find roomObj for roomID", roomID);
+ continue;
+ }
+
+ roomObj.setName(name);
+ roomObj.setNotificationCount(notification_count);
+ roomObj.setNotificationHighlightCount(notification_highlight_count);
+ roomObj.setJoinedCount(joined_count);
+ roomObj.setInvitedCount(invited_count);
+ }
+ this.emit("rooms", [...this.roomToRoom.values()]);
+ }
+
+ /**
+ * addInViewRoom
+ *
+ * Tells the sync that a room with a certain roomID is inView.
+ */
+ public addInViewRoom(roomID: string) {
+ this.roomsInView.push(roomID);
+ }
+
+ /**
+ * removeInViewRoom
+ *
+ * Tells the sync that a room isn't in the room anymore
+ */
+ public removeInViewRoom(roomID: string) {
+ this.roomsInView = this.roomsInView.filter(room => room !== roomID);
+ }
+
+
+ private async getLoginFlows(): Promise<ILoginFlows> {
+ if (!this.hostname) {
+ throw Error("Hostname must be set first");
+ }
+ const resp = await fetch(`${this.hostname}/_matrix/client/v3/login`);
+ if (!resp.ok) {
+ console.error(resp);
+ throw Error("Error requesting login flows. See console for error.");
+ }
+ const json = await resp.json() as ILoginFlows;
+ return json;
+ }
+
+ private async getWellKnown(): Promise<IWellKnown> {
+ if (!this.hostname) {
+ throw Error("Hostname must be set first");
+ }
+ const resp = await fetch(`${this.hostname}/.well-known/matrix/client`);
+ if (!resp.ok) {
+ console.error(resp);
+ throw Error("Error requesting login flows. See console for error.");
+ }
+ const json = await resp.json() as IWellKnown;
+ return json;
+ }
+
+ public async passwordLogin(username: string, password: string, triesLeft = 5) {
+ this.setHostname = `https://${username.split(':')[1]}`;
+
+ try {
+ const well_known = await this.getWellKnown();
+ if (well_known["m.homeserver"]?.base_url) {
+ this.setHostname = well_known["m.homeserver"].base_url;
+ }
+ if (well_known["org.matrix.msc3575.proxy"]?.url) {
+ this.slidingSyncHostname = well_known["org.matrix.msc3575.proxy"].url;
+ } else {
+ // TODO: we might want to check for synapse support somehow
+ throw Error("No sliding sync proxy found");
+ }
+ } catch (e: any) {
+ console.warn(`No well-known found for ${this.hostname}:\n${e}`);
+ }
+
+ const loginFlows = await this.getLoginFlows();
+ if ((loginFlows.flows.filter((flow) => flow.type === 'm.login.password')?.length || 0) == 0) {
+ throw Error("Password login is not supported by this homeserver");
+ }
+
+ const resp = await fetch(`${this.hostname}/_matrix/client/r0/login`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ type: "m.login.password",
+ identifier: {
+ type: 'm.id.user',
+ user: username,
+ },
+ user: username,
+ password: password
+ })
+ });
+ if (!resp.ok) {
+ console.error(resp);
+ throw Error("Error logging in. See console for error.");
+ }
+ const json = await resp.json();
+ if (isErrorResp(json)) {
+ throw Error(`Error logging in: ${json.errcode}: ${json.error}`);
+ }
+ if (isRateLimitError(json)) {
+ console.error(`Rate limited. Retrying in ${json.retry_after_ms}ms. ${triesLeft} tries left.`);
+ await this.passwordLogin(username, password, triesLeft - 1);
+ }
+ if (isLoginResponse(json)) {
+ this.access_token = json.access_token;
+ this.device_id = json.device_id;
+ this.mxid = json.user_id;
+ }
+ }
+}
+
+function isRateLimitError(arg: any): arg is IRateLimitError {
+ return arg.retry_after_ms !== undefined;
+}
+
+function isLoginResponse(arg: any): arg is ILoginResponse {
+ return arg.access_token !== undefined;
+}
+
+function isErrorResp(arg: any): arg is IErrorResp {
+ return arg.errcode !== undefined;
+}
+
+export const defaultMatrixClient = MatrixClient.Instance;
+export const MatrixContext = createContext(defaultMatrixClient);
+
+// List of rooms
+export function useRooms() {
+ const client = MatrixClient.Instance;
+ const [rooms, setRooms] = useState<Room[]>([]);
+
+ useEffect(() => {
+ // This is a no-op if there is already a sync
+ client.startSync();
+ // Listen for room updates
+ client.on("rooms", (rooms: Room[]) => {
+ setRooms(rooms);
+ });
+ }, [])
+ return rooms;
+}
+\ No newline at end of file
diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts
@@ -0,0 +1,56 @@
+export class Room {
+ private events: any[] = [];
+ private name: string = "Unknown Room";
+
+ private notification_count: number = 0;
+ private notification_highlight_count: number = 0;
+ private joined_count: number = 0;
+ private invited_count: number = 0;
+
+
+ constructor(public roomID: string) { }
+
+ public addEvent(event: any) {
+ this.events.push(event);
+ }
+
+ public setName(name: string) {
+ this.name = name;
+ }
+
+ public getName(): string {
+ return this.name;
+ }
+
+ public setNotificationCount(count: number) {
+ this.notification_count = count;
+ }
+
+ public getNotificationCount(): number {
+ return this.notification_count;
+ }
+
+ public setNotificationHighlightCount(count: number) {
+ this.notification_highlight_count = count;
+ }
+
+ public getNotificationHighlightCount(): number {
+ return this.notification_highlight_count;
+ }
+
+ public setJoinedCount(count: number) {
+ this.joined_count = count;
+ }
+
+ public getJoinedCount(): number {
+ return this.joined_count;
+ }
+
+ public setInvitedCount(count: number) {
+ this.invited_count = count;
+ }
+
+ public getInvitedCount(): number {
+ return this.invited_count;
+ }
+}
+\ No newline at end of file
diff --git a/src/app/store.ts b/src/app/store.ts
@@ -1,46 +0,0 @@
-import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
-import { matrixApi, auth } from "./api/api";
-import { setupListeners } from "@reduxjs/toolkit/dist/query/react";
-import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistCombineReducers, persistStore } from "reduxjs-toolkit-persist";
-import createIdbStorage from '@piotr-cz/redux-persist-idb-storage'
-
-const persistConfig = {
- key: 'root',
- version: 1,
- storage: createIdbStorage({ name: 'cetirizine', storeName: 'cetirizine' }),
- serialize: false,
- deserialize: false,
-}
-
-const persistedReducer = persistCombineReducers(
- persistConfig,
- {
- auth: auth,
- [matrixApi.reducerPath]: matrixApi.reducer,
- }
-);
-
-export const store = configureStore({
- reducer: persistedReducer,
- middleware: (getDefaultMiddleware) => [
- ...getDefaultMiddleware({
- serializableCheck: {
- ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
- },
- }),
- matrixApi.middleware,
- ],
-});
-
-setupListeners(store.dispatch)
-
-export const persistor = persistStore(store);
-
-export type AppDispatch = typeof store.dispatch;
-export type RootState = ReturnType<typeof store.getState>;
-export type AppThunk<ReturnType = void> = ThunkAction<
- ReturnType,
- RootState,
- unknown,
- Action<string>
->;
-\ No newline at end of file
diff --git a/src/components/input/basic/input.tsx b/src/components/input/basic/input.tsx
@@ -31,7 +31,7 @@ const Input: FC<InputProps> = ({ placeholder, password = false, autoFocus = fals
return (
<input
disabled={readonly}
- className='form-input rounded-lg disabled:bg-slate-200 disabled:cursor-not-allowed transition-colors ease-in-out delay-150'
+ className='text-base form-input rounded-lg disabled:bg-slate-200 disabled:cursor-not-allowed transition-colors ease-in-out delay-150'
value={value}
type={password ? "password" : "text"}
autoFocus={autoFocus}
diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx
@@ -1,58 +1,27 @@
-import { useState } from 'react';
-import { useAppDispatch } from '../../app/hooks';
-import { setHost, useDoLoginMutation, useLazyGetLoginFlowsQuery, useLazyGetWellKnownQuery } from '../../app/api/api';
+import { useContext, useState } from 'react';
import Button from '../button/button';
import Header from '../header/header';
import Input from '../input/basic/input';
import { Navigate } from 'react-router-dom';
-import { SerializedError } from '@reduxjs/toolkit';
-import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query';
+import { MatrixContext } from '../../app/sdk/client';
export function Login() {
- const [login, { isLoading: loginPending, isSuccess, error: loginErrorRaw }] = useDoLoginMutation();
- const [triggerWellKnown] = useLazyGetWellKnownQuery();
- const [triggerLoginFlows] = useLazyGetLoginFlowsQuery();
- const [loginError, setLoginError] = useState<FetchBaseQueryError | SerializedError | undefined>(loginErrorRaw);
+ const matrixClient = useContext(MatrixContext);
+ const [loginPending, setLoginPending] = useState(false);
+ const [loginError, setLoginError] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
- const dispatch = useAppDispatch();
-
- if (isSuccess) {
+ if (matrixClient.isLoggedIn) {
return <Navigate to="/" />;
}
const startLogin = async () => {
- dispatch(setHost(`https://${username.split(':')[1]}`));
- const { isError, error } = await triggerWellKnown(undefined);
- if (isError) {
- setLoginError(error);
- return;
- }
- const { data } = await triggerLoginFlows(undefined, true);
- if ((data?.flows?.filter((flow) => flow.type === 'm.login.password')?.length || 0) > 0) {
- const payload = await login({
- type: 'm.login.password',
- identifier: {
- type: 'm.id.user',
- user: username,
- },
- user: username,
- password: password
- });
- if ('error' in payload) {
- if (JSON.stringify(payload.error).includes('M_LIMIT_EXCEEDED')) {
- setLoginError({ status: "CUSTOM_ERROR", error: 'Too many attempts, try again later' });
- return;
- }
- if (JSON.stringify(payload.error).includes('M_FORBIDDEN')) {
- setLoginError({ status: "CUSTOM_ERROR", error: (payload.error as any).data.error });
- return;
- }
- setLoginError(payload.error);
- return;
- }
- } else {
- setLoginError({ status: "CUSTOM_ERROR", error: 'No password login flow found' });
+ try {
+ setLoginPending(true);
+ await matrixClient.passwordLogin(username, password);
+ } catch (e: any) {
+ setLoginError(e);
}
+ setLoginPending(false);
}
return (
<form className="flex flex-col rounded-md shadow p-4 bg-white gap-2 min-w-[30rem]" onSubmit={(e) => {
@@ -60,7 +29,7 @@ export function Login() {
startLogin();
}}>
<Header>Login</Header>
- {loginError ? <h2 className='text-red-500 font-normal text-sm'>{'error' in loginError ? loginError.error : ('data' in loginError ? JSON.stringify(loginError.data) : loginError.message)}</h2> : <div className='min-h-[1.25rem]'></div>}
+ {loginError ? <h2 className='text-red-500 font-normal text-sm'>{loginError}</h2> : <div className='min-h-[1.25rem]'></div>}
<Input
readonly={loginPending}
value={username}
diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx
@@ -83,6 +83,7 @@ const RoomListRooms: FC<RoomListRoomsProps> = ({ sectionID, rooms, onClick, acti
const roomsRendered = rooms.map(room => {
return (
<RoomListItem
+ roomId={room.roomID}
hidden={hidden}
key={room.roomID + sectionID}
avatarUrl={room.avatarUrl}
diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx
@@ -1,9 +1,14 @@
import { useInView } from "react-intersection-observer";
import Avatar from "../../avatar/avatar";
-import { FC } from "react";
+import { FC, useContext, useEffect } from "react";
+import { MatrixContext } from "../../../app/sdk/client";
type RoomListItemProps = {
/**
+ * Room id
+ */
+ roomId: string
+ /**
* The URL of the Avatar image
*/
avatarUrl?: string
@@ -33,12 +38,20 @@ type RoomListItemProps = {
hidden: boolean
};
-const RoomListItem: FC<RoomListItemProps> = ({ avatarUrl, displayname, dm = false, online = false, active = false, onClick, hidden }: RoomListItemProps) => {
+const RoomListItem: FC<RoomListItemProps> = ({ roomId, avatarUrl, displayname, dm = false, online = false, active = false, onClick, hidden }: RoomListItemProps) => {
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: '200px 0px',
skip: hidden,
});
+ const matrixClient = useContext(MatrixContext);
+ useEffect(() => {
+ if (inView) {
+ matrixClient.addInViewRoom(roomId)
+ } else {
+ matrixClient.removeInViewRoom(roomId)
+ }
+ }, [inView])
return (
<div ref={ref} onClick={onClick} className="w-full cursor-pointer">
{
diff --git a/src/main.tsx b/src/main.tsx
@@ -1,22 +1,21 @@
/// <reference types="./@types/global.d.ts" />
import React from 'react'
import { createRoot } from 'react-dom/client';
-import { Provider } from 'react-redux';
-import { persistor, store } from './app/store';
import App from './App';
import './index.scss';
-import { PersistGate } from 'reduxjs-toolkit-persist/integration/react';
+import { enableMapSet, enablePatches } from "immer"
+import { MatrixContext, defaultMatrixClient } from './app/sdk/client';
+
+enableMapSet();
+enablePatches();
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<React.StrictMode>
- <Provider store={store}>
- <PersistGate loading={<span>Loading...</span>} persistor={persistor}>
- <App />
- </PersistGate>
-
- </Provider>
+ <MatrixContext.Provider value={defaultMatrixClient}>
+ <App />
+ </MatrixContext.Provider>
</React.StrictMode>
);
\ No newline at end of file
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx
@@ -3,11 +3,10 @@ import Avatar from '../components/avatar/avatar';
import ChatInput from '../components/input/chat/input';
import RoomList from '../components/roomList/roomList';
import './MainPage.scss';
-import { useSyncQuery } from '../app/api/api';
-
+import { useRooms } from '../app/sdk/client';
export default function MainPage() {
- const { } = useSyncQuery();
- // TODO: Calculate rooms and sections within a RTK Query
+ const matrixRooms = useRooms();
+ console.log(matrixRooms.length);
const sections = [
{
sectionName: "Test Section",
@@ -79,7 +78,6 @@ export default function MainPage() {
</div>
<div className='flex-1 flex flex-col'>
<div className='flex-1'>
- TODO: events and welcome page
</div>
<ChatInput namespace='Editor' onChange={() => { }} onError={(e) => console.error(e)} />
</div>