element-call-Github/src/ClientContext.tsx

339 lines
8.5 KiB
TypeScript
Raw Normal View History

2022-01-06 09:19:03 +08:00
/*
Copyright 2021-2022 New Vector Ltd
2022-01-06 09:19:03 +08:00
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {
FC,
2022-01-06 09:19:03 +08:00
useCallback,
useEffect,
useState,
createContext,
useMemo,
useContext,
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2022-06-28 05:41:07 +08:00
import { logger } from "matrix-js-sdk/src/logger";
import { ErrorView } from "./FullScreenView";
import {
initClient,
initMatroskaClient,
defaultHomeserver,
CryptoStoreIntegrityError,
} from "./matrix-utils";
2022-01-06 09:19:03 +08:00
declare global {
interface Window {
matrixclient: MatrixClient;
}
}
export interface Session {
user_id: string;
device_id: string;
access_token: string;
passwordlessUser: boolean;
tempPassword?: string;
}
const loadSession = (): Session => {
const data = localStorage.getItem("matrix-auth-store");
if (data) return JSON.parse(data);
return null;
};
const saveSession = (session: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const clearSession = () => localStorage.removeItem("matrix-auth-store");
interface ClientState {
loading: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
client: MatrixClient;
userName: string;
changePassword: (password: string) => Promise<void>;
logout: () => void;
setClient: (client: MatrixClient, session: Session) => void;
2022-07-28 06:22:48 +08:00
error?: Error;
}
const ClientContext = createContext<ClientState>(null);
type ClientProviderState = Omit<
ClientState,
"changePassword" | "logout" | "setClient"
> & { error?: Error };
2022-01-06 09:19:03 +08:00
2022-07-08 21:56:00 +08:00
interface Props {
children: JSX.Element;
}
export const ClientProvider: FC<Props> = ({ children }) => {
2022-01-06 09:19:03 +08:00
const history = useHistory();
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
2022-01-06 09:19:03 +08:00
setState,
] = useState<ClientProviderState>({
2022-01-06 09:19:03 +08:00
loading: true,
isAuthenticated: false,
isPasswordlessUser: false,
client: undefined,
userName: null,
error: undefined,
2022-01-06 09:19:03 +08:00
});
useEffect(() => {
2022-06-28 05:41:07 +08:00
const init = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => {
2022-06-28 05:41:07 +08:00
const query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId");
const parentUrl = query.get("parentUrl");
if (widgetId && parentUrl) {
// We're inside a widget, so let's engage *Matroska mode*
logger.log("Using a Matroska client");
2022-01-06 09:19:03 +08:00
2022-06-28 05:41:07 +08:00
return {
client: await initMatroskaClient(widgetId, parentUrl),
isPasswordlessUser: false,
};
} else {
// We're running as a standalone application
try {
const session = loadSession();
if (!session) return { client: undefined, isPasswordlessUser: false };
logger.log("Using a standalone client");
/* eslint-disable camelcase */
const { user_id, device_id, access_token, passwordlessUser } =
session;
try {
return {
client: await initClient(
{
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
},
true
),
isPasswordlessUser: passwordlessUser,
};
} catch (err) {
if (err instanceof CryptoStoreIntegrityError) {
// We can't use this session anymore, so let's log it out
try {
const client = await initClient(
{
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
},
false // Don't need the crypto store just to log out
);
2022-08-13 08:13:52 +08:00
await client.logout(undefined, true);
} catch (err_) {
logger.warn(
"The previous session was lost, and we couldn't log it out, " +
"either"
);
}
}
throw err;
2022-06-28 05:41:07 +08:00
}
/* eslint-enable camelcase */
2022-06-28 05:41:07 +08:00
} catch (err) {
clearSession();
throw err;
}
2022-01-06 09:19:03 +08:00
}
};
2022-01-06 09:19:03 +08:00
2022-06-28 05:41:07 +08:00
init()
.then(({ client, isPasswordlessUser }) => {
2022-01-06 09:19:03 +08:00
setState({
client,
loading: false,
isAuthenticated: Boolean(client),
isPasswordlessUser,
2022-01-06 09:19:03 +08:00
userName: client?.getUserIdLocalpart(),
2022-08-02 06:46:16 +08:00
error: undefined,
2022-01-06 09:19:03 +08:00
});
})
2022-06-28 05:41:07 +08:00
.catch((err) => {
logger.error(err);
2022-01-06 09:19:03 +08:00
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
2022-08-02 06:46:16 +08:00
error: undefined,
2022-01-06 09:19:03 +08:00
});
});
}, []);
const changePassword = useCallback(
async (password: string) => {
const { tempPassword, ...session } = loadSession();
2022-01-06 09:19:03 +08:00
await client.setPassword(
{
type: "m.login.password",
identifier: {
type: "m.id.user",
user: session.user_id,
2022-01-06 09:19:03 +08:00
},
user: session.user_id,
2022-01-06 09:19:03 +08:00
password: tempPassword,
},
password
);
saveSession({ ...session, passwordlessUser: false });
2022-01-06 09:19:03 +08:00
setState({
client,
loading: false,
isAuthenticated: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
2022-08-02 06:46:16 +08:00
error: undefined,
2022-01-06 09:19:03 +08:00
});
},
[client]
);
2022-02-16 04:46:58 +08:00
const setClient = useCallback(
(newClient: MatrixClient, session: Session) => {
2022-02-16 04:46:58 +08:00
if (client && client !== newClient) {
client.stopClient();
}
2022-01-06 09:19:03 +08:00
2022-02-16 04:46:58 +08:00
if (newClient) {
saveSession(session);
2022-01-06 09:19:03 +08:00
2022-02-16 04:46:58 +08:00
setState({
client: newClient,
loading: false,
isAuthenticated: true,
isPasswordlessUser: session.passwordlessUser,
2022-02-16 04:46:58 +08:00
userName: newClient.getUserIdLocalpart(),
2022-08-02 06:46:16 +08:00
error: undefined,
2022-02-16 04:46:58 +08:00
});
} else {
clearSession();
2022-02-16 04:46:58 +08:00
setState({
client: undefined,
loading: false,
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
2022-08-02 06:46:16 +08:00
error: undefined,
2022-02-16 04:46:58 +08:00
});
}
},
[client]
);
2022-01-06 09:19:03 +08:00
const logout = useCallback(() => {
clearSession();
history.push("/");
2022-01-06 09:19:03 +08:00
}, [history]);
useEffect(() => {
if (client) {
const loadTime = Date.now();
const onToDeviceEvent = (event: MatrixEvent) => {
if (event.getType() !== "org.matrix.call_duplicate_session") return;
const content = event.getContent();
if (content.session_id === client.getSessionId()) return;
if (content.timestamp > loadTime) {
client?.stopClient();
setState((prev) => ({
...prev,
error: new Error(
"This application has been opened in another tab."
),
}));
}
};
client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
client.sendToDevice("org.matrix.call_duplicate_session", {
[client.getUserId()]: {
"*": { session_id: client.getSessionId(), timestamp: loadTime },
},
});
return () => {
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
};
}
}, [client]);
const context = useMemo<ClientState>(
2022-01-06 09:19:03 +08:00
() => ({
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
2022-08-02 06:46:16 +08:00
error: undefined,
2022-01-06 09:19:03 +08:00
}),
[
loading,
isAuthenticated,
isPasswordlessUser,
client,
changePassword,
logout,
userName,
setClient,
]
);
2022-02-16 04:46:58 +08:00
useEffect(() => {
window.matrixclient = client;
}, [client]);
if (error) {
return <ErrorView error={error} />;
}
2022-01-06 09:19:03 +08:00
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
);
};
2022-01-06 09:19:03 +08:00
export const useClient = () => useContext(ClientContext);