commit be3705f5fba2cd36cc5b27bab14ecb94c10ce789
parent 53417f00f4d524592ef1f91199854adf0105bbc7
Author: MTRNord <mtrnord1@gmail.com>
Date: Sat, 22 Apr 2023 20:19:30 +0200
feat: Properly hookup login together with restoring the client after reloads if possible
Diffstat:
7 files changed, 87 insertions(+), 24 deletions(-)
diff --git a/src/app/api/api.ts b/src/app/api/api.ts
@@ -1,23 +1,38 @@
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';
-function login(baseUrl: string, userId: string, password: string): Promise<MatrixClient> {
- return initMatrixClient(baseUrl, userId, 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()!);
+
+ return client;
}
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 {
- yield put(LOGIN_REQUEST_ACTION());
- if (!action.baseUrl.startsWith("https://")) {
- yield put(LOGIN_FAILURE_ACTION("Homeserver url must start with https://"));
- return;
- }
-
- const client = yield call(login, action.baseUrl, action.username, action.password);
+ 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;
}
}
@@ -29,7 +44,7 @@ export function* apiSagas() {
yield all([fork(watchLoginSaga)]);
}
-async function initMatrixClient(baseURL: string, userId: string, accessToken?: string, password?: string): Promise<MatrixClient> {
+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 {
@@ -44,8 +59,19 @@ async function initMatrixClient(baseURL: string, userId: string, accessToken?: s
await store.startup();
}
+ const clientConfig = await AutoDiscovery.findClientConfig(baseUrl.replace("https://", ''));
+
+ 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;
+ }
+ }
+
const matrixClient = createClient({
- baseUrl: baseURL,
+ baseUrl: baseUrl,
accessToken: accessToken,
userId: accessToken ? userId : undefined,
useAuthorizationHeader: true,
diff --git a/src/app/api/reducers.ts b/src/app/api/reducers.ts
@@ -16,7 +16,7 @@ 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, error: undefined, client: undefined };
+const initialLoginState: ApiLoginStatus = { loginPending: false };
export const apiLoginReducer = createReducer(initialLoginState, (builder) => {
diff --git a/src/app/store.ts b/src/app/store.ts
@@ -2,6 +2,7 @@ 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];
@@ -14,6 +15,7 @@ export const store = configureStore({
...getDefaultMiddleware(),
...middlewares,
],
+ preloadedState: await getInitialState()
});
sagaMiddleware.run(rootSaga);
@@ -25,3 +27,21 @@ export type AppThunk<ReturnType = void> = ThunkAction<
unknown,
Action<string>
>;
+
+async function getInitialState() {
+ const localStorage = window.localStorage;
+ const accessToken = localStorage.getItem("accessToken");
+ const baseUrl = localStorage.getItem("baseUrl");
+ const userId = localStorage.getItem("userId");
+ if (accessToken && baseUrl && userId) {
+ const client = await initMatrixClient(baseUrl, userId, accessToken);
+ return {
+ login: {
+ client,
+ loginPending: false
+ }
+ }
+ } else {
+ return undefined
+ }
+}
+\ No newline at end of file
diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx
@@ -18,14 +18,19 @@ type ButtonProps = {
* The button Label
*/
children: string
+
+ /**
+ * If the button is readonly
+ */
+ readonly: boolean
};
-export default memo(function Button({ type = "button", style = "primary", onClick, children }: ButtonProps) {
+export default memo(function Button({ type = "button", style = "primary", onClick, children, readonly }: ButtonProps) {
if (style === "secondary") {
- return <button onClick={onClick} className="button bg-orange-400 hover:bg-orange-500 ease-out duration-150" type={type}>{children}</button>;
+ return <button disabled={readonly} onClick={onClick} className="button bg-orange-400 hover:bg-orange-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>;
} else if (style === "abort") {
- return <button onClick={onClick} className="button bg-red-400 hover:bg-red-500 ease-out duration-150" type={type}>{children}</button>;
+ return <button disabled={readonly} onClick={onClick} className="button bg-red-400 hover:bg-red-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>;
} else {
- return <button onClick={onClick} className="button bg-green-400 hover:bg-green-500 ease-out duration-150" type={type}>{children}</button>;
+ return <button disabled={readonly} onClick={onClick} className="button bg-green-400 hover:bg-green-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>;
}
});
\ No newline at end of file
diff --git a/src/components/input/basic/input.stories.tsx b/src/components/input/basic/input.stories.tsx
@@ -49,7 +49,5 @@ export const Default: Story = {
placeholder: "Placeholder",
password: false,
autoFocus: false,
- value: "",
- onChange: (_) => { }
}
};
\ No newline at end of file
diff --git a/src/components/input/basic/input.tsx b/src/components/input/basic/input.tsx
@@ -18,15 +18,20 @@ type InputProps = {
*/
value: string
/**
+ * If the input is readonly
+ */
+ readonly: boolean
+ /**
* Handler for the onChange event
*/
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
-export default memo(function Input({ placeholder, password = false, autoFocus = false, value, onChange }: InputProps) {
+export default memo(function Input({ placeholder, password = false, autoFocus = false, value, readonly, onChange }: InputProps) {
return (
<input
- className='form-input rounded-lg'
+ disabled={readonly}
+ className='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
@@ -9,39 +9,47 @@ import { LOGIN_ACTION } from '../../app/api/reducers';
export function Login() {
const loginError = useAppSelector((state: RootState) => getLoginError(state));
+ const loginPending = useAppSelector((state: RootState) => state.login.loginPending);
const [homeserver, setHomeserver] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const dispatch = useAppDispatch();
return (
- <div className="flex flex-col rounded-md shadow p-4 bg-white gap-2">
+ <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 });
+ }}>
<Header>Login</Header>
- {loginError ? <h2 className='text-red-500 font-normal text-sm'>{loginError}</h2> : <></>}
+ {loginError ? <h2 className='text-red-500 font-normal text-sm'>{loginError}</h2> : <div className='min-h-[1.25rem]'></div>}
<Input
+ readonly={loginPending}
value={homeserver}
autoFocus={true}
placeholder="Homeserver"
onChange={e => setHomeserver(e.target.value)}
/>
<Input
+ readonly={loginPending}
value={username}
placeholder="Username"
onChange={e => setUsername(e.target.value)}
/>
<Input
+ readonly={loginPending}
value={password}
password={true}
placeholder="Password"
onChange={e => setPassword(e.target.value)}
/>
<Button
+ readonly={loginPending}
style="primary"
- type="button"
+ type="submit"
onClick={() => dispatch({ type: LOGIN_ACTION, baseUrl: homeserver, username: username, password: password })}
>
Login
</Button>
- </div>
+ </form>
);
}
\ No newline at end of file