diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f4107103..c7803415 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -1,6 +1,16 @@
module.exports = {
plugins: ["matrix-org"],
- extends: ["plugin:matrix-org/react", "plugin:matrix-org/a11y", "prettier"],
+ extends: [
+ "prettier",
+ "plugin:matrix-org/react",
+ "plugin:matrix-org/a11y",
+ "plugin:matrix-org/typescript",
+ ],
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: "module",
+ project: ["./tsconfig.json"],
+ },
env: {
browser: true,
node: true,
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index a9298638..baa3a5f0 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -19,6 +19,6 @@ jobs:
- name: i18n
run: "yarn run i18n:check"
- name: ESLint
- run: "yarn run lint:js"
+ run: "yarn run lint:eslint"
- name: Type check
run: "yarn run lint:types"
diff --git a/package.json b/package.json
index d70395d7..3d3d938b 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,9 @@
"build-storybook": "build-storybook",
"prettier:check": "prettier -c .",
"prettier:format": "prettier -w .",
- "lint": "yarn lint:types && yarn lint:js",
- "lint:js": "eslint --max-warnings 0 src",
+ "lint": "yarn lint:types && yarn lint:eslint",
+ "lint:eslint": "eslint --max-warnings 0 src",
+ "lint:eslint-fix": "eslint --max-warnings 0 src --fix",
"lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js",
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
@@ -46,6 +47,7 @@
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@types/grecaptcha": "^3.0.4",
+ "@types/react-router-dom": "^5.3.3",
"@types/sdp-transform": "^2.4.5",
"@use-gesture/react": "^10.2.11",
"@vitejs/plugin-basic-ssl": "^1.0.1",
@@ -58,7 +60,7 @@
"i18next-http-backend": "^1.4.4",
"livekit-client": "1.12.0",
"lodash": "^4.17.21",
- "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#9ba44b96202e36d8bd6fbcff3222b7c70fc371d7",
+ "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d79d9ae69c3220c02406706d4a1ec52c22c44fbd",
"matrix-widget-api": "^1.3.1",
"mermaid": "^8.13.8",
"normalize.css": "^8.0.1",
@@ -70,13 +72,15 @@
"react-dom": "18",
"react-i18next": "^11.18.6",
"react-json-view": "^1.21.3",
- "react-router": "6",
"react-router-dom": "^5.2.0",
"react-use-clipboard": "^1.0.7",
"react-use-measure": "^2.1.1",
"sdp-transform": "^2.14.1",
"tinyqueue": "^2.0.3",
- "unique-names-generator": "^4.6.0"
+ "unique-names-generator": "^4.6.0",
+ "uuid": "9",
+ "@types/uuid": "9",
+ "@types/content-type": "^1.1.5"
},
"devDependencies": {
"@babel/core": "^7.16.5",
diff --git a/src/App.tsx b/src/App.tsx
index d020655d..0ef5bcd6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,6 +18,7 @@ import { Suspense, useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays";
+import { History } from "history";
import { HomePage } from "./home/HomePage";
import { LoginPage } from "./auth/LoginPage";
@@ -51,6 +52,8 @@ export default function App({ history }: AppProps) {
const errorPage = ;
return (
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
{loaded ? (
diff --git a/src/Avatar.tsx b/src/Avatar.tsx
index 9549cd48..c11d71d4 100644
--- a/src/Avatar.tsx
+++ b/src/Avatar.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import { useMemo, CSSProperties, HTMLAttributes, FC } from "react";
import classNames from "classnames";
-import { MatrixClient } from "matrix-js-sdk/src/client";
import { getAvatarUrl } from "./matrix-utils";
import { useClient } from "./ClientContext";
@@ -59,9 +58,6 @@ function hashStringToArrIndex(str: string, arrLength: number) {
return sum % arrLength;
}
-const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
- src?.startsWith("mxc://") ? client && getAvatarUrl(client, src, size) : src;
-
interface Props extends HTMLAttributes {
bgKey?: string;
src?: string;
@@ -99,10 +95,10 @@ export const Avatar: FC = ({
[size]
);
- const resolvedSrc = useMemo(
- () => resolveAvatarSrc(client, src, sizePx),
- [client, src, sizePx]
- );
+ const resolvedSrc = useMemo(() => {
+ if (!client || !src || !sizePx) return undefined;
+ return src.startsWith("mxc://") ? getAvatarUrl(client, src, sizePx) : src;
+ }, [client, src, sizePx]);
const backgroundColor = useMemo(() => {
const index = hashStringToArrIndex(
diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx
index ba1b217b..d488d6de 100644
--- a/src/ClientContext.tsx
+++ b/src/ClientContext.tsx
@@ -20,9 +20,9 @@ import {
useEffect,
useState,
createContext,
- useMemo,
useContext,
useRef,
+ useMemo,
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
@@ -31,9 +31,9 @@ import { useTranslation } from "react-i18next";
import { ErrorView } from "./FullScreenView";
import {
- initClient,
CryptoStoreIntegrityError,
fallbackICEServerAllowed,
+ initClient,
} from "./matrix-utils";
import { widget } from "./widget";
import {
@@ -47,184 +47,141 @@ import { Config } from "./config/Config";
declare global {
interface Window {
matrixclient: MatrixClient;
- isPasswordlessUser: boolean;
+ passwordlessUser: boolean;
}
}
-export interface Session {
- user_id: string;
- device_id: string;
- access_token: string;
+export type ClientState = ValidClientState | ErrorState;
+
+export type ValidClientState = {
+ state: "valid";
+ authenticated?: AuthenticatedClient;
+ setClient: (params?: SetClientParams) => void;
+};
+
+export type AuthenticatedClient = {
+ client: MatrixClient;
+ isPasswordlessUser: boolean;
+ changePassword: (password: string) => Promise;
+ logout: () => void;
+};
+
+export type ErrorState = {
+ state: "error";
+ error: Error;
+};
+
+export type SetClientParams = {
+ client: MatrixClient;
+ session: Session;
+};
+
+const ClientContext = createContext(undefined);
+
+export const useClientState = () => useContext(ClientContext);
+
+export function useClient(): {
+ client?: MatrixClient;
+ setClient?: (params?: SetClientParams) => void;
+} {
+ let client;
+ let setClient;
+
+ const clientState = useClientState();
+ if (clientState?.state === "valid") {
+ client = clientState.authenticated?.client;
+ setClient = clientState.setClient;
+ }
+
+ return { client, setClient };
+}
+
+// Plain representation of the `ClientContext` as a helper for old components that expected an object with multiple fields.
+export function useClientLegacy(): {
+ client?: MatrixClient;
+ setClient?: (params?: SetClientParams) => void;
passwordlessUser: boolean;
- tempPassword?: string;
+ loading: boolean;
+ authenticated: boolean;
+ logout?: () => void;
+ error?: Error;
+} {
+ const clientState = useClientState();
+
+ let client;
+ let setClient;
+ let passwordlessUser = false;
+ let loading = true;
+ let error;
+ let authenticated = false;
+ let logout;
+
+ if (clientState?.state === "valid") {
+ client = clientState.authenticated?.client;
+ setClient = clientState.setClient;
+ passwordlessUser = clientState.authenticated?.isPasswordlessUser ?? false;
+ loading = false;
+ authenticated = client !== undefined;
+ logout = clientState.authenticated?.logout;
+ } else if (clientState?.state === "error") {
+ error = clientState.error;
+ loading = false;
+ }
+
+ return {
+ client,
+ setClient,
+ passwordlessUser,
+ loading,
+ authenticated,
+ logout,
+ error,
+ };
}
const loadChannel =
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
-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;
- logout: () => void;
- setClient: (client: MatrixClient, session: Session) => void;
- error?: Error;
-}
-
-const ClientContext = createContext(null);
-
-type ClientProviderState = Omit<
- ClientState,
- "changePassword" | "logout" | "setClient"
-> & { error?: Error };
-
interface Props {
children: JSX.Element;
}
export const ClientProvider: FC = ({ children }) => {
const history = useHistory();
- const initializing = useRef(false);
- const [
- { loading, isAuthenticated, isPasswordlessUser, client, userName, error },
- setState,
- ] = useState({
- loading: true,
- isAuthenticated: false,
- isPasswordlessUser: false,
- client: undefined,
- userName: null,
- error: undefined,
- });
+ const [initClientState, setInitClientState] = useState<
+ InitResult | undefined
+ >(undefined);
+
+ const initializing = useRef(false);
useEffect(() => {
// In case the component is mounted, unmounted, and remounted quickly (as
// React does in strict mode), we need to make sure not to doubly initialize
- // the client
+ // the client.
if (initializing.current) return;
initializing.current = true;
- const init = async (): Promise<
- Pick
- > => {
- if (widget) {
- // We're inside a widget, so let's engage *matryoshka mode*
- logger.log("Using a matryoshka client");
- return {
- client: await widget.client,
- 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;
-
- const livekit = Config.get().livekit;
- const foci = livekit
- ? [
- {
- livekitServiceUrl: livekit.livekit_service_url,
- },
- ]
- : undefined;
-
- try {
- return {
- client: await initClient(
- {
- baseUrl: Config.defaultHomeserverUrl(),
- accessToken: access_token,
- userId: user_id,
- deviceId: device_id,
- fallbackICEServerAllowed: fallbackICEServerAllowed,
- foci,
- },
- 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: Config.defaultHomeserverUrl(),
- accessToken: access_token,
- userId: user_id,
- deviceId: device_id,
- fallbackICEServerAllowed: fallbackICEServerAllowed,
- foci,
- },
- false // Don't need the crypto store just to log out
- );
- await client.logout(true);
- } catch (err_) {
- logger.warn(
- "The previous session was lost, and we couldn't log it out, " +
- "either"
- );
- }
- }
- throw err;
- }
- /* eslint-enable camelcase */
- } catch (err) {
- clearSession();
- throw err;
+ loadClient()
+ .then((maybeClient) => {
+ if (!maybeClient) {
+ logger.error("Failed to initialize client");
+ return;
}
- }
- };
- init()
- .then(({ client, isPasswordlessUser }) => {
- setState({
- client,
- loading: false,
- isAuthenticated: Boolean(client),
- isPasswordlessUser,
- userName: client?.getUserIdLocalpart(),
- error: undefined,
- });
- })
- .catch((err) => {
- logger.error(err);
- setState({
- client: undefined,
- loading: false,
- isAuthenticated: false,
- isPasswordlessUser: false,
- userName: null,
- error: undefined,
- });
+ setInitClientState(maybeClient);
})
+ .catch((err) => logger.error(err))
.finally(() => (initializing.current = false));
}, []);
const changePassword = useCallback(
async (password: string) => {
- const { tempPassword, ...session } = loadSession();
+ const session = loadSession();
+ if (!initClientState?.client || !session) {
+ return;
+ }
- await client.setPassword(
+ await initClientState.client.setPassword(
{
type: "m.login.password",
identifier: {
@@ -232,73 +189,56 @@ export const ClientProvider: FC = ({ children }) => {
user: session.user_id,
},
user: session.user_id,
- password: tempPassword,
+ password: session.tempPassword,
},
password
);
saveSession({ ...session, passwordlessUser: false });
- setState({
- client,
- loading: false,
- isAuthenticated: true,
- isPasswordlessUser: false,
- userName: client.getUserIdLocalpart(),
- error: undefined,
+ setInitClientState({
+ client: initClientState.client,
+ passwordlessUser: false,
});
},
- [client]
+ [initClientState?.client]
);
const setClient = useCallback(
- (newClient: MatrixClient, session: Session) => {
- if (client && client !== newClient) {
- client.stopClient();
+ (clientParams?: SetClientParams) => {
+ const oldClient = initClientState?.client;
+ const newClient = clientParams?.client;
+ if (oldClient && oldClient !== newClient) {
+ oldClient.stopClient();
}
- if (newClient) {
- saveSession(session);
-
- setState({
- client: newClient,
- loading: false,
- isAuthenticated: true,
- isPasswordlessUser: session.passwordlessUser,
- userName: newClient.getUserIdLocalpart(),
- error: undefined,
+ if (clientParams) {
+ saveSession(clientParams.session);
+ setInitClientState({
+ client: clientParams.client,
+ passwordlessUser: clientParams.session.passwordlessUser,
});
} else {
clearSession();
-
- setState({
- client: undefined,
- loading: false,
- isAuthenticated: false,
- isPasswordlessUser: false,
- userName: null,
- error: undefined,
- });
+ setInitClientState(undefined);
}
},
- [client]
+ [initClientState?.client]
);
const logout = useCallback(async () => {
+ const client = initClientState?.client;
+ if (!client) {
+ return;
+ }
+
await client.logout(true);
await client.clearStores();
clearSession();
- setState({
- client: undefined,
- loading: false,
- isAuthenticated: false,
- isPasswordlessUser: true,
- userName: "",
- error: undefined,
- });
+ setInitClientState(undefined);
history.push("/");
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
- }, [history, client]);
+ }, [history, initClientState?.client]);
const { t } = useTranslation();
@@ -310,61 +250,144 @@ export const ClientProvider: FC = ({ children }) => {
if (!widget) loadChannel?.postMessage({});
}, []);
+ const [alreadyOpenedErr, setAlreadyOpenedErr] = useState(
+ undefined
+ );
useEventTarget(
loadChannel,
"message",
useCallback(() => {
- client?.stopClient();
-
- setState((prev) => ({
- ...prev,
- error: translatedError(
- "This application has been opened in another tab.",
- t
- ),
- }));
- }, [client, setState, t])
+ initClientState?.client.stopClient();
+ setAlreadyOpenedErr(
+ translatedError("This application has been opened in another tab.", t)
+ );
+ }, [initClientState?.client, setAlreadyOpenedErr, t])
);
- const context = useMemo(
- () => ({
- loading,
- isAuthenticated,
- isPasswordlessUser,
- client,
- changePassword,
- logout,
- userName,
- setClient,
- error: undefined,
- }),
- [
- loading,
- isAuthenticated,
- isPasswordlessUser,
- client,
- changePassword,
- logout,
- userName,
- setClient,
- ]
- );
+ const state: ClientState = useMemo(() => {
+ if (alreadyOpenedErr) {
+ return { state: "error", error: alreadyOpenedErr };
+ }
+
+ let authenticated = undefined;
+ if (initClientState) {
+ authenticated = {
+ client: initClientState.client,
+ isPasswordlessUser: initClientState.passwordlessUser,
+ changePassword,
+ logout,
+ };
+ }
+
+ return { state: "valid", authenticated, setClient };
+ }, [alreadyOpenedErr, changePassword, initClientState, logout, setClient]);
useEffect(() => {
- window.matrixclient = client;
- window.isPasswordlessUser = isPasswordlessUser;
+ if (!initClientState) {
+ return;
+ }
+
+ window.matrixclient = initClientState.client;
+ window.passwordlessUser = initClientState.passwordlessUser;
if (PosthogAnalytics.hasInstance())
PosthogAnalytics.instance.onLoginStatusChanged();
- }, [client, isPasswordlessUser]);
+ }, [initClientState]);
- if (error) {
- return ;
+ if (alreadyOpenedErr) {
+ return ;
}
return (
- {children}
+ {children}
);
};
-export const useClient = () => useContext(ClientContext);
+type InitResult = {
+ client: MatrixClient;
+ passwordlessUser: boolean;
+};
+
+async function loadClient(): Promise {
+ if (widget) {
+ // We're inside a widget, so let's engage *matryoshka mode*
+ logger.log("Using a matryoshka client");
+ const client = await widget.client;
+ return {
+ client,
+ passwordlessUser: false,
+ };
+ } else {
+ // We're running as a standalone application
+ try {
+ const session = loadSession();
+ if (!session) {
+ throw new Error("No session stored");
+ }
+
+ logger.log("Using a standalone client");
+
+ const foci = Config.get().livekit
+ ? [{ livekitServiceUrl: Config.get().livekit!.livekit_service_url }]
+ : undefined;
+
+ /* eslint-disable camelcase */
+ const { user_id, device_id, access_token, passwordlessUser } = session;
+ const initClientParams = {
+ baseUrl: Config.defaultHomeserverUrl()!,
+ accessToken: access_token,
+ userId: user_id,
+ deviceId: device_id,
+ fallbackICEServerAllowed: fallbackICEServerAllowed,
+ foci,
+ };
+
+ try {
+ const client = await initClient(initClientParams, true);
+ return {
+ client,
+ 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(initClientParams, false); // Don't need the crypto store just to log out)
+ await client.logout(true);
+ } catch (err) {
+ logger.warn(
+ "The previous session was lost, and we couldn't log it out, " +
+ err +
+ "either"
+ );
+ }
+ }
+ throw err;
+ }
+ /* eslint-enable camelcase */
+ } catch (err) {
+ clearSession();
+ throw err;
+ }
+ }
+}
+
+export interface Session {
+ user_id: string;
+ device_id: string;
+ access_token: string;
+ passwordlessUser: boolean;
+ tempPassword?: string;
+}
+
+const clearSession = () => localStorage.removeItem("matrix-auth-store");
+const saveSession = (s: Session) =>
+ localStorage.setItem("matrix-auth-store", JSON.stringify(s));
+const loadSession = (): Session | undefined => {
+ const data = localStorage.getItem("matrix-auth-store");
+ if (!data) {
+ return undefined;
+ }
+
+ return JSON.parse(data);
+};
diff --git a/src/Facepile.tsx b/src/Facepile.tsx
index f5ffff62..0c9ec239 100644
--- a/src/Facepile.tsx
+++ b/src/Facepile.tsx
@@ -47,8 +47,8 @@ export function Facepile({
}: Props) {
const { t } = useTranslation();
- const _size = sizes.get(size);
- const _overlap = overlapMap[size];
+ const _size = sizes.get(size)!;
+ const _overlap = overlapMap[size]!;
const title = useMemo(() => {
return members.reduce(
diff --git a/src/ListBox.tsx b/src/ListBox.tsx
index 0ee2542a..b7ec7c72 100644
--- a/src/ListBox.tsx
+++ b/src/ListBox.tsx
@@ -36,15 +36,16 @@ export function ListBox({
listBoxRef,
...rest
}: ListBoxProps) {
- const ref = useRef();
- if (!listBoxRef) listBoxRef = ref;
+ const ref = useRef(null);
- const { listBoxProps } = useListBox(rest, state, listBoxRef);
+ const listRef = listBoxRef ?? ref;
+
+ const { listBoxProps } = useListBox(rest, state, listRef);
return (
{[...state.collection].map((item) => (
@@ -66,7 +67,7 @@ interface OptionProps {
}
function Option({ item, state, className }: OptionProps) {
- const ref = useRef();
+ const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key },
state,
@@ -83,7 +84,11 @@ function Option({ item, state, className }: OptionProps) {
const origPointerUp = optionProps.onPointerUp;
delete optionProps.onPointerUp;
optionProps.onClick = useCallback(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
(e) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
origPointerUp(e as unknown as PointerEvent);
},
[origPointerUp]
diff --git a/src/Menu.tsx b/src/Menu.tsx
index 1e158413..d7982df7 100644
--- a/src/Menu.tsx
+++ b/src/Menu.tsx
@@ -26,7 +26,7 @@ import styles from "./Menu.module.css";
interface MenuProps extends AriaMenuOptions {
className?: String;
- onClose?: () => void;
+ onClose: () => void;
onAction: (value: Key) => void;
label?: string;
}
@@ -39,7 +39,7 @@ export function Menu({
...rest
}: MenuProps) {
const state = useTreeState({ ...rest, selectionMode: "none" });
- const menuRef = useRef();
+ const menuRef = useRef(null);
const { menuProps } = useMenu(rest, state, menuRef);
return (
@@ -69,7 +69,7 @@ interface MenuItemProps {
}
function MenuItem({ item, state, onAction, onClose }: MenuItemProps) {
- const ref = useRef();
+ const ref = useRef(null);
const { menuItemProps } = useMenuItem(
{
key: item.key,
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 07217539..56db481e 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -55,7 +55,7 @@ export function Modal({
...rest
}: ModalProps) {
const { t } = useTranslation();
- const modalRef = useRef();
+ const modalRef = useRef(null);
const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose },
modalRef
@@ -63,7 +63,7 @@ export function Modal({
usePreventScroll();
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(rest, modalRef);
- const closeButtonRef = useRef();
+ const closeButtonRef = useRef(null);
const { buttonProps: closeButtonProps } = useButton(
{
onPress: () => onClose(),
diff --git a/src/SequenceDiagramViewerPage.tsx b/src/SequenceDiagramViewerPage.tsx
index 208352fe..9fb66f94 100644
--- a/src/SequenceDiagramViewerPage.tsx
+++ b/src/SequenceDiagramViewerPage.tsx
@@ -36,6 +36,9 @@ export function SequenceDiagramViewerPage() {
const [debugLog, setDebugLog] = useState();
const [selectedUserId, setSelectedUserId] = useState();
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
const onChangeDebugLog = useCallback((e) => {
if (e.target.files && e.target.files.length > 0) {
e.target.files[0].text().then((text: string) => {
@@ -55,7 +58,7 @@ export function SequenceDiagramViewerPage() {
onChange={onChangeDebugLog}
/>
- {debugLog && (
+ {debugLog && selectedUserId && (
(
const tooltipTriggerProps = { delay: 250, ...rest };
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
const triggerRef = useObjectRef(ref);
- const overlayRef = useRef();
+ const overlayRef = useRef(null);
const { triggerProps, tooltipProps } = useTooltipTrigger(
tooltipTriggerProps,
tooltipState,
diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx
index 5648edd9..9df3309d 100644
--- a/src/UserMenu.tsx
+++ b/src/UserMenu.tsx
@@ -36,7 +36,7 @@ interface UserMenuProps {
isAuthenticated: boolean;
isPasswordlessUser: boolean;
displayName: string;
- avatarUrl: string;
+ avatarUrl?: string;
onAction: (value: string) => void;
}
@@ -119,21 +119,24 @@ export function UserMenu({
)}
- {(props) => (
-
- )}
+ {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (props: any) => (
+
+ )
+ }
);
}
diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx
index 4702c4fe..6a83133e 100644
--- a/src/UserMenuContainer.tsx
+++ b/src/UserMenuContainer.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import { useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
-import { useClient } from "./ClientContext";
+import { useClientLegacy } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { SettingsModal } from "./settings/SettingsModal";
@@ -30,8 +30,7 @@ interface Props {
export function UserMenuContainer({ preventNavigation = false }: Props) {
const location = useLocation();
const history = useHistory();
- const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
- useClient();
+ const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
const { displayName, avatarUrl } = useProfile(client);
const { modalState, modalProps } = useModalTriggerState();
@@ -49,7 +48,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
modalState.open();
break;
case "logout":
- logout();
+ logout?.();
break;
case "login":
history.push("/login", { state: { from: location } });
@@ -59,19 +58,18 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
[history, location, logout, modalState]
);
+ const userName = client?.getUserIdLocalpart() ?? "";
return (
<>
- {modalState.isOpen && (
+ {modalState.isOpen && client && (
;
+ private identificationPromise?: Promise;
private readonly enabled: boolean = false;
private anonymity = Anonymity.Disabled;
private platformSuperProperties = {};
@@ -255,7 +255,9 @@ export class PosthogAnalytics {
} catch (e) {
// The above could fail due to network requests, but not essential to starting the application,
// so swallow it.
- logger.log("Unable to identify user for tracking" + e.toString());
+ logger.log(
+ "Unable to identify user for tracking" + (e as Error)?.toString()
+ );
}
if (analyticsID) {
this.posthog.identify(analyticsID);
@@ -366,7 +368,7 @@ export class PosthogAnalytics {
if (anonymity === Anonymity.Pseudonymous) {
this.setRegistrationType(
- window.matrixclient.isGuest() || window.isPasswordlessUser
+ window.matrixclient.isGuest() || window.passwordlessUser
? RegistrationType.Guest
: RegistrationType.Registered
);
diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx
index c5a090f1..f17f24fa 100644
--- a/src/auth/LoginPage.tsx
+++ b/src/auth/LoginPage.tsx
@@ -35,8 +35,8 @@ export const LoginPage: FC = () => {
const { setClient } = useClient();
const login = useInteractiveLogin();
const homeserver = Config.defaultHomeserverUrl(); // TODO: Make this configurable
- const usernameRef = useRef();
- const passwordRef = useRef();
+ const usernameRef = useRef(null);
+ const passwordRef = useRef(null);
const history = useHistory();
const location = useLocation();
const [loading, setLoading] = useState(false);
@@ -49,12 +49,27 @@ export const LoginPage: FC = () => {
e.preventDefault();
setLoading(true);
+ if (!homeserver || !usernameRef.current || !passwordRef.current) {
+ setError(Error("Login parameters are undefined"));
+ setLoading(false);
+ return;
+ }
+
login(homeserver, usernameRef.current.value, passwordRef.current.value)
.then(([client, session]) => {
- setClient(client, session);
+ if (!setClient) {
+ return;
+ }
- if (location.state && location.state.from) {
- history.push(location.state.from);
+ setClient({ client, session });
+
+ const locationState = location.state;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ if (locationState && locationState.from) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ history.push(locationState.from);
} else {
history.push("/");
}
diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx
index c96dea29..a486aa46 100644
--- a/src/auth/RegisterPage.tsx
+++ b/src/auth/RegisterPage.tsx
@@ -30,7 +30,7 @@ import { Trans, useTranslation } from "react-i18next";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
-import { useClient } from "../ClientContext";
+import { useClientLegacy } from "../ClientContext";
import { useInteractiveRegistration } from "./useInteractiveRegistration";
import styles from "./LoginPage.module.css";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
@@ -45,9 +45,10 @@ export const RegisterPage: FC = () => {
const { t } = useTranslation();
usePageTitle(t("Register"));
- const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
- useClient();
- const confirmPasswordRef = useRef();
+ const { loading, authenticated, passwordlessUser, client, setClient } =
+ useClientLegacy();
+
+ const confirmPasswordRef = useRef(null);
const history = useHistory();
const location = useLocation();
const [registering, setRegistering] = useState(false);
@@ -75,10 +76,15 @@ export const RegisterPage: FC = () => {
userName,
password,
userName,
- recaptchaResponse
+ recaptchaResponse,
+ passwordlessUser
);
- if (client && isPasswordlessUser) {
+ if (!client || !client.groupCallEventHandler || !setClient) {
+ return;
+ }
+
+ if (passwordlessUser) {
// Migrate the user's rooms
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
const roomId = groupCall.room.roomId;
@@ -86,7 +92,11 @@ export const RegisterPage: FC = () => {
try {
await newClient.joinRoom(roomId);
} catch (error) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
if (error.errcode === "M_LIMIT_EXCEEDED") {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
await sleep(error.data.retry_after_ms);
await newClient.joinRoom(roomId);
} else {
@@ -97,13 +107,17 @@ export const RegisterPage: FC = () => {
}
}
- setClient(newClient, session);
+ setClient({ client: newClient, session });
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
};
submit()
.then(() => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
if (location.state?.from) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
history.push(location.state?.from);
} else {
history.push("/");
@@ -119,7 +133,7 @@ export const RegisterPage: FC = () => {
register,
location,
history,
- isPasswordlessUser,
+ passwordlessUser,
reset,
execute,
client,
@@ -136,10 +150,10 @@ export const RegisterPage: FC = () => {
}, [password, passwordConfirmation, t]);
useEffect(() => {
- if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
+ if (!loading && authenticated && !passwordlessUser && !registering) {
history.push("/");
}
- }, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
+ }, [loading, history, authenticated, passwordlessUser, registering]);
if (loading) {
return ;
diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts
index 81b923f9..07dec12d 100644
--- a/src/auth/useInteractiveLogin.ts
+++ b/src/auth/useInteractiveLogin.ts
@@ -41,8 +41,10 @@ export const useInteractiveLogin = () =>
},
password,
}),
- stateUpdated: null,
- requestEmailToken: null,
+ stateUpdated: (...args) => {},
+ requestEmailToken: (...args): Promise<{ sid: string }> => {
+ return Promise.resolve({ sid: "" });
+ },
});
// XXX: This claims to return an IAuthData which contains none of these
diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts
index 2db33773..a4863671 100644
--- a/src/auth/useInteractiveRegistration.ts
+++ b/src/auth/useInteractiveRegistration.ts
@@ -23,28 +23,32 @@ import { Session } from "../ClientContext";
import { Config } from "../config/Config";
export const useInteractiveRegistration = (): {
- privacyPolicyUrl: string;
- recaptchaKey: string;
+ privacyPolicyUrl?: string;
+ recaptchaKey?: string;
register: (
username: string,
password: string,
displayName: string,
recaptchaResponse: string,
- passwordlessUser?: boolean
+ passwordlessUser: boolean
) => Promise<[MatrixClient, Session]>;
} => {
- const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState();
- const [recaptchaKey, setRecaptchaKey] = useState();
+ const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState(
+ undefined
+ );
+ const [recaptchaKey, setRecaptchaKey] = useState(
+ undefined
+ );
const authClient = useRef();
if (!authClient.current) {
authClient.current = createClient({
- baseUrl: Config.defaultHomeserverUrl(),
+ baseUrl: Config.defaultHomeserverUrl()!,
});
}
useEffect(() => {
- authClient.current.registerRequest({}).catch((error) => {
+ authClient.current!.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
);
@@ -58,12 +62,12 @@ export const useInteractiveRegistration = (): {
password: string,
displayName: string,
recaptchaResponse: string,
- passwordlessUser?: boolean
+ passwordlessUser: boolean
): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({
- matrixClient: authClient.current,
+ matrixClient: authClient.current!,
doRequest: (auth) =>
- authClient.current.registerRequest({
+ authClient.current!.registerRequest({
username,
password,
auth: auth || undefined,
@@ -84,7 +88,9 @@ export const useInteractiveRegistration = (): {
});
}
},
- requestEmailToken: null,
+ requestEmailToken: (...args) => {
+ return Promise.resolve({ sid: "dummy" });
+ },
});
// XXX: This claims to return an IAuthData which contains none of these
@@ -95,7 +101,7 @@ export const useInteractiveRegistration = (): {
const client = await initClient(
{
- baseUrl: Config.defaultHomeserverUrl(),
+ baseUrl: Config.defaultHomeserverUrl()!,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
@@ -117,7 +123,7 @@ export const useInteractiveRegistration = (): {
session.tempPassword = password;
}
- const user = client.getUser(client.getUserId());
+ const user = client.getUser(client.getUserId()!)!;
user.setRawDisplayName(displayName);
user.setDisplayName(displayName);
diff --git a/src/auth/useRecaptcha.ts b/src/auth/useRecaptcha.ts
index ef3c7c26..647370da 100644
--- a/src/auth/useRecaptcha.ts
+++ b/src/auth/useRecaptcha.ts
@@ -34,7 +34,7 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void;
}
-export const useRecaptcha = (sitekey: string) => {
+export const useRecaptcha = (sitekey?: string) => {
const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef();
@@ -68,9 +68,9 @@ export const useRecaptcha = (sitekey: string) => {
}
}, [recaptchaId, sitekey]);
- const execute = useCallback(() => {
+ const execute = useCallback((): Promise => {
if (!sitekey) {
- return Promise.resolve(null);
+ return Promise.resolve("");
}
if (!window.grecaptcha) {
diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts
index 94d40cd2..983789ba 100644
--- a/src/auth/useRegisterPasswordlessUser.ts
+++ b/src/auth/useRegisterPasswordlessUser.ts
@@ -23,9 +23,9 @@ import { generateRandomName } from "../auth/generateRandomName";
import { useRecaptcha } from "../auth/useRecaptcha";
interface UseRegisterPasswordlessUserType {
- privacyPolicyUrl: string;
+ privacyPolicyUrl?: string;
registerPasswordlessUser: (displayName: string) => Promise;
- recaptchaId: string;
+ recaptchaId?: string;
}
export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
@@ -36,6 +36,10 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
const registerPasswordlessUser = useCallback(
async (displayName: string) => {
+ if (!setClient) {
+ throw new Error("No client context");
+ }
+
try {
const recaptchaResponse = await execute();
const userName = generateRandomName();
@@ -46,7 +50,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
recaptchaResponse,
true
);
- setClient(client, session);
+ setClient({ client, session });
} catch (e) {
reset();
throw e;
diff --git a/src/button/LinkButton.tsx b/src/button/LinkButton.tsx
index 3f3b1d55..3392935d 100644
--- a/src/button/LinkButton.tsx
+++ b/src/button/LinkButton.tsx
@@ -46,7 +46,7 @@ export function LinkButton({
= ({ callType, setCallType }) => {
const { t } = useTranslation();
+ const onAction = (key: React.Key) => {
+ setCallType(key.toString() as CallType);
+ };
+
+ const onClose = () => {};
+
return (
{(props: JSX.IntrinsicAttributes) => (
-