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/package.json b/package.json index af5e240b..a5f5e979 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-react": "^4.0.1", @@ -69,7 +71,6 @@ "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", 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..1e1b8dbe 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -99,10 +99,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 resolveAvatarSrc(client, src, sizePx); + }, [client, src, sizePx]); const backgroundColor = useMemo(() => { const index = hashStringToArrIndex( diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index ba1b217b..a440e46a 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -20,7 +20,6 @@ import { useEffect, useState, createContext, - useMemo, useContext, useRef, } from "react"; @@ -31,9 +30,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 +46,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 +188,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 +249,146 @@ 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, setState] = useState(undefined); + useEffect(() => { + if (alreadyOpenedErr) { + setState({ state: "error", error: alreadyOpenedErr }); + return; + } + + let authenticated = undefined; + if (initClientState) { + authenticated = { + client: initClientState.client, + isPasswordlessUser: initClientState.passwordlessUser, + changePassword, + logout, + }; + } + + setState({ 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..92e297e4 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -119,7 +119,7 @@ export function UserMenu({ )} - {(props) => ( + {(props: any) => ( {items.map(({ key, icon: Icon, label, dataTestid }) => ( diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 4702c4fe..d1af87da 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,9 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { modalState.open(); break; case "logout": - logout(); + if (logout) { + logout(); + } break; case "login": history.push("/login", { state: { from: location } }); @@ -59,19 +60,22 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { [history, location, logout, modalState] ); + const userName = client?.getUserIdLocalpart() ?? ""; return ( <> - - {modalState.isOpen && ( + {avatarUrl && ( + + )} + {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..f519294c 100644 --- a/src/button/LinkButton.tsx +++ b/src/button/LinkButton.tsx @@ -46,7 +46,7 @@ export function LinkButton({ = ({ callType, setCallType }) => { {(props: JSX.IntrinsicAttributes) => ( - + { + const callType = key.toString(); + setCallType(callType as CallType); + }} + onClose={() => {}} + > {t("Video call")} diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index 4c5522df..018e0512 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -16,7 +16,7 @@ limitations under the License. import { useTranslation } from "react-i18next"; -import { useClient } from "../ClientContext"; +import { useClientState } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; import { UnauthenticatedView } from "./UnauthenticatedView"; import { RegisteredView } from "./RegisteredView"; @@ -26,16 +26,18 @@ export function HomePage() { const { t } = useTranslation(); usePageTitle(t("Home")); - const { isAuthenticated, isPasswordlessUser, loading, error, client } = - useClient(); + const clientState = useClientState(); - if (loading) { + if (!clientState) { return ; - } else if (error) { - return ; + } else if (clientState.state === "error") { + return ; } else { - return isAuthenticated ? ( - + return clientState.authenticated ? ( + ) : ( ); diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index a0ee8904..0ae7e98d 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -83,11 +83,17 @@ export const UnauthenticatedView: FC = () => { try { [roomIdOrAlias] = await createRoom(client, roomName, ptt); } catch (error) { + if (!setClient) { + throw error; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore if (error.errcode === "M_ROOM_IN_USE") { setOnFinished(() => { - setClient(client, session); + setClient({ client, session }); const aliasLocalpart = roomAliasLocalpartFromRoomName(roomName); - const [, serverName] = client.getUserId().split(":"); + const [, serverName] = client.getUserId()!.split(":"); history.push(`/room/#${aliasLocalpart}:${serverName}`); }); @@ -100,7 +106,11 @@ export const UnauthenticatedView: FC = () => { } // Only consider the registration successful if we managed to create the room, too - setClient(client, session); + if (!setClient) { + throw new Error("setClient is undefined"); + } + + setClient({ client, session }); history.push(`/room/${roomIdOrAlias}`); } @@ -204,7 +214,7 @@ export const UnauthenticatedView: FC = () => { - {modalState.isOpen && ( + {modalState.isOpen && onFinished && ( )} diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index fcbe67ff..463a1bb3 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -42,7 +42,7 @@ function getLastTs(client: MatrixClient, r: Room) { return ts; } - const myUserId = client.getUserId(); + const myUserId = client.getUserId()!; if (r.getMyMembership() !== "join") { const membershipEvent = r.currentState.getStateEvents( @@ -79,25 +79,30 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] { } export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { - const [rooms, setRooms] = useState([]); + const [rooms, setRooms] = useState([]); useEffect(() => { function updateRooms() { + if (!client.groupCallEventHandler) { + return; + } + const groupCalls = client.groupCallEventHandler.groupCalls.values(); const rooms = Array.from(groupCalls).map((groupCall) => groupCall.room); const sortedRooms = sortRooms(client, rooms); const items = sortedRooms.map((room) => { - const groupCall = client.getGroupCallForRoom(room.roomId); + const groupCall = client.getGroupCallForRoom(room.roomId)!; return { roomId: room.getCanonicalAlias() || room.roomId, roomName: room.name, - avatarUrl: room.getMxcAvatarUrl(), + avatarUrl: room.getMxcAvatarUrl()!, room, groupCall, - participants: [...groupCall.participants], + participants: [...groupCall!.participants.keys()], }; }); + setRooms(items); } diff --git a/src/initializer.tsx b/src/initializer.tsx index df5c8af6..f1f37966 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -229,5 +229,6 @@ export class Initializer { resolve(); } } - private initPromise: Promise | null; + + private initPromise?: Promise; } diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index 9280fad4..484f440e 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -15,10 +15,14 @@ limitations under the License. */ import { useObjectRef } from "@react-aria/utils"; -import { AllHTMLAttributes, ChangeEvent, useEffect } from "react"; -import { useCallback } from "react"; -import { useState } from "react"; -import { forwardRef } from "react"; +import { + AllHTMLAttributes, + useEffect, + useCallback, + useState, + forwardRef, + ChangeEvent, +} from "react"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; @@ -43,7 +47,7 @@ export const AvatarInputField = forwardRef( const { t } = useTranslation(); const [removed, setRemoved] = useState(false); - const [objUrl, setObjUrl] = useState(null); + const [objUrl, setObjUrl] = useState(undefined); const fileInputRef = useObjectRef(ref); @@ -52,11 +56,11 @@ export const AvatarInputField = forwardRef( const onChange = (e: Event) => { const inputEvent = e as unknown as ChangeEvent; - if (inputEvent.target.files.length > 0) { + if (inputEvent.target.files && inputEvent.target.files.length > 0) { setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); setRemoved(false); } else { - setObjUrl(null); + setObjUrl(undefined); } }; @@ -77,7 +81,7 @@ export const AvatarInputField = forwardRef(
    void; + onChange?: (event: ChangeEvent) => void; } export const InputField = forwardRef< @@ -119,6 +119,8 @@ export const InputField = forwardRef< > {prefix && {prefix}} {type === "textarea" ? ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore