diff --git a/.eslintrc.cjs b/.eslintrc.cjs index ac9f913f..7f0dd576 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,13 +1,31 @@ +const COPYRIGHT_HEADER = `/* +Copyright %%CURRENT_YEAR%% New Vector Ltd + +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. +*/ + +`; + module.exports = { plugins: ["matrix-org"], extends: [ - "prettier", "plugin:matrix-org/react", "plugin:matrix-org/a11y", "plugin:matrix-org/typescript", + "prettier", ], parserOptions: { - ecmaVersion: 2018, + ecmaVersion: "latest", sourceType: "module", project: ["./tsconfig.json"], }, @@ -15,29 +33,13 @@ module.exports = { browser: true, node: true, }, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, rules: { - "jsx-a11y/media-has-caption": ["off"], + "matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER], + "jsx-a11y/media-has-caption": "off", + "deprecate/import": "off", // Disabled because it crashes the linter + // We should use the js-sdk logger, never console directly. + "no-console": ["error"], }, - overrides: [ - { - files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"], - extends: [ - "plugin:matrix-org/typescript", - "plugin:matrix-org/react", - "prettier", - ], - rules: { - // We're aiming to convert this code to strict mode - "@typescript-eslint/no-non-null-assertion": "off", - // We should use the js-sdk logger, never console directly. - "no-console": ["error"], - }, - }, - ], settings: { react: { version: "detect", diff --git a/.storybook/main.js b/.storybook/main.js index 682489d9..9d03e947 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -14,7 +14,7 @@ module.exports = { Array.isArray(item) && item.length > 0 && item[0].name === "vite-plugin-mdx" - ) + ), ); config.plugins.push(svgrPlugin()); config.resolve = config.resolve || {}; diff --git a/package.json b/package.json index e281939a..43414002 100644 --- a/package.json +++ b/package.json @@ -105,19 +105,22 @@ "eslint": "^8.14.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", + "eslint-plugin-deprecate": "^0.8.2", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-matrix-org": "^0.4.0", + "eslint-plugin-matrix-org": "^1.2.1", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-unicorn": "^48.0.1", "i18next-parser": "^8.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.2.2", "jest-environment-jsdom": "^29.3.1", "jest-mock": "^29.5.0", - "prettier": "^2.6.2", + "prettier": "^3.0.0", "sass": "^1.42.1", "typescript": "^5.1.6", + "typescript-eslint-language-service": "^5.0.5", "vite": "^4.2.0", "vite-plugin-html-template": "^1.1.0", "vite-plugin-svgr": "^4.0.0" diff --git a/public/index.html b/public/index.html index 25987410..93dd8973 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index a55793f7..c3b31d36 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -114,5 +114,10 @@ "Call not found": "Nie znaleziono połączenia", "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.", "You": "Ty", - "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117" + "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117", + "Invite": "Zaproś", + "Link copied to clipboard": "Skopiowano link do schowka", + "Participants": "Uczestnicy", + "Copy link": "Kopiuj link", + "Invite to this call": "Zaproś do połączenia" } diff --git a/src/App.tsx b/src/App.tsx index 7bfd5a0c..4d6fa6a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Suspense, useEffect, useState } from "react"; +import { FC, Suspense, useEffect, useState } from "react"; import { BrowserRouter as Router, Switch, @@ -41,7 +41,7 @@ interface BackgroundProviderProps { children: JSX.Element; } -const BackgroundProvider = ({ children }: BackgroundProviderProps) => { +const BackgroundProvider: FC = ({ children }) => { const { pathname } = useLocation(); useEffect(() => { @@ -61,7 +61,7 @@ interface AppProps { history: History; } -export default function App({ history }: AppProps) { +export const App: FC = ({ history }) => { const [loaded, setLoaded] = useState(false); useEffect(() => { @@ -109,4 +109,4 @@ export default function App({ history }: AppProps) { ); -} +}; diff --git a/src/Avatar.tsx b/src/Avatar.tsx index e23bd909..06583445 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -58,7 +58,7 @@ export const Avatar: FC = ({ Object.values(Size).includes(size as Size) ? sizes.get(size as Size) : (size as number), - [size] + [size], ); const resolvedSrc = useMemo(() => { diff --git a/src/Banner.tsx b/src/Banner.tsx index fcc68a3b..87ce8a96 100644 --- a/src/Banner.tsx +++ b/src/Banner.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode } from "react"; +import { FC, ReactNode } from "react"; import styles from "./Banner.module.css"; @@ -22,6 +22,6 @@ interface Props { children: ReactNode; } -export const Banner = ({ children }: Props) => { +export const Banner: FC = ({ children }) => { return
{children}
; }; diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index f7ceea9f..9e47a624 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -82,7 +82,8 @@ export type SetClientParams = { const ClientContext = createContext(undefined); -export const useClientState = () => useContext(ClientContext); +export const useClientState = (): ClientState | undefined => + useContext(ClientContext); export function useClient(): { client?: MatrixClient; @@ -189,7 +190,7 @@ export const ClientProvider: FC = ({ children }) => { user: session.user_id, password: session.tempPassword, }, - password + password, ); saveSession({ ...session, passwordlessUser: false }); @@ -199,7 +200,7 @@ export const ClientProvider: FC = ({ children }) => { passwordlessUser: false, }); }, - [initClientState?.client] + [initClientState?.client], ); const setClient = useCallback( @@ -221,7 +222,7 @@ export const ClientProvider: FC = ({ children }) => { setInitClientState(null); } }, - [initClientState?.client] + [initClientState?.client], ); const logout = useCallback(async () => { @@ -249,7 +250,7 @@ export const ClientProvider: FC = ({ children }) => { }, []); const [alreadyOpenedErr, setAlreadyOpenedErr] = useState( - undefined + undefined, ); useEventTarget( loadChannel, @@ -257,9 +258,9 @@ export const ClientProvider: FC = ({ children }) => { useCallback(() => { initClientState?.client.stopClient(); setAlreadyOpenedErr( - translatedError("This application has been opened in another tab.", t) + translatedError("This application has been opened in another tab.", t), ); - }, [initClientState?.client, setAlreadyOpenedErr, t]) + }, [initClientState?.client, setAlreadyOpenedErr, t]), ); const [isDisconnected, setIsDisconnected] = useState(false); @@ -300,7 +301,7 @@ export const ClientProvider: FC = ({ children }) => { (state: SyncState, _old: SyncState | null, data?: ISyncStateData) => { setIsDisconnected(clientIsDisconnected(state, data)); }, - [] + [], ); useEffect(() => { @@ -386,7 +387,7 @@ async function loadClient(): Promise { logger.warn( "The previous session was lost, and we couldn't log it out, " + err + - "either" + "either", ); } } @@ -408,8 +409,8 @@ export interface Session { tempPassword?: string; } -const clearSession = () => localStorage.removeItem("matrix-auth-store"); -const saveSession = (s: Session) => +const clearSession = (): void => localStorage.removeItem("matrix-auth-store"); +const saveSession = (s: Session): void => localStorage.setItem("matrix-auth-store", JSON.stringify(s)); const loadSession = (): Session | undefined => { const data = localStorage.getItem("matrix-auth-store"); @@ -422,5 +423,6 @@ const loadSession = (): Session | undefined => { const clientIsDisconnected = ( syncState: SyncState, - syncData?: ISyncStateData -) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError"; + syncData?: ISyncStateData, +): boolean => + syncState === "ERROR" && syncData?.error?.name === "ConnectionError"; diff --git a/src/DisconnectedBanner.tsx b/src/DisconnectedBanner.tsx index 6ec5d5ac..06369cc5 100644 --- a/src/DisconnectedBanner.tsx +++ b/src/DisconnectedBanner.tsx @@ -15,22 +15,22 @@ limitations under the License. */ import classNames from "classnames"; -import { HTMLAttributes, ReactNode } from "react"; +import { FC, HTMLAttributes, ReactNode } from "react"; import { useTranslation } from "react-i18next"; import styles from "./DisconnectedBanner.module.css"; import { ValidClientState, useClientState } from "./ClientContext"; -interface DisconnectedBannerProps extends HTMLAttributes { +interface Props extends HTMLAttributes { children?: ReactNode; className?: string; } -export function DisconnectedBanner({ +export const DisconnectedBanner: FC = ({ children, className, ...rest -}: DisconnectedBannerProps) { +}) => { const { t } = useTranslation(); const clientState = useClientState(); let shouldShowBanner = false; @@ -50,4 +50,4 @@ export function DisconnectedBanner({ )} ); -} +}; diff --git a/src/E2EEBanner.tsx b/src/E2EEBanner.tsx index c4223b2d..8ce30482 100644 --- a/src/E2EEBanner.tsx +++ b/src/E2EEBanner.tsx @@ -15,13 +15,14 @@ limitations under the License. */ import { Trans } from "react-i18next"; +import { FC } from "react"; import { Banner } from "./Banner"; import styles from "./E2EEBanner.module.css"; import LockOffIcon from "./icons/LockOff.svg?react"; import { useEnableE2EE } from "./settings/useSetting"; -export const E2EEBanner = () => { +export const E2EEBanner: FC = () => { const [e2eeEnabled] = useEnableE2EE(); if (e2eeEnabled) return null; diff --git a/src/FullScreenView.tsx b/src/FullScreenView.tsx index 5ce2694c..04791a73 100644 --- a/src/FullScreenView.tsx +++ b/src/FullScreenView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode, useCallback, useEffect } from "react"; +import { FC, ReactNode, useCallback, useEffect } from "react"; import { useLocation } from "react-router-dom"; import classNames from "classnames"; import { Trans, useTranslation } from "react-i18next"; @@ -33,7 +33,10 @@ interface FullScreenViewProps { children: ReactNode; } -export function FullScreenView({ className, children }: FullScreenViewProps) { +export const FullScreenView: FC = ({ + className, + children, +}) => { return (
@@ -47,13 +50,13 @@ export function FullScreenView({ className, children }: FullScreenViewProps) {
); -} +}; interface ErrorViewProps { error: Error; } -export function ErrorView({ error }: ErrorViewProps) { +export const ErrorView: FC = ({ error }) => { const location = useLocation(); const { t } = useTranslation(); @@ -96,9 +99,9 @@ export function ErrorView({ error }: ErrorViewProps) { )} ); -} +}; -export function CrashView() { +export const CrashView: FC = () => { const { t } = useTranslation(); const onReload = useCallback(() => { @@ -127,9 +130,9 @@ export function CrashView() { ); -} +}; -export function LoadingView() { +export const LoadingView: FC = () => { const { t } = useTranslation(); return ( @@ -137,4 +140,4 @@ export function LoadingView() {

{t("Loading…")}

); -} +}; diff --git a/src/Glass.tsx b/src/Glass.tsx index 5f01d47a..be59172d 100644 --- a/src/Glass.tsx +++ b/src/Glass.tsx @@ -48,5 +48,5 @@ export const Glass = forwardRef( > {Children.only(children)} - ) + ), ); diff --git a/src/Header.tsx b/src/Header.tsx index 848319f2..bb170190 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -32,13 +32,13 @@ interface HeaderProps extends HTMLAttributes { className?: string; } -export function Header({ children, className, ...rest }: HeaderProps) { +export const Header: FC = ({ children, className, ...rest }) => { return (
{children}
); -} +}; interface LeftNavProps extends HTMLAttributes { children: ReactNode; @@ -46,26 +46,26 @@ interface LeftNavProps extends HTMLAttributes { hideMobile?: boolean; } -export function LeftNav({ +export const LeftNav: FC = ({ children, className, hideMobile, ...rest -}: LeftNavProps) { +}) => { return (
{children}
); -} +}; interface RightNavProps extends HTMLAttributes { children?: ReactNode; @@ -73,32 +73,32 @@ interface RightNavProps extends HTMLAttributes { hideMobile?: boolean; } -export function RightNav({ +export const RightNav: FC = ({ children, className, hideMobile, ...rest -}: RightNavProps) { +}) => { return (
{children}
); -} +}; interface HeaderLogoProps { className?: string; } -export function HeaderLogo({ className }: HeaderLogoProps) { +export const HeaderLogo: FC = ({ className }) => { const { t } = useTranslation(); return ( @@ -110,7 +110,7 @@ export function HeaderLogo({ className }: HeaderLogoProps) { ); -} +}; interface RoomHeaderInfoProps { id: string; diff --git a/src/LazyEventEmitter.ts b/src/LazyEventEmitter.ts index 121eebf6..6b1730f4 100644 --- a/src/LazyEventEmitter.ts +++ b/src/LazyEventEmitter.ts @@ -63,7 +63,7 @@ export class LazyEventEmitter extends EventEmitter { public addListener( type: string | symbol, // eslint-disable-next-line @typescript-eslint/no-explicit-any - listener: (...args: any[]) => void + listener: (...args: any[]) => void, ): this { return this.on(type, listener); } diff --git a/src/ListBox.tsx b/src/ListBox.tsx index b7ec7c72..ea6e686b 100644 --- a/src/ListBox.tsx +++ b/src/ListBox.tsx @@ -14,7 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MutableRefObject, PointerEvent, useCallback, useRef } from "react"; +import { + MutableRefObject, + PointerEvent, + ReactNode, + useCallback, + useRef, +} from "react"; import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox"; import { ListState } from "@react-stately/list"; import { Node } from "@react-types/shared"; @@ -35,7 +41,7 @@ export function ListBox({ className, listBoxRef, ...rest -}: ListBoxProps) { +}: ListBoxProps): ReactNode { const ref = useRef(null); const listRef = listBoxRef ?? ref; @@ -66,12 +72,12 @@ interface OptionProps { item: Node; } -function Option({ item, state, className }: OptionProps) { +function Option({ item, state, className }: OptionProps): ReactNode { const ref = useRef(null); const { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, - ref + ref, ); // Hack: remove the onPointerUp event handler and re-wire it to @@ -91,7 +97,7 @@ function Option({ item, state, className }: OptionProps) { // @ts-ignore origPointerUp(e as unknown as PointerEvent); }, - [origPointerUp] + [origPointerUp], ); return ( diff --git a/src/Menu.tsx b/src/Menu.tsx index c1e8e40b..cfc0bc53 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Key, useRef, useState } from "react"; +import { Key, ReactNode, useRef, useState } from "react"; import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu"; import { TreeState, useTreeState } from "@react-stately/tree"; import { mergeProps } from "@react-aria/utils"; @@ -37,7 +37,7 @@ export function Menu({ onClose, label, ...rest -}: MenuProps) { +}: MenuProps): ReactNode { const state = useTreeState({ ...rest, selectionMode: "none" }); const menuRef = useRef(null); const { menuProps } = useMenu(rest, state, menuRef); @@ -68,7 +68,12 @@ interface MenuItemProps { onClose: () => void; } -function MenuItem({ item, state, onAction, onClose }: MenuItemProps) { +function MenuItem({ + item, + state, + onAction, + onClose, +}: MenuItemProps): ReactNode { const ref = useRef(null); const { menuItemProps } = useMenuItem( { @@ -77,7 +82,7 @@ function MenuItem({ item, state, onAction, onClose }: MenuItemProps) { onClose, }, state, - ref + ref, ); const [isFocused, setFocused] = useState(false); diff --git a/src/Modal.tsx b/src/Modal.tsx index 83e43eb2..5a6aea94 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode, useCallback } from "react"; +import { FC, ReactNode, useCallback } from "react"; import { AriaDialogProps } from "@react-types/dialog"; import { useTranslation } from "react-i18next"; import { @@ -37,7 +37,7 @@ import { useMediaQuery } from "./useMediaQuery"; import { Glass } from "./Glass"; // TODO: Support tabs -export interface ModalProps extends AriaDialogProps { +export interface Props extends AriaDialogProps { title: string; children: ReactNode; className?: string; @@ -59,14 +59,14 @@ export interface ModalProps extends AriaDialogProps { * A modal, taking the form of a drawer / bottom sheet on touchscreen devices, * and a dialog box on desktop. */ -export function Modal({ +export const Modal: FC = ({ title, children, className, open, onDismiss, ...rest -}: ModalProps) { +}) => { const { t } = useTranslation(); // Empirically, Chrome on Android can end up not matching (hover: none), but // still matching (pointer: coarse) :/ @@ -75,7 +75,7 @@ export function Modal({ (open: boolean) => { if (!open) onDismiss?.(); }, - [onDismiss] + [onDismiss], ); if (touchscreen) { @@ -92,7 +92,7 @@ export function Modal({ className, overlayStyles.overlay, styles.modal, - styles.drawer + styles.drawer, )} {...rest} > @@ -124,7 +124,7 @@ export function Modal({ overlayStyles.overlay, overlayStyles.animate, styles.modal, - styles.dialog + styles.dialog, )} >
@@ -152,4 +152,4 @@ export function Modal({ ); } -} +}; diff --git a/src/Toast.tsx b/src/Toast.tsx index de532cde..ca3e2fab 100644 --- a/src/Toast.tsx +++ b/src/Toast.tsx @@ -70,7 +70,7 @@ export const Toast: FC = ({ (open: boolean) => { if (!open) onDismiss(); }, - [onDismiss] + [onDismiss], ); useEffect(() => { @@ -91,7 +91,7 @@ export const Toast: FC = ({ className={classNames( overlayStyles.overlay, overlayStyles.animate, - styles.toast + styles.toast, )} > diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index da2d52ea..b90c3f86 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -43,7 +43,7 @@ interface TooltipProps { const Tooltip = forwardRef( ( { state, className, children, ...rest }: TooltipProps, - ref: ForwardedRef + ref: ForwardedRef, ) => { const { tooltipProps } = useTooltip(rest, state); @@ -56,7 +56,7 @@ const Tooltip = forwardRef( {children}
); - } + }, ); interface TooltipTriggerProps { @@ -69,7 +69,7 @@ interface TooltipTriggerProps { export const TooltipTrigger = forwardRef( ( { children, placement, tooltip, ...rest }: TooltipTriggerProps, - ref: ForwardedRef + ref: ForwardedRef, ) => { const tooltipTriggerProps = { delay: 250, ...rest }; const tooltipState = useTooltipTriggerState(tooltipTriggerProps); @@ -78,7 +78,7 @@ export const TooltipTrigger = forwardRef( const { triggerProps, tooltipProps } = useTooltipTrigger( tooltipTriggerProps, tooltipState, - triggerRef + triggerRef, ); const { overlayProps } = useOverlayPosition({ @@ -94,7 +94,7 @@ export const TooltipTrigger = forwardRef( ( children.props, - rest + rest, )} /> {tooltipState.isOpen && ( @@ -110,5 +110,5 @@ export const TooltipTrigger = forwardRef( )} ); - } + }, ); diff --git a/src/TranslatedError.ts b/src/TranslatedError.ts index 62960f04..6c7f5dad 100644 --- a/src/TranslatedError.ts +++ b/src/TranslatedError.ts @@ -37,5 +37,7 @@ class TranslatedErrorImpl extends TranslatedError {} // i18next-parser can't detect calls to a constructor, so we expose a bare // function instead -export const translatedError = (messageKey: string, t: typeof i18n.t) => - new TranslatedErrorImpl(messageKey, t); +export const translatedError = ( + messageKey: string, + t: typeof i18n.t, +): TranslatedError => new TranslatedErrorImpl(messageKey, t); diff --git a/src/UrlParams.ts b/src/UrlParams.ts index f1d9210e..498d7cc5 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -119,17 +119,17 @@ interface UrlParams { // file. export function editFragmentQuery( hash: string, - edit: (params: URLSearchParams) => URLSearchParams + edit: (params: URLSearchParams) => URLSearchParams, ): string { const fragmentQueryStart = hash.indexOf("?"); const fragmentParams = edit( new URLSearchParams( - fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) - ) + fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart), + ), ); return `${hash.substring( 0, - fragmentQueryStart + fragmentQueryStart, )}?${fragmentParams.toString()}`; } @@ -137,30 +137,30 @@ class ParamParser { private fragmentParams: URLSearchParams; private queryParams: URLSearchParams; - constructor(search: string, hash: string) { + public constructor(search: string, hash: string) { this.queryParams = new URLSearchParams(search); const fragmentQueryStart = hash.indexOf("?"); this.fragmentParams = new URLSearchParams( - fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) + fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart), ); } // Normally, URL params should be encoded in the fragment so as to avoid // leaking them to the server. However, we also check the normal query // string for backwards compatibility with versions that only used that. - getParam(name: string): string | null { + public getParam(name: string): string | null { return this.fragmentParams.get(name) ?? this.queryParams.get(name); } - getAllParams(name: string): string[] { + public getAllParams(name: string): string[] { return [ ...this.fragmentParams.getAll(name), ...this.queryParams.getAll(name), ]; } - getFlagParam(name: string, defaultValue = false): boolean { + public getFlagParam(name: string, defaultValue = false): boolean { const param = this.getParam(name); return param === null ? defaultValue : param !== "false"; } @@ -174,7 +174,7 @@ class ParamParser { */ export const getUrlParams = ( search = window.location.search, - hash = window.location.hash + hash = window.location.hash, ): UrlParams => { const parser = new ParamParser(search, hash); @@ -221,7 +221,7 @@ export const useUrlParams = (): UrlParams => { export function getRoomIdentifierFromUrl( pathname: string, search: string, - hash: string + hash: string, ): RoomIdentifier { let roomAlias: string | null = null; pathname = pathname.substring(1); // Strip the "/" @@ -281,6 +281,6 @@ export const useRoomIdentifier = (): RoomIdentifier => { const { pathname, search, hash } = useLocation(); return useMemo( () => getRoomIdentifierFromUrl(pathname, search, hash), - [pathname, search, hash] + [pathname, search, hash], ); }; diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 42f31951..58f61390 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useMemo } from "react"; +import { FC, ReactNode, useCallback, useMemo } from "react"; import { Item } from "@react-stately/collections"; import { useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -31,7 +31,7 @@ import LogoutIcon from "./icons/Logout.svg?react"; import { Body } from "./typography/Typography"; import styles from "./UserMenu.module.css"; -interface UserMenuProps { +interface Props { preventNavigation: boolean; isAuthenticated: boolean; isPasswordlessUser: boolean; @@ -41,7 +41,7 @@ interface UserMenuProps { onAction: (value: string) => void; } -export function UserMenu({ +export const UserMenu: FC = ({ preventNavigation, isAuthenticated, isPasswordlessUser, @@ -49,7 +49,7 @@ export function UserMenu({ displayName, avatarUrl, onAction, -}: UserMenuProps) { +}) => { const { t } = useTranslation(); const location = useLocation(); @@ -123,7 +123,7 @@ export function UserMenu({ { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props: any) => ( + (props: any): ReactNode => ( {items.map(({ key, icon: Icon, label, dataTestid }) => ( @@ -141,4 +141,4 @@ export function UserMenu({ } ); -} +}; diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 359f75f6..c0155dcb 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useState } from "react"; +import { FC, useCallback, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import { useClientLegacy } from "./ClientContext"; @@ -26,7 +26,7 @@ interface Props { preventNavigation?: boolean; } -export function UserMenuContainer({ preventNavigation = false }: Props) { +export const UserMenuContainer: FC = ({ preventNavigation = false }) => { const location = useLocation(); const history = useHistory(); const { client, logout, authenticated, passwordlessUser } = useClientLegacy(); @@ -34,7 +34,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { const [settingsModalOpen, setSettingsModalOpen] = useState(false); const onDismissSettingsModal = useCallback( () => setSettingsModalOpen(false), - [setSettingsModalOpen] + [setSettingsModalOpen], ); const [defaultSettingsTab, setDefaultSettingsTab] = useState(); @@ -58,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { break; } }, - [history, location, logout, setSettingsModalOpen] + [history, location, logout, setSettingsModalOpen], ); const userName = client?.getUserIdLocalpart() ?? ""; @@ -83,4 +83,4 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { )} ); -} +}; diff --git a/src/analytics/AnalyticsNotice.tsx b/src/analytics/AnalyticsNotice.tsx index 544de61a..7b1d3fb5 100644 --- a/src/analytics/AnalyticsNotice.tsx +++ b/src/analytics/AnalyticsNotice.tsx @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 { FC } from "react"; import { Trans } from "react-i18next"; diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 9d4f3f5c..c1778f8b 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -117,7 +117,7 @@ export class PosthogAnalytics { return this.internalInstance; } - constructor(private readonly posthog: PostHog) { + private constructor(private readonly posthog: PostHog) { const posthogConfig: PosthogSettings = { project_api_key: Config.get().posthog?.api_key, api_host: Config.get().posthog?.api_host, @@ -146,7 +146,7 @@ export class PosthogAnalytics { this.enabled = true; } else { logger.info( - "Posthog is not enabled because there is no api key or no host given in the config" + "Posthog is not enabled because there is no api key or no host given in the config", ); this.enabled = false; } @@ -157,7 +157,7 @@ export class PosthogAnalytics { private sanitizeProperties = ( properties: Properties, - _eventName: string + _eventName: string, ): Properties => { // Callback from posthog to sanitize properties before sending them to the server. // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. @@ -183,7 +183,7 @@ export class PosthogAnalytics { return properties; }; - private registerSuperProperties(properties: Properties) { + private registerSuperProperties(properties: Properties): void { if (this.enabled) { this.posthog.register(properties); } @@ -201,8 +201,8 @@ export class PosthogAnalytics { private capture( eventName: string, properties: Properties, - options?: CaptureOptions - ) { + options?: CaptureOptions, + ): void { if (!this.enabled) { return; } @@ -213,7 +213,7 @@ export class PosthogAnalytics { return this.enabled; } - setAnonymity(anonymity: Anonymity): void { + private setAnonymity(anonymity: Anonymity): void { // Update this.anonymity. // To update the anonymity typically you want to call updateAnonymityFromSettings // to ensure this value is in step with the user's settings. @@ -236,7 +236,9 @@ export class PosthogAnalytics { .join(""); } - private async identifyUser(analyticsIdGenerator: () => string) { + private async identifyUser( + analyticsIdGenerator: () => string, + ): Promise { if (this.anonymity == Anonymity.Pseudonymous && this.enabled) { // Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows // different devices to send the same ID. @@ -258,27 +260,27 @@ export class PosthogAnalytics { // 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 as Error)?.toString() + "Unable to identify user for tracking" + (e as Error)?.toString(), ); } if (analyticsID) { this.posthog.identify(analyticsID); } else { logger.info( - "No analyticsID is availble. Should not try to setup posthog" + "No analyticsID is availble. Should not try to setup posthog", ); } } } - async getAnalyticsId() { + private async getAnalyticsId(): Promise { const client: MatrixClient = window.matrixclient; let accountAnalyticsId; if (widget) { accountAnalyticsId = getUrlParams().analyticsID; } else { const accountData = await client.getAccountDataFromServer( - PosthogAnalytics.ANALYTICS_EVENT_TYPE + PosthogAnalytics.ANALYTICS_EVENT_TYPE, ); accountAnalyticsId = accountData?.id; } @@ -291,12 +293,14 @@ export class PosthogAnalytics { return null; } - async hashedEcAnalyticsId(accountAnalyticsId: string): Promise { + private async hashedEcAnalyticsId( + accountAnalyticsId: string, + ): Promise { const client: MatrixClient = window.matrixclient; const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId(); const bufferForPosthogId = await crypto.subtle.digest( "sha-256", - Buffer.from(posthogIdMaterial, "utf-8") + Buffer.from(posthogIdMaterial, "utf-8"), ); const view = new Int32Array(bufferForPosthogId); return Array.from(view) @@ -304,17 +308,17 @@ export class PosthogAnalytics { .join(""); } - async setAccountAnalyticsId(analyticsID: string) { + private async setAccountAnalyticsId(analyticsID: string): Promise { if (!widget) { const client = window.matrixclient; // the analytics ID only needs to be set in the standalone version. const accountData = await client.getAccountDataFromServer( - PosthogAnalytics.ANALYTICS_EVENT_TYPE + PosthogAnalytics.ANALYTICS_EVENT_TYPE, ); await client.setAccountData( PosthogAnalytics.ANALYTICS_EVENT_TYPE, - Object.assign({ id: analyticsID }, accountData) + Object.assign({ id: analyticsID }, accountData), ); } } @@ -335,7 +339,7 @@ export class PosthogAnalytics { this.updateAnonymityAndIdentifyUser(optInAnalytics); } - private updateSuperProperties() { + private updateSuperProperties(): void { // Update super properties in posthog with our platform (app version, platform). // These properties will be subsequently passed in every event. // @@ -356,7 +360,7 @@ export class PosthogAnalytics { } private async updateAnonymityAndIdentifyUser( - pseudonymousOptIn: boolean + pseudonymousOptIn: boolean, ): Promise { // Update this.anonymity based on the user's analytics opt-in settings const anonymity = pseudonymousOptIn @@ -372,11 +376,11 @@ export class PosthogAnalytics { this.setRegistrationType( window.matrixclient.isGuest() || window.passwordlessUser ? RegistrationType.Guest - : RegistrationType.Registered + : RegistrationType.Registered, ); // store the promise to await posthog-tracking-events until the identification is done. this.identificationPromise = this.identifyUser( - PosthogAnalytics.getRandomAnalyticsId + PosthogAnalytics.getRandomAnalyticsId, ); await this.identificationPromise; if (this.userRegisteredInThisSession()) { @@ -391,7 +395,7 @@ export class PosthogAnalytics { public async trackEvent( { eventName, ...properties }: E, - options?: CaptureOptions + options?: CaptureOptions, ): Promise { if (this.identificationPromise) { // only make calls to posthog after the identificaion is done diff --git a/src/analytics/PosthogEvents.ts b/src/analytics/PosthogEvents.ts index 97e25b6f..c200f74b 100644 --- a/src/analytics/PosthogEvents.ts +++ b/src/analytics/PosthogEvents.ts @@ -36,18 +36,22 @@ export class CallEndedTracker { maxParticipantsCount: 0, }; - cacheStartCall(time: Date) { + public cacheStartCall(time: Date): void { this.cache.startTime = time; } - cacheParticipantCountChanged(count: number) { + public cacheParticipantCountChanged(count: number): void { this.cache.maxParticipantsCount = Math.max( count, - this.cache.maxParticipantsCount + this.cache.maxParticipantsCount, ); } - track(callId: string, callParticipantsNow: number, sendInstantly: boolean) { + public track( + callId: string, + callParticipantsNow: number, + sendInstantly: boolean, + ): void { PosthogAnalytics.instance.trackEvent( { eventName: "CallEnded", @@ -56,7 +60,7 @@ export class CallEndedTracker { callParticipantsOnLeave: callParticipantsNow, callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000, }, - { send_instantly: sendInstantly } + { send_instantly: sendInstantly }, ); } } @@ -67,7 +71,7 @@ interface CallStarted extends IPosthogEvent { } export class CallStartedTracker { - track(callId: string) { + public track(callId: string): void { PosthogAnalytics.instance.trackEvent({ eventName: "CallStarted", callId: callId, @@ -86,19 +90,19 @@ export class SignupTracker { signupEnd: new Date(0), }; - cacheSignupStart(time: Date) { + public cacheSignupStart(time: Date): void { this.cache.signupStart = time; } - getSignupEndTime() { + public getSignupEndTime(): Date { return this.cache.signupEnd; } - cacheSignupEnd(time: Date) { + public cacheSignupEnd(time: Date): void { this.cache.signupEnd = time; } - track() { + public track(): void { PosthogAnalytics.instance.trackEvent({ eventName: "Signup", signupDuration: Date.now() - this.cache.signupStart.getTime(), @@ -112,7 +116,7 @@ interface Login extends IPosthogEvent { } export class LoginTracker { - track() { + public track(): void { PosthogAnalytics.instance.trackEvent({ eventName: "Login", }); @@ -127,7 +131,7 @@ interface MuteMicrophone { } export class MuteMicrophoneTracker { - track(targetIsMute: boolean, callId: string) { + public track(targetIsMute: boolean, callId: string): void { PosthogAnalytics.instance.trackEvent({ eventName: "MuteMicrophone", targetMuteState: targetIsMute ? "mute" : "unmute", @@ -143,7 +147,7 @@ interface MuteCamera { } export class MuteCameraTracker { - track(targetIsMute: boolean, callId: string) { + public track(targetIsMute: boolean, callId: string): void { PosthogAnalytics.instance.trackEvent({ eventName: "MuteCamera", targetMuteState: targetIsMute ? "mute" : "unmute", @@ -158,7 +162,7 @@ interface UndecryptableToDeviceEvent { } export class UndecryptableToDeviceEventTracker { - track(callId: string) { + public track(callId: string): void { PosthogAnalytics.instance.trackEvent({ eventName: "UndecryptableToDeviceEvent", callId, @@ -174,7 +178,7 @@ interface QualitySurveyEvent { } export class QualitySurveyEventTracker { - track(callId: string, feedbackText: string, stars: number) { + public track(callId: string, feedbackText: string, stars: number): void { PosthogAnalytics.instance.trackEvent({ eventName: "QualitySurvey", callId, @@ -190,7 +194,7 @@ interface CallDisconnectedEvent { } export class CallDisconnectedEventTracker { - track(reason?: DisconnectReason) { + public track(reason?: DisconnectReason): void { PosthogAnalytics.instance.trackEvent({ eventName: "CallDisconnected", reason, diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index effa6436..8904b3e1 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -39,9 +39,9 @@ const maxRejoinMs = 2 * 60 * 1000; // 2 minutes * Span processor that extracts certain metrics from spans to send to PostHog */ export class PosthogSpanProcessor implements SpanProcessor { - async forceFlush(): Promise {} + public async forceFlush(): Promise {} - onStart(span: Span): void { + public onStart(span: Span): void { // Hack: Yield to allow attributes to be set before processing Promise.resolve().then(() => { switch (span.name) { @@ -55,7 +55,7 @@ export class PosthogSpanProcessor implements SpanProcessor { }); } - onEnd(span: ReadableSpan): void { + public onEnd(span: ReadableSpan): void { switch (span.name) { case "matrix.groupCallMembership": this.onGroupCallMembershipEnd(span); @@ -148,7 +148,7 @@ export class PosthogSpanProcessor implements SpanProcessor { ratioPeerConnectionToDevices: ratioPeerConnectionToDevices, }, // Send instantly because the window might be closing - { send_instantly: true } + { send_instantly: true }, ); } } @@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor { /** * Shutdown the processor. */ - shutdown(): Promise { + public shutdown(): Promise { return Promise.resolve(); } } diff --git a/src/analytics/RageshakeSpanProcessor.ts b/src/analytics/RageshakeSpanProcessor.ts index b5b055f2..91d0e190 100644 --- a/src/analytics/RageshakeSpanProcessor.ts +++ b/src/analytics/RageshakeSpanProcessor.ts @@ -1,4 +1,20 @@ -import { Attributes } from "@opentelemetry/api"; +/* +Copyright 2023 New Vector Ltd + +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 { AttributeValue, Attributes } from "@opentelemetry/api"; import { hrTimeToMicroseconds } from "@opentelemetry/core"; import { SpanProcessor, @@ -6,7 +22,21 @@ import { Span, } from "@opentelemetry/sdk-trace-base"; -const dumpAttributes = (attr: Attributes) => +const dumpAttributes = ( + attr: Attributes, +): { + key: string; + type: + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function"; + value: AttributeValue | undefined; +}[] => Object.entries(attr).map(([key, value]) => ({ key, type: typeof value, @@ -20,13 +50,13 @@ const dumpAttributes = (attr: Attributes) => export class RageshakeSpanProcessor implements SpanProcessor { private readonly spans: ReadableSpan[] = []; - async forceFlush(): Promise {} + public async forceFlush(): Promise {} - onStart(span: Span): void { + public onStart(span: Span): void { this.spans.push(span); } - onEnd(): void {} + public onEnd(): void {} /** * Dumps the spans collected so far as Jaeger-compatible JSON. @@ -110,5 +140,5 @@ export class RageshakeSpanProcessor implements SpanProcessor { }); } - async shutdown(): Promise {} + public async shutdown(): Promise {} } diff --git a/src/array-utils.ts b/src/array-utils.ts index 6aeb1818..0bb0270d 100644 --- a/src/array-utils.ts +++ b/src/array-utils.ts @@ -22,7 +22,7 @@ limitations under the License. // Array.prototype.findLastIndex export function findLastIndex( array: T[], - predicate: (item: T, index: number) => boolean + predicate: (item: T, index: number) => boolean, ): number | null { for (let i = array.length - 1; i >= 0; i--) { if (predicate(array[i], i)) return i; @@ -36,9 +36,9 @@ export function findLastIndex( */ export const count = ( array: T[], - predicate: (item: T, index: number) => boolean + predicate: (item: T, index: number) => boolean, ): number => array.reduce( (acc, item, index) => (predicate(item, index) ? acc + 1 : acc), - 0 + 0, ); diff --git a/src/auth/LoginPage.tsx b/src/auth/LoginPage.tsx index 6569f262..d414a2bc 100644 --- a/src/auth/LoginPage.tsx +++ b/src/auth/LoginPage.tsx @@ -80,7 +80,7 @@ export const LoginPage: FC = () => { setLoading(false); }); }, - [login, location, history, homeserver, setClient] + [login, location, history, homeserver, setClient], ); return ( diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index 52c652f6..946989c8 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -69,7 +69,7 @@ export const RegisterPage: FC = () => { if (password !== passwordConfirmation) return; - const submit = async () => { + const submit = async (): Promise => { setRegistering(true); const recaptchaResponse = await execute(); @@ -78,7 +78,7 @@ export const RegisterPage: FC = () => { password, userName, recaptchaResponse, - passwordlessUser + passwordlessUser, ); if (client && client?.groupCallEventHandler && passwordlessUser) { @@ -135,7 +135,7 @@ export const RegisterPage: FC = () => { execute, client, setClient, - ] + ], ); useEffect(() => { @@ -184,7 +184,7 @@ export const RegisterPage: FC = () => { required name="password" type="password" - onChange={(e: ChangeEvent) => + onChange={(e: ChangeEvent): void => setPassword(e.target.value) } value={password} @@ -198,7 +198,7 @@ export const RegisterPage: FC = () => { required type="password" name="passwordConfirmation" - onChange={(e: ChangeEvent) => + onChange={(e: ChangeEvent): void => setPasswordConfirmation(e.target.value) } value={passwordConfirmation} diff --git a/src/auth/useInteractiveLogin.ts b/src/auth/useInteractiveLogin.ts index 07dec12d..83767f51 100644 --- a/src/auth/useInteractiveLogin.ts +++ b/src/auth/useInteractiveLogin.ts @@ -21,12 +21,16 @@ import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import { initClient } from "../matrix-utils"; import { Session } from "../ClientContext"; -export const useInteractiveLogin = () => - useCallback< +export function useInteractiveLogin(): ( + homeserver: string, + username: string, + password: string, +) => Promise<[MatrixClient, Session]> { + return useCallback< ( homeserver: string, username: string, - password: string + password: string, ) => Promise<[MatrixClient, Session]> >(async (homeserver: string, username: string, password: string) => { const authClient = createClient({ baseUrl: homeserver }); @@ -41,8 +45,8 @@ export const useInteractiveLogin = () => }, password, }), - stateUpdated: (...args) => {}, - requestEmailToken: (...args): Promise<{ sid: string }> => { + stateUpdated: (): void => {}, + requestEmailToken: (): Promise<{ sid: string }> => { return Promise.resolve({ sid: "" }); }, }); @@ -66,9 +70,9 @@ export const useInteractiveLogin = () => userId: user_id, deviceId: device_id, }, - false + false, ); /* eslint-enable camelcase */ - return [client, session]; }, []); +} diff --git a/src/auth/useInteractiveRegistration.ts b/src/auth/useInteractiveRegistration.ts index a4863671..9a69da6a 100644 --- a/src/auth/useInteractiveRegistration.ts +++ b/src/auth/useInteractiveRegistration.ts @@ -30,14 +30,14 @@ export const useInteractiveRegistration = (): { password: string, displayName: string, recaptchaResponse: string, - passwordlessUser: boolean + passwordlessUser: boolean, ) => Promise<[MatrixClient, Session]>; } => { const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState( - undefined + undefined, ); const [recaptchaKey, setRecaptchaKey] = useState( - undefined + undefined, ); const authClient = useRef(); @@ -50,7 +50,7 @@ export const useInteractiveRegistration = (): { useEffect(() => { authClient.current!.registerRequest({}).catch((error) => { setPrivacyPolicyUrl( - error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url + error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url, ); setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key); }); @@ -62,7 +62,7 @@ export const useInteractiveRegistration = (): { password: string, displayName: string, recaptchaResponse: string, - passwordlessUser: boolean + passwordlessUser: boolean, ): Promise<[MatrixClient, Session]> => { const interactiveAuth = new InteractiveAuth({ matrixClient: authClient.current!, @@ -72,7 +72,7 @@ export const useInteractiveRegistration = (): { password, auth: auth || undefined, }), - stateUpdated: (nextStage, status) => { + stateUpdated: (nextStage, status): void => { if (status.error) { throw new Error(status.error); } @@ -88,7 +88,7 @@ export const useInteractiveRegistration = (): { }); } }, - requestEmailToken: (...args) => { + requestEmailToken: (): Promise<{ sid: string }> => { return Promise.resolve({ sid: "dummy" }); }, }); @@ -106,7 +106,7 @@ export const useInteractiveRegistration = (): { userId: user_id, deviceId: device_id, }, - false + false, ); await client.setDisplayName(displayName); @@ -129,7 +129,7 @@ export const useInteractiveRegistration = (): { return [client, session]; }, - [] + [], ); return { privacyPolicyUrl, recaptchaKey, register }; diff --git a/src/auth/useRecaptcha.ts b/src/auth/useRecaptcha.ts index 0ee9f33b..b7583cd4 100644 --- a/src/auth/useRecaptcha.ts +++ b/src/auth/useRecaptcha.ts @@ -35,7 +35,11 @@ interface RecaptchaPromiseRef { reject: (error: Error) => void; } -export const useRecaptcha = (sitekey?: string) => { +export function useRecaptcha(sitekey?: string): { + execute: () => Promise; + reset: () => void; + recaptchaId: string; +} { const { t } = useTranslation(); const [recaptchaId] = useState(() => randomString(16)); const promiseRef = useRef(); @@ -43,7 +47,7 @@ export const useRecaptcha = (sitekey?: string) => { useEffect(() => { if (!sitekey) return; - const onRecaptchaLoaded = () => { + const onRecaptchaLoaded = (): void => { if (!document.getElementById(recaptchaId)) return; window.grecaptcha.render(recaptchaId, { @@ -91,11 +95,11 @@ export const useRecaptcha = (sitekey?: string) => { }); promiseRef.current = { - resolve: (value) => { + resolve: (value): void => { resolve(value); observer.disconnect(); }, - reject: (error) => { + reject: (error): void => { reject(error); observer.disconnect(); }, @@ -104,7 +108,7 @@ export const useRecaptcha = (sitekey?: string) => { window.grecaptcha.execute(); const iframe = document.querySelector( - 'iframe[src*="recaptcha/api2/bframe"]' + 'iframe[src*="recaptcha/api2/bframe"]', ); if (iframe?.parentNode?.parentNode) { @@ -120,4 +124,4 @@ export const useRecaptcha = (sitekey?: string) => { }, []); return { execute, reset, recaptchaId }; -}; +} diff --git a/src/auth/useRegisterPasswordlessUser.ts b/src/auth/useRegisterPasswordlessUser.ts index 983789ba..d34a479e 100644 --- a/src/auth/useRegisterPasswordlessUser.ts +++ b/src/auth/useRegisterPasswordlessUser.ts @@ -48,7 +48,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { randomString(16), displayName, recaptchaResponse, - true + true, ); setClient({ client, session }); } catch (e) { @@ -56,7 +56,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType { throw e; } }, - [execute, reset, register, setClient] + [execute, reset, register, setClient], ); return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId }; diff --git a/src/button/Button.module.css b/src/button/Button.module.css index d5cc8b15..7f915648 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -146,7 +146,9 @@ limitations under the License. .copyButton { width: 100%; height: 40px; - transition: border-color 250ms, background-color 250ms; + transition: + border-color 250ms, + background-color 250ms; } .copyButton span { diff --git a/src/button/Button.tsx b/src/button/Button.tsx index efb9f295..933be484 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -13,7 +13,7 @@ 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 { forwardRef } from "react"; +import { FC, forwardRef } from "react"; import { PressEvent } from "@react-types/shared"; import classNames from "classnames"; import { useButton } from "@react-aria/button"; @@ -94,12 +94,12 @@ export const Button = forwardRef( onPressStart, ...rest }, - ref + ref, ) => { const buttonRef = useObjectRef(ref); const { buttonProps } = useButton( { onPress, onPressStart, ...rest }, - buttonRef + buttonRef, ); // TODO: react-aria's useButton hook prevents form submission via keyboard @@ -121,7 +121,7 @@ export const Button = forwardRef( { [styles.on]: on, [styles.off]: off, - } + }, )} {...mergeProps(rest, filteredButtonProps)} ref={buttonRef} @@ -132,17 +132,14 @@ export const Button = forwardRef( ); - } + }, ); -export function MicButton({ - muted, - ...rest -}: { +export const MicButton: FC<{ muted: boolean; // TODO: add all props for ); -} +}; -export function VideoButton({ - muted, - ...rest -}: { +export const VideoButton: FC<{ muted: boolean; // TODO: add all props for ); -} +}; -export function ScreenshareButton({ - enabled, - className, - ...rest -}: { +export const ScreenshareButton: FC<{ enabled: boolean; className?: string; // TODO: add all props for ); -} +}; -export function HangupButton({ - className, - ...rest -}: { +export const HangupButton: FC<{ className?: string; // TODO: add all props for ); -} +}; -export function SettingsButton({ - className, - ...rest -}: { +export const SettingsButton: FC<{ className?: string; // TODO: add all props for ); -} +}; interface AudioButtonProps extends Omit { /** @@ -248,7 +232,7 @@ interface AudioButtonProps extends Omit { volume: number; } -export function AudioButton({ volume, ...rest }: AudioButtonProps) { +export const AudioButton: FC = ({ volume, ...rest }) => { const { t } = useTranslation(); return ( @@ -258,16 +242,16 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) { ); -} +}; interface FullscreenButtonProps extends Omit { fullscreen?: boolean; } -export function FullscreenButton({ +export const FullscreenButton: FC = ({ fullscreen, ...rest -}: FullscreenButtonProps) { +}) => { const { t } = useTranslation(); const Icon = fullscreen ? FullscreenExit : Fullscreen; const label = fullscreen ? t("Exit full screen") : t("Full screen"); @@ -279,4 +263,4 @@ export function FullscreenButton({ ); -} +}; diff --git a/src/button/CopyButton.tsx b/src/button/CopyButton.tsx index c2631c6e..1cbc6785 100644 --- a/src/button/CopyButton.tsx +++ b/src/button/CopyButton.tsx @@ -16,6 +16,7 @@ limitations under the License. import { useTranslation } from "react-i18next"; import useClipboard from "react-use-clipboard"; +import { FC } from "react"; import CheckIcon from "../icons/Check.svg?react"; import CopyIcon from "../icons/Copy.svg?react"; @@ -28,14 +29,15 @@ interface Props { variant?: ButtonVariant; copiedMessage?: string; } -export function CopyButton({ + +export const CopyButton: FC = ({ value, children, className, variant, copiedMessage, ...rest -}: Props) { +}) => { const { t } = useTranslation(); const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 }); @@ -62,4 +64,4 @@ export function CopyButton({ )} ); -} +}; diff --git a/src/button/LinkButton.tsx b/src/button/LinkButton.tsx index 3392935d..eb30441c 100644 --- a/src/button/LinkButton.tsx +++ b/src/button/LinkButton.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { HTMLAttributes } from "react"; +import { FC, HTMLAttributes } from "react"; import { Link } from "react-router-dom"; import classNames from "classnames"; import * as H from "history"; @@ -34,20 +34,20 @@ interface Props extends HTMLAttributes { className?: string; } -export function LinkButton({ +export const LinkButton: FC = ({ children, to, size, variant, className, ...rest -}: Props) { +}) => { return ( ); -} +}; diff --git a/src/config/Config.ts b/src/config/Config.ts index f414c58b..0930af49 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -57,7 +57,7 @@ export class Config { } async function downloadConfig( - configJsonFilename: string + configJsonFilename: string, ): Promise { const url = new URL(configJsonFilename, window.location.href); url.searchParams.set("cachebuster", Date.now().toString()); diff --git a/src/form/Form.tsx b/src/form/Form.tsx index 54961791..1f66ac1a 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -36,5 +36,5 @@ export const Form = forwardRef( {children} ); - } + }, ); diff --git a/src/home/CallList.tsx b/src/home/CallList.tsx index 8bd3778d..187228b9 100644 --- a/src/home/CallList.tsx +++ b/src/home/CallList.tsx @@ -18,6 +18,7 @@ import { Link } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room } from "matrix-js-sdk/src/models/room"; +import { FC } from "react"; import { CopyButton } from "../button"; import { Avatar, Size } from "../Avatar"; @@ -31,7 +32,8 @@ interface CallListProps { rooms: GroupCallRoom[]; client: MatrixClient; } -export function CallList({ rooms, client }: CallListProps) { + +export const CallList: FC = ({ rooms, client }) => { return ( <>
@@ -54,7 +56,7 @@ export function CallList({ rooms, client }: CallListProps) {
); -} +}; interface CallTileProps { name: string; avatarUrl: string; @@ -62,7 +64,8 @@ interface CallTileProps { participants: RoomMember[]; client: MatrixClient; } -function CallTile({ name, avatarUrl, room }: CallTileProps) { + +const CallTile: FC = ({ name, avatarUrl, room }) => { const roomSharedKey = useRoomSharedKey(room.roomId); return ( @@ -71,7 +74,7 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) { to={getRelativeRoomUrl( room.roomId, room.name, - roomSharedKey ?? undefined + roomSharedKey ?? undefined, )} className={styles.callTileLink} > @@ -89,9 +92,9 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) { value={getAbsoluteRoomUrl( room.roomId, room.name, - roomSharedKey ?? undefined + roomSharedKey ?? undefined, )} /> ); -} +}; diff --git a/src/home/HomePage.tsx b/src/home/HomePage.tsx index a78cb8bd..779a7168 100644 --- a/src/home/HomePage.tsx +++ b/src/home/HomePage.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import { useTranslation } from "react-i18next"; +import { FC } from "react"; import { useClientState } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; @@ -22,7 +23,7 @@ import { UnauthenticatedView } from "./UnauthenticatedView"; import { RegisteredView } from "./RegisteredView"; import { usePageTitle } from "../usePageTitle"; -export function HomePage() { +export const HomePage: FC = () => { const { t } = useTranslation(); usePageTitle(t("Home")); @@ -39,4 +40,4 @@ export function HomePage() { ); } -} +}; diff --git a/src/home/JoinExistingCallModal.tsx b/src/home/JoinExistingCallModal.tsx index 35672295..d8ed9fce 100644 --- a/src/home/JoinExistingCallModal.tsx +++ b/src/home/JoinExistingCallModal.tsx @@ -16,6 +16,7 @@ limitations under the License. import { PressEvent } from "@react-types/shared"; import { useTranslation } from "react-i18next"; +import { FC } from "react"; import { Modal } from "../Modal"; import { Button } from "../button"; @@ -28,7 +29,11 @@ interface Props { onJoin: (e: PressEvent) => void; } -export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) { +export const JoinExistingCallModal: FC = ({ + onJoin, + open, + onDismiss, +}) => { const { t } = useTranslation(); return ( @@ -42,4 +47,4 @@ export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) { ); -} +}; diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index 995f7a54..4a130899 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useState, useCallback, FormEvent, FormEventHandler } from "react"; +import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react"; import { useHistory } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; @@ -46,7 +46,7 @@ interface Props { client: MatrixClient; } -export function RegisteredView({ client }: Props) { +export const RegisteredView: FC = ({ client }) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [optInAnalytics] = useOptInAnalytics(); @@ -56,7 +56,7 @@ export function RegisteredView({ client }: Props) { useState(false); const onDismissJoinExistingCallModal = useCallback( () => setJoinExistingCallModalOpen(false), - [setJoinExistingCallModalOpen] + [setJoinExistingCallModalOpen], ); const [e2eeEnabled] = useEnableE2EE(); @@ -70,22 +70,22 @@ export function RegisteredView({ client }: Props) { ? sanitiseRoomNameInput(roomNameData) : ""; - async function submit() { + async function submit(): Promise { setError(undefined); setLoading(true); const createRoomResult = await createRoom( client, roomName, - e2eeEnabled ?? false + e2eeEnabled ?? false, ); history.push( getRelativeRoomUrl( createRoomResult.roomId, roomName, - createRoomResult.password - ) + createRoomResult.password, + ), ); } @@ -102,7 +102,7 @@ export function RegisteredView({ client }: Props) { } }); }, - [client, history, setJoinExistingCallModalOpen, e2eeEnabled] + [client, history, setJoinExistingCallModalOpen, e2eeEnabled], ); const recentRooms = useGroupCallRooms(client); @@ -175,4 +175,4 @@ export function RegisteredView({ client }: Props) { /> ); -} +}; diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 2f675436..beb419db 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -57,7 +57,7 @@ export const UnauthenticatedView: FC = () => { useState(false); const onDismissJoinExistingCallModal = useCallback( () => setJoinExistingCallModalOpen(false), - [setJoinExistingCallModalOpen] + [setJoinExistingCallModalOpen], ); const [onFinished, setOnFinished] = useState<() => void>(); const history = useHistory(); @@ -72,7 +72,7 @@ export const UnauthenticatedView: FC = () => { const roomName = sanitiseRoomNameInput(data.get("callName") as string); const displayName = data.get("displayName") as string; - async function submit() { + async function submit(): Promise { setError(undefined); setLoading(true); const recaptchaResponse = await execute(); @@ -82,7 +82,7 @@ export const UnauthenticatedView: FC = () => { randomString(16), displayName, recaptchaResponse, - true + true, ); let createRoomResult; @@ -90,7 +90,7 @@ export const UnauthenticatedView: FC = () => { createRoomResult = await createRoom( client, roomName, - e2eeEnabled ?? false + e2eeEnabled ?? false, ); } catch (error) { if (!setClient) { @@ -124,8 +124,8 @@ export const UnauthenticatedView: FC = () => { getRelativeRoomUrl( createRoomResult.roomId, roomName, - createRoomResult.password - ) + createRoomResult.password, + ), ); } @@ -144,7 +144,7 @@ export const UnauthenticatedView: FC = () => { setJoinExistingCallModalOpen, setClient, e2eeEnabled, - ] + ], ); return ( diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index 51633d8e..77572ad6 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -31,7 +31,7 @@ export interface GroupCallRoom { } const tsCache: { [index: string]: number } = {}; -function getLastTs(client: MatrixClient, r: Room) { +function getLastTs(client: MatrixClient, r: Room): number { if (tsCache[r.roomId]) { return tsCache[r.roomId]; } @@ -47,7 +47,7 @@ function getLastTs(client: MatrixClient, r: Room) { if (r.getMyMembership() !== "join") { const membershipEvent = r.currentState.getStateEvents( "m.room.member", - myUserId + myUserId, ); if (membershipEvent && !Array.isArray(membershipEvent)) { @@ -82,7 +82,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { const [rooms, setRooms] = useState([]); useEffect(() => { - function updateRooms() { + function updateRooms(): void { if (!client.groupCallEventHandler) { return; } @@ -115,7 +115,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms); client.removeListener( GroupCallEventHandlerEvent.Participants, - updateRooms + updateRooms, ); }; }, [client]); diff --git a/src/index.css b/src/index.css index d9f7d052..f647e87c 100644 --- a/src/index.css +++ b/src/index.css @@ -68,7 +68,8 @@ limitations under the License. font-weight: 400; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-Regular.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-Regular.woff2") format("woff2"), url("/fonts/Inter/Inter-Regular.woff") format("woff"); } @@ -78,7 +79,8 @@ limitations under the License. font-weight: 400; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-Italic.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-Italic.woff2") format("woff2"), url("/fonts/Inter/Inter-Italic.woff") format("woff"); } @@ -88,7 +90,8 @@ limitations under the License. font-weight: 500; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-Medium.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-Medium.woff2") format("woff2"), url("/fonts/Inter/Inter-Medium.woff") format("woff"); } @@ -98,7 +101,8 @@ limitations under the License. font-weight: 500; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"), url("/fonts/Inter/Inter-MediumItalic.woff") format("woff"); } @@ -108,7 +112,8 @@ limitations under the License. font-weight: 600; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"), url("/fonts/Inter/Inter-SemiBold.woff") format("woff"); } @@ -118,7 +123,8 @@ limitations under the License. font-weight: 600; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"), url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff"); } @@ -128,7 +134,8 @@ limitations under the License. font-weight: 700; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-Bold.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-Bold.woff2") format("woff2"), url("/fonts/Inter/Inter-Bold.woff") format("woff"); } @@ -138,7 +145,8 @@ limitations under the License. font-weight: 700; font-display: swap; unicode-range: var(--inter-unicode-range); - src: url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"), + src: + url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"), url("/fonts/Inter/Inter-BoldItalic.woff") format("woff"); } diff --git a/src/initializer.tsx b/src/initializer.tsx index 9a8152dd..57000936 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -35,11 +35,11 @@ enum LoadState { class DependencyLoadStates { // TODO: decide where olm should be initialized (see TODO comment below) // olm: LoadState = LoadState.None; - config: LoadState = LoadState.None; - sentry: LoadState = LoadState.None; - openTelemetry: LoadState = LoadState.None; + public config: LoadState = LoadState.None; + public sentry: LoadState = LoadState.None; + public openTelemetry: LoadState = LoadState.None; - allDepsAreLoaded() { + public allDepsAreLoaded(): boolean { return !Object.values(this).some((s) => s !== LoadState.Loaded); } } @@ -52,7 +52,7 @@ export class Initializer { return Initializer.internalInstance?.isInitialized; } - public static initBeforeReact() { + public static initBeforeReact(): void { // this maybe also needs to return a promise in the future, // if we have to do async inits before showing the loading screen // but this should be avioded if possible @@ -99,13 +99,13 @@ export class Initializer { if (fontScale !== null) { document.documentElement.style.setProperty( "--font-scale", - fontScale.toString() + fontScale.toString(), ); } if (fonts.length > 0) { document.documentElement.style.setProperty( "--font-family", - fonts.map((f) => `"${f}"`).join(", ") + fonts.map((f) => `"${f}"`).join(", "), ); } @@ -126,9 +126,9 @@ export class Initializer { return Initializer.internalInstance.initPromise; } - loadStates = new DependencyLoadStates(); + private loadStates = new DependencyLoadStates(); - initStep(resolve: (value: void | PromiseLike) => void) { + private initStep(resolve: (value: void | PromiseLike) => void): void { // TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`) // we need to decide if we want to init it here or keep it in initClient // if (this.loadStates.olm === LoadState.None) { diff --git a/src/input/AvatarInputField.tsx b/src/input/AvatarInputField.tsx index b8897c4a..266f2717 100644 --- a/src/input/AvatarInputField.tsx +++ b/src/input/AvatarInputField.tsx @@ -52,7 +52,7 @@ export const AvatarInputField = forwardRef( onRemoveAvatar, ...rest }, - ref + ref, ) => { const { t } = useTranslation(); @@ -64,7 +64,7 @@ export const AvatarInputField = forwardRef( useEffect(() => { const currentInput = fileInputRef.current; - const onChange = (e: Event) => { + const onChange = (e: Event): void => { const inputEvent = e as unknown as ChangeEvent; if (inputEvent.target.files && inputEvent.target.files.length > 0) { setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); @@ -76,7 +76,7 @@ export const AvatarInputField = forwardRef( currentInput.addEventListener("change", onChange); - return () => { + return (): void => { currentInput?.removeEventListener("change", onChange); }; }); @@ -120,5 +120,5 @@ export const AvatarInputField = forwardRef( )} ); - } + }, ); diff --git a/src/input/Input.module.css b/src/input/Input.module.css index dc1bde19..b81a903f 100644 --- a/src/input/Input.module.css +++ b/src/input/Input.module.css @@ -85,8 +85,11 @@ limitations under the License. } .inputField label { - transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s, - top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s; + transition: + font-size 0.25s ease-out 0.1s, + color 0.25s ease-out 0.1s, + top 0.25s ease-out 0.1s, + background-color 0.25s ease-out 0.1s; color: var(--cpd-color-text-secondary); background-color: transparent; font-size: var(--font-size-body); @@ -118,8 +121,11 @@ limitations under the License. .inputField textarea:not(:placeholder-shown) + label, .inputField.prefix textarea + label { background-color: var(--cpd-color-bg-canvas-default); - transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s, - top 0.25s ease-out 0s, background-color 0.25s ease-out 0s; + transition: + font-size 0.25s ease-out 0s, + color 0.25s ease-out 0s, + top 0.25s ease-out 0s, + background-color 0.25s ease-out 0s; font-size: var(--font-size-micro); top: -13px; padding: 0 2px; diff --git a/src/input/Input.tsx b/src/input/Input.tsx index b6826681..24bea83f 100644 --- a/src/input/Input.tsx +++ b/src/input/Input.tsx @@ -44,7 +44,7 @@ export function FieldRow({ className={classNames( styles.fieldRow, { [styles.rightAlign]: rightAlign }, - className + className, )} > {children} @@ -102,7 +102,7 @@ export const InputField = forwardRef< disabled, ...rest }, - ref + ref, ) => { const descriptionId = useId(); @@ -114,7 +114,7 @@ export const InputField = forwardRef< [styles.prefix]: !!prefix, [styles.disabled]: disabled, }, - className + className, )} > {prefix && {prefix}} @@ -163,7 +163,7 @@ export const InputField = forwardRef< )} ); - } + }, ); interface ErrorMessageProps { diff --git a/src/input/SelectInput.tsx b/src/input/SelectInput.tsx index 0feb346e..f0817582 100644 --- a/src/input/SelectInput.tsx +++ b/src/input/SelectInput.tsx @@ -38,7 +38,7 @@ export function SelectInput(props: Props): JSX.Element { const { labelProps, triggerProps, valueProps, menuProps } = useSelect( props, state, - ref + ref, ); const { buttonProps } = useButton(triggerProps, ref); diff --git a/src/input/StarRatingInput.tsx b/src/input/StarRatingInput.tsx index 8667998e..cd52082c 100644 --- a/src/input/StarRatingInput.tsx +++ b/src/input/StarRatingInput.tsx @@ -41,8 +41,8 @@ export function StarRatingInput({ return (
setHover(index)} - onMouseLeave={() => setHover(rating)} + onMouseEnter={(): void => setHover(index)} + onMouseLeave={(): void => setHover(rating)} key={index} > { + onChange={(_ev): void => { setRating(index); onChange(index); }} diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 1605edf3..df5b3fd9 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -51,8 +51,8 @@ export interface MediaDevices { // Cargo-culted from @livekit/components-react function useObservableState( observable: Observable | undefined, - startWith: T -) { + startWith: T, +): T { const [state, setState] = useState(startWith); useEffect(() => { // observable state doesn't run in SSR @@ -67,7 +67,7 @@ function useMediaDevice( kind: MediaDeviceKind, fallbackDevice: string | undefined, usingNames: boolean, - alwaysDefault: boolean = false + alwaysDefault: boolean = false, ): MediaDevice { // Make sure we don't needlessly reset to a device observer without names, // once permissions are already given @@ -83,7 +83,7 @@ function useMediaDevice( // kind, which then results in multiple permissions requests. const deviceObserver = useMemo( () => createMediaDeviceObserver(kind, requestPermissions), - [kind, requestPermissions] + [kind, requestPermissions], ); const available = useObservableState(deviceObserver, []); const [selectedId, select] = useState(fallbackDevice); @@ -143,18 +143,18 @@ export const MediaDevicesProvider: FC = ({ children }) => { const audioInput = useMediaDevice( "audioinput", audioInputSetting, - usingNames + usingNames, ); const audioOutput = useMediaDevice( "audiooutput", audioOutputSetting, useOutputNames, - alwaysUseDefaultAudio + alwaysUseDefaultAudio, ); const videoInput = useMediaDevice( "videoinput", videoInputSetting, - usingNames + usingNames, ); useEffect(() => { @@ -176,11 +176,11 @@ export const MediaDevicesProvider: FC = ({ children }) => { const startUsingDeviceNames = useCallback( () => setNumCallersUsingNames((n) => n + 1), - [setNumCallersUsingNames] + [setNumCallersUsingNames], ); const stopUsingDeviceNames = useCallback( () => setNumCallersUsingNames((n) => n - 1), - [setNumCallersUsingNames] + [setNumCallersUsingNames], ); const context: MediaDevices = useMemo( @@ -197,7 +197,7 @@ export const MediaDevicesProvider: FC = ({ children }) => { videoInput, startUsingDeviceNames, stopUsingDeviceNames, - ] + ], ); return ( @@ -207,7 +207,8 @@ export const MediaDevicesProvider: FC = ({ children }) => { ); }; -export const useMediaDevices = () => useContext(MediaDevicesContext); +export const useMediaDevices = (): MediaDevices => + useContext(MediaDevicesContext); /** * React hook that requests for the media devices context to be populated with @@ -215,7 +216,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext); * default because it may involve requesting additional permissions from the * user. */ -export const useMediaDeviceNames = (context: MediaDevices, enabled = true) => +export const useMediaDeviceNames = ( + context: MediaDevices, + enabled = true, +): void => useEffect(() => { if (enabled) { context.startUsingDeviceNames(); diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index d10f56fb..8b447485 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -42,14 +42,14 @@ export type OpenIDClientParts = Pick< export function useOpenIDSFU( client: OpenIDClientParts, - rtcSession: MatrixRTCSession -) { + rtcSession: MatrixRTCSession, +): SFUConfig | undefined { const [sfuConfig, setSFUConfig] = useState(undefined); const activeFocus = useActiveFocus(rtcSession); useEffect(() => { - (async () => { + (async (): Promise => { const sfuConfig = activeFocus ? await getSFUConfigWithOpenID(client, activeFocus) : undefined; @@ -62,20 +62,20 @@ export function useOpenIDSFU( export async function getSFUConfigWithOpenID( client: OpenIDClientParts, - activeFocus: LivekitFocus + activeFocus: LivekitFocus, ): Promise { const openIdToken = await client.getOpenIdToken(); logger.debug("Got openID token", openIdToken); try { logger.info( - `Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...` + `Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`, ); const sfuConfig = await getLiveKitJWT( client, activeFocus.livekit_service_url, activeFocus.livekit_alias, - openIdToken + openIdToken, ); logger.info(`Got JWT from call's active focus URL.`); @@ -83,7 +83,7 @@ export async function getSFUConfigWithOpenID( } catch (e) { logger.warn( `Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`, - e + e, ); return undefined; } @@ -93,7 +93,7 @@ async function getLiveKitJWT( client: OpenIDClientParts, livekitServiceURL: string, roomName: string, - openIDToken: IOpenIDToken + openIDToken: IOpenIDToken, ): Promise { try { const res = await fetch(livekitServiceURL + "/sfu/get", { diff --git a/src/livekit/options.ts b/src/livekit/options.ts index 7f9695f1..00f19b12 100644 --- a/src/livekit/options.ts +++ b/src/livekit/options.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 { AudioPresets, DefaultReconnectPolicy, diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts index e9298eb7..0b18cd10 100644 --- a/src/livekit/useECConnectionState.ts +++ b/src/livekit/useECConnectionState.ts @@ -51,7 +51,7 @@ async function doConnect( livekitRoom: Room, sfuConfig: SFUConfig, audioEnabled: boolean, - audioOptions: AudioCaptureOptions + audioOptions: AudioCaptureOptions, ): Promise { await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt); @@ -76,12 +76,12 @@ export function useECConnectionState( initialAudioOptions: AudioCaptureOptions, initialAudioEnabled: boolean, livekitRoom?: Room, - sfuConfig?: SFUConfig + sfuConfig?: SFUConfig, ): ECConnectionState { const [connState, setConnState] = useState( sfuConfig && livekitRoom ? livekitRoom.state - : ECAddonConnectionState.ECWaiting + : ECAddonConnectionState.ECWaiting, ); const [isSwitchingFocus, setSwitchingFocus] = useState(false); @@ -116,10 +116,10 @@ export function useECConnectionState( !sfuConfigEquals(currentSFUConfig.current, sfuConfig) ) { logger.info( - `SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}` + `SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`, ); - (async () => { + (async (): Promise => { setSwitchingFocus(true); await livekitRoom?.disconnect(); setIsInDoConnect(true); @@ -128,7 +128,7 @@ export function useECConnectionState( livekitRoom!, sfuConfig!, initialAudioEnabled, - initialAudioOptions + initialAudioOptions, ); } finally { setIsInDoConnect(false); @@ -149,7 +149,7 @@ export function useECConnectionState( livekitRoom!, sfuConfig!, initialAudioEnabled, - initialAudioOptions + initialAudioOptions, ).finally(() => setIsInDoConnect(false)); } diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 5396d4b3..c8a5e401 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -52,7 +52,7 @@ interface UseLivekitResult { export function useLiveKit( muteStates: MuteStates, sfuConfig?: SFUConfig, - e2eeConfig?: E2EEConfig + e2eeConfig?: E2EEConfig, ): UseLivekitResult { const e2eeOptions = useMemo(() => { if (!e2eeConfig?.sharedKey) return undefined; @@ -67,7 +67,7 @@ export function useLiveKit( if (!e2eeConfig?.sharedKey || !e2eeOptions) return; (e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey( - e2eeConfig?.sharedKey + e2eeConfig?.sharedKey, ); }, [e2eeOptions, e2eeConfig?.sharedKey]); @@ -93,7 +93,7 @@ export function useLiveKit( }, e2ee: e2eeOptions, }), - [e2eeOptions] + [e2eeOptions], ); // useECConnectionState creates and publishes an audio track by hand. To keep @@ -131,7 +131,7 @@ export function useLiveKit( }, initialMuteStates.current.audio.enabled, room, - sfuConfig + sfuConfig, ); // Unblock audio once the connection is finished @@ -154,7 +154,7 @@ export function useLiveKit( audio: muteStates.audio.enabled, video: muteStates.video.enabled, }; - const syncMuteStateAudio = async () => { + const syncMuteStateAudio = async (): Promise => { if ( participant.isMicrophoneEnabled !== buttonEnabled.current.audio && !audioMuteUpdating.current @@ -174,7 +174,7 @@ export function useLiveKit( syncMuteStateAudio(); } }; - const syncMuteStateVideo = async () => { + const syncMuteStateVideo = async (): Promise => { if ( participant.isCameraEnabled !== buttonEnabled.current.video && !videoMuteUpdating.current @@ -198,7 +198,7 @@ export function useLiveKit( useEffect(() => { // Sync the requested devices with LiveKit's devices if (room !== undefined && connectionState === ConnectionState.Connected) { - const syncDevice = (kind: MediaDeviceKind, device: MediaDevice) => { + const syncDevice = (kind: MediaDeviceKind, device: MediaDevice): void => { const id = device.selectedId; // Detect if we're trying to use chrome's default device, in which case @@ -215,11 +215,11 @@ export function useLiveKit( room.options.audioCaptureDefaults?.deviceId === "default" ) { const activeMicTrack = Array.from( - room.localParticipant.audioTracks.values() + room.localParticipant.audioTracks.values(), ).find((d) => d.source === Track.Source.Microphone)?.track; const defaultDevice = device.available.find( - (d) => d.deviceId === "default" + (d) => d.deviceId === "default", ); if ( defaultDevice && @@ -245,7 +245,7 @@ export function useLiveKit( room .switchActiveDevice(kind, id) .catch((e) => - logger.error(`Failed to sync ${kind} device with LiveKit`, e) + logger.error(`Failed to sync ${kind} device with LiveKit`, e), ); } } diff --git a/src/main.tsx b/src/main.tsx index 4e962181..ed64a09c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -30,7 +30,7 @@ import { setLogLevel, } from "livekit-client"; -import App from "./App"; +import { App } from "./App"; import { init as initRageshake } from "./settings/rageshake"; import { Initializer } from "./initializer"; @@ -48,7 +48,7 @@ if (!window.isSecureContext) { fatalError = new Error( "This app cannot run in an insecure context. To fix this, access the app " + "via a local loopback address, or serve it over HTTPS.\n" + - "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", ); } else if (!navigator.mediaDevices) { fatalError = new Error("Your browser does not support WebRTC."); @@ -66,5 +66,5 @@ const history = createBrowserHistory(); root.render( - + , ); diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 54105519..52c55959 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -42,7 +42,7 @@ export const fallbackICEServerAllowed = import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; export class CryptoStoreIntegrityError extends Error { - constructor() { + public constructor() { super("Crypto store data was expected, but none was found"); } } @@ -54,13 +54,13 @@ const SYNC_STORE_NAME = "element-call-sync"; // (It's a good opportunity to make the database names consistent.) const CRYPTO_STORE_NAME = "element-call-crypto"; -function waitForSync(client: MatrixClient) { +function waitForSync(client: MatrixClient): Promise { return new Promise((resolve, reject) => { const onSync = ( state: SyncState, _old: SyncState | null, - data?: ISyncStateData - ) => { + data?: ISyncStateData, + ): void => { if (state === "PREPARED") { client.removeListener(ClientEvent.Sync, onSync); resolve(); @@ -83,7 +83,7 @@ function secureRandomString(entropyBytes: number): string { // yet) so just use the built-in one and convert, replace the chars and strip the // padding from the end (otherwise we'd need to pull in another dependency). return btoa( - key.reduce((acc, current) => acc + String.fromCharCode(current), "") + key.reduce((acc, current) => acc + String.fromCharCode(current), ""), ) .replace("+", "-") .replace("/", "_") @@ -101,7 +101,7 @@ function secureRandomString(entropyBytes: number): string { */ export async function initClient( clientOptions: ICreateClientOpts, - restore: boolean + restore: boolean, ): Promise { await loadOlm(); @@ -127,7 +127,7 @@ export async function initClient( // Chrome supports it. (It bundles them fine in production mode.) workerFactory: import.meta.env.DEV ? undefined - : () => new IndexedDBWorker(), + : (): Worker => new IndexedDBWorker(), }); } else if (localStorage) { baseOpts.store = new MemoryStore({ localStorage }); @@ -148,7 +148,7 @@ export async function initClient( if (indexedDB) { const cryptoStoreExists = await IndexedDBCryptoStore.exists( indexedDB, - CRYPTO_STORE_NAME + CRYPTO_STORE_NAME, ); if (!cryptoStoreExists) throw new CryptoStoreIntegrityError(); } else if (localStorage) { @@ -164,7 +164,7 @@ export async function initClient( if (indexedDB) { baseOpts.cryptoStore = new IndexedDBCryptoStore( indexedDB, - CRYPTO_STORE_NAME + CRYPTO_STORE_NAME, ); } else if (localStorage) { baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); @@ -198,7 +198,7 @@ export async function initClient( } catch (error) { logger.error( "Error starting matrix client store. Falling back to memory store.", - error + error, ); client.store = new MemoryStore({ localStorage }); await client.store.startup(); @@ -268,7 +268,7 @@ export function roomNameFromRoomId(roomId: string): string { .substring(1) .split("-") .map((part) => - part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part + part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part, ) .join(" ") .toLowerCase(); @@ -297,7 +297,7 @@ interface CreateRoomResult { export async function createRoom( client: MatrixClient, name: string, - e2ee: boolean + e2ee: boolean, ): Promise { logger.log(`Creating room for group call`); const createPromise = client.createRoom({ @@ -332,7 +332,7 @@ export async function createRoom( // Wait for the room to arrive await new Promise((resolve, reject) => { - const onRoom = async (room: Room) => { + const onRoom = async (room: Room): Promise => { if (room.roomId === (await createPromise).room_id) { resolve(); cleanUp(); @@ -343,7 +343,7 @@ export async function createRoom( cleanUp(); }); - const cleanUp = () => { + const cleanUp = (): void => { client.off(ClientEvent.Room, onRoom); }; client.on(ClientEvent.Room, onRoom); @@ -358,7 +358,7 @@ export async function createRoom( GroupCallType.Video, false, GroupCallIntent.Room, - true + true, ); let password; @@ -366,7 +366,7 @@ export async function createRoom( password = secureRandomString(16); setLocalStorageItem( getRoomSharedKeyLocalStorageKey(result.room_id), - password + password, ); } @@ -386,7 +386,7 @@ export async function createRoom( export function getAbsoluteRoomUrl( roomId: string, roomName?: string, - password?: string + password?: string, ): string { return `${window.location.protocol}//${ window.location.host @@ -402,7 +402,7 @@ export function getAbsoluteRoomUrl( export function getRelativeRoomUrl( roomId: string, roomName?: string, - password?: string + password?: string, ): string { // The password shouldn't need URL encoding here (we generate URL-safe ones) but encode // it in case it came from another client that generated a non url-safe one @@ -419,7 +419,7 @@ export function getRelativeRoomUrl( export function getAvatarUrl( client: MatrixClient, mxcUrl: string, - avatarSize = 96 + avatarSize = 96, ): string { const width = Math.floor(avatarSize * window.devicePixelRatio); const height = Math.floor(avatarSize * window.devicePixelRatio); diff --git a/src/media-utils.ts b/src/media-utils.ts index ec829c76..74e5ca33 100644 --- a/src/media-utils.ts +++ b/src/media-utils.ts @@ -23,10 +23,10 @@ limitations under the License. export async function findDeviceByName( deviceName: string, kind: MediaDeviceKind, - devices: MediaDeviceInfo[] + devices: MediaDeviceInfo[], ): Promise { const deviceInfo = devices.find( - (d) => d.kind === kind && d.label === deviceName + (d) => d.kind === kind && d.label === deviceName, ); return deviceInfo?.deviceId; } diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts index c7a7fb1a..13157763 100644 --- a/src/otel/OTelCall.ts +++ b/src/otel/OTelCall.ts @@ -44,65 +44,65 @@ export class OTelCall { OTelCallAbstractMediaStreamSpan >(); - constructor( + public constructor( public userId: string, public deviceId: string, public call: MatrixCall, - public span: Span + public span: Span, ) { if (call.peerConn) { this.addCallPeerConnListeners(); } else { this.call.once( CallEvent.PeerConnectionCreated, - this.addCallPeerConnListeners + this.addCallPeerConnListeners, ); } } - public dispose() { + public dispose(): void { this.call.peerConn?.removeEventListener( "connectionstatechange", - this.onCallConnectionStateChanged + this.onCallConnectionStateChanged, ); this.call.peerConn?.removeEventListener( "signalingstatechange", - this.onCallSignalingStateChanged + this.onCallSignalingStateChanged, ); this.call.peerConn?.removeEventListener( "iceconnectionstatechange", - this.onIceConnectionStateChanged + this.onIceConnectionStateChanged, ); this.call.peerConn?.removeEventListener( "icegatheringstatechange", - this.onIceGatheringStateChanged + this.onIceGatheringStateChanged, ); this.call.peerConn?.removeEventListener( "icecandidateerror", - this.onIceCandidateError + this.onIceCandidateError, ); } private addCallPeerConnListeners = (): void => { this.call.peerConn?.addEventListener( "connectionstatechange", - this.onCallConnectionStateChanged + this.onCallConnectionStateChanged, ); this.call.peerConn?.addEventListener( "signalingstatechange", - this.onCallSignalingStateChanged + this.onCallSignalingStateChanged, ); this.call.peerConn?.addEventListener( "iceconnectionstatechange", - this.onIceConnectionStateChanged + this.onIceConnectionStateChanged, ); this.call.peerConn?.addEventListener( "icegatheringstatechange", - this.onIceGatheringStateChanged + this.onIceGatheringStateChanged, ); this.call.peerConn?.addEventListener( "icecandidateerror", - this.onIceCandidateError + this.onIceCandidateError, ); }; @@ -147,8 +147,8 @@ export class OTelCall { new OTelCallFeedMediaStreamSpan( ElementCallOpenTelemetry.instance, this.span, - feed - ) + feed, + ), ); } this.trackFeedSpan.get(feed.stream)?.update(feed); @@ -171,13 +171,13 @@ export class OTelCall { new OTelCallTransceiverMediaStreamSpan( ElementCallOpenTelemetry.instance, this.span, - transStats - ) + transStats, + ), ); } this.trackTransceiverSpan.get(transStats.mid)?.update(transStats); prvTransSpan = prvTransSpan.filter( - (prvStreamId) => prvStreamId !== transStats.mid + (prvStreamId) => prvStreamId !== transStats.mid, ); }); @@ -190,7 +190,7 @@ export class OTelCall { public end(): void { this.trackFeedSpan.forEach((feedSpan) => feedSpan.end()); this.trackTransceiverSpan.forEach((transceiverSpan) => - transceiverSpan.end() + transceiverSpan.end(), ); this.span.end(); } diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts index aa77051d..c34f963c 100644 --- a/src/otel/OTelCallAbstractMediaStreamSpan.ts +++ b/src/otel/OTelCallAbstractMediaStreamSpan.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 opentelemetry, { Span } from "@opentelemetry/api"; import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; @@ -14,13 +30,13 @@ export abstract class OTelCallAbstractMediaStreamSpan { public readonly span; public constructor( - readonly oTel: ElementCallOpenTelemetry, - readonly callSpan: Span, - protected readonly type: string + protected readonly oTel: ElementCallOpenTelemetry, + protected readonly callSpan: Span, + protected readonly type: string, ) { const ctx = opentelemetry.trace.setSpan( opentelemetry.context.active(), - callSpan + callSpan, ); const options = { links: [ @@ -32,13 +48,13 @@ export abstract class OTelCallAbstractMediaStreamSpan { this.span = oTel.tracer.startSpan(this.type, options, ctx); } - protected upsertTrackSpans(tracks: TrackStats[]) { + protected upsertTrackSpans(tracks: TrackStats[]): void { let prvTracks: TrackId[] = [...this.trackSpans.keys()]; tracks.forEach((t) => { if (!this.trackSpans.has(t.id)) { this.trackSpans.set( t.id, - new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t) + new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t), ); } this.trackSpans.get(t.id)?.update(t); diff --git a/src/otel/OTelCallFeedMediaStreamSpan.ts b/src/otel/OTelCallFeedMediaStreamSpan.ts index 6023fa65..02dd2f86 100644 --- a/src/otel/OTelCallFeedMediaStreamSpan.ts +++ b/src/otel/OTelCallFeedMediaStreamSpan.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 { Span } from "@opentelemetry/api"; import { CallFeedStats, @@ -10,10 +26,10 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; - constructor( - readonly oTel: ElementCallOpenTelemetry, - readonly callSpan: Span, - callFeed: CallFeedStats + public constructor( + protected readonly oTel: ElementCallOpenTelemetry, + protected readonly callSpan: Span, + callFeed: CallFeedStats, ) { const postFix = callFeed.type === "local" && callFeed.prefix === "from-call-feed" diff --git a/src/otel/OTelCallMediaStreamTrackSpan.ts b/src/otel/OTelCallMediaStreamTrackSpan.ts index 935e22fc..72fee9b4 100644 --- a/src/otel/OTelCallMediaStreamTrackSpan.ts +++ b/src/otel/OTelCallMediaStreamTrackSpan.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import opentelemetry, { Span } from "@opentelemetry/api"; @@ -8,13 +24,13 @@ export class OTelCallMediaStreamTrackSpan { private prev: TrackStats; public constructor( - readonly oTel: ElementCallOpenTelemetry, - readonly streamSpan: Span, - data: TrackStats + protected readonly oTel: ElementCallOpenTelemetry, + protected readonly streamSpan: Span, + data: TrackStats, ) { const ctx = opentelemetry.trace.setSpan( opentelemetry.context.active(), - streamSpan + streamSpan, ); const options = { links: [ diff --git a/src/otel/OTelCallTransceiverMediaStreamSpan.ts b/src/otel/OTelCallTransceiverMediaStreamSpan.ts index 97006cd8..2be5dbe6 100644 --- a/src/otel/OTelCallTransceiverMediaStreamSpan.ts +++ b/src/otel/OTelCallTransceiverMediaStreamSpan.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 New Vector Ltd + +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 { Span } from "@opentelemetry/api"; import { TrackStats, @@ -13,10 +29,10 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr currentDirection: string; }; - constructor( - readonly oTel: ElementCallOpenTelemetry, - readonly callSpan: Span, - stats: TransceiverStats + public constructor( + protected readonly oTel: ElementCallOpenTelemetry, + protected readonly callSpan: Span, + stats: TransceiverStats, ) { super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); this.span.setAttribute("transceiver.mid", stats.mid); diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index b60d5aa9..b700e2bc 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -62,7 +62,10 @@ export class OTelGroupCallMembership { }; private readonly speakingSpans = new Map>(); - constructor(private groupCall: GroupCall, client: MatrixClient) { + public constructor( + private groupCall: GroupCall, + client: MatrixClient, + ) { const clientId = client.getUserId(); if (clientId) { this.myUserId = clientId; @@ -76,14 +79,14 @@ export class OTelGroupCallMembership { this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); } - dispose() { + public dispose(): void { this.groupCall.removeListener( GroupCallEvent.CallsChanged, - this.onCallsChanged + this.onCallsChanged, ); } - public onJoinCall() { + public onJoinCall(): void { if (!ElementCallOpenTelemetry.instance) return; if (this.callMembershipSpan !== undefined) { logger.warn("Call membership span is already started"); @@ -93,28 +96,28 @@ export class OTelGroupCallMembership { // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = ElementCallOpenTelemetry.instance.tracer.startSpan( - "matrix.groupCallMembership" + "matrix.groupCallMembership", ); this.callMembershipSpan.setAttribute( "matrix.confId", - this.groupCall.groupCallId + this.groupCall.groupCallId, ); this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute( "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name" + this.myMember ? this.myMember.name : "unknown-name", ); this.groupCallContext = opentelemetry.trace.setSpan( opentelemetry.context.active(), - this.callMembershipSpan + this.callMembershipSpan, ); this.callMembershipSpan?.addEvent("matrix.joinCall"); } - public onLeaveCall() { + public onLeaveCall(): void { if (this.callMembershipSpan === undefined) { logger.warn("Call membership span is already ended"); return; @@ -127,7 +130,7 @@ export class OTelGroupCallMembership { this.groupCallContext = undefined; } - public onUpdateRoomState(event: MatrixEvent) { + public onUpdateRoomState(event: MatrixEvent): void { if ( !event || (!event.getType().startsWith("m.call") && @@ -138,11 +141,11 @@ export class OTelGroupCallMembership { this.callMembershipSpan?.addEvent( `matrix.roomStateEvent_${event.getType()}`, - ObjectFlattener.flattenVoipEvent(event.getContent()) + ObjectFlattener.flattenVoipEvent(event.getContent()), ); } - public onCallsChanged = (calls: CallsByUserAndDevice) => { + public onCallsChanged(calls: CallsByUserAndDevice): void { for (const [userId, userCalls] of calls.entries()) { for (const [deviceId, call] of userCalls.entries()) { if (!this.callsByCallId.has(call.callId)) { @@ -150,7 +153,7 @@ export class OTelGroupCallMembership { const span = ElementCallOpenTelemetry.instance.tracer.startSpan( `matrix.call`, undefined, - this.groupCallContext + this.groupCallContext, ); // XXX: anonymity span.setAttribute("matrix.call.target.userId", userId); @@ -160,7 +163,7 @@ export class OTelGroupCallMembership { span.setAttribute("matrix.call.target.displayName", displayName); this.callsByCallId.set( call.callId, - new OTelCall(userId, deviceId, call, span) + new OTelCall(userId, deviceId, call, span), ); } } @@ -179,9 +182,9 @@ export class OTelGroupCallMembership { this.callsByCallId.delete(callTrackingInfo.call.callId); } } - }; + } - public onCallStateChange(call: MatrixCall, newState: CallState) { + public onCallStateChange(call: MatrixCall, newState: CallState): void { const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { logger.error(`Got call state change for unknown call ID ${call.callId}`); @@ -193,7 +196,7 @@ export class OTelGroupCallMembership { }); } - public onSendEvent(call: MatrixCall, event: VoipEvent) { + public onSendEvent(call: MatrixCall, event: VoipEvent): void { const eventType = event.eventType as string; if ( !eventType.startsWith("m.call") && @@ -210,17 +213,17 @@ export class OTelGroupCallMembership { if (event.type === "toDevice") { callTrackingInfo.span.addEvent( `matrix.sendToDeviceEvent_${event.eventType}`, - ObjectFlattener.flattenVoipEvent(event) + ObjectFlattener.flattenVoipEvent(event), ); } else if (event.type === "sendEvent") { callTrackingInfo.span.addEvent( `matrix.sendToRoomEvent_${event.eventType}`, - ObjectFlattener.flattenVoipEvent(event) + ObjectFlattener.flattenVoipEvent(event), ); } } - public onReceivedVoipEvent(event: MatrixEvent) { + public onReceivedVoipEvent(event: MatrixEvent): void { // These come straight from CallEventHandler so don't have // a call already associated (in principle we could receive // events for calls we don't know about). @@ -239,7 +242,7 @@ export class OTelGroupCallMembership { "matrix.receive_voip_event_unknown_callid", { "sender.userId": event.getSender(), - } + }, ); logger.error("Received call event for unknown call ID " + callId); return; @@ -251,37 +254,41 @@ export class OTelGroupCallMembership { }); } - public onToggleMicrophoneMuted(newValue: boolean) { + public onToggleMicrophoneMuted(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, }); } - public onSetMicrophoneMuted(setMuted: boolean) { + public onSetMicrophoneMuted(setMuted: boolean): void { this.callMembershipSpan?.addEvent("matrix.setMicMuted", { "matrix.microphone.muted": setMuted, }); } - public onToggleLocalVideoMuted(newValue: boolean) { + public onToggleLocalVideoMuted(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { "matrix.video.muted": newValue, }); } - public onSetLocalVideoMuted(setMuted: boolean) { + public onSetLocalVideoMuted(setMuted: boolean): void { this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.video.muted": setMuted, }); } - public onToggleScreensharing(newValue: boolean) { + public onToggleScreensharing(newValue: boolean): void { this.callMembershipSpan?.addEvent("matrix.setVidMuted", { "matrix.screensharing.enabled": newValue, }); } - public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) { + public onSpeaking( + member: RoomMember, + deviceId: string, + speaking: boolean, + ): void { if (speaking) { // Ensure that there's an audio activity span for this speaker let deviceMap = this.speakingSpans.get(member); @@ -294,7 +301,7 @@ export class OTelGroupCallMembership { const span = ElementCallOpenTelemetry.instance.tracer.startSpan( "matrix.audioActivity", undefined, - this.groupCallContext + this.groupCallContext, ); span.setAttribute("matrix.userId", member.userId); span.setAttribute("matrix.displayName", member.rawDisplayName); @@ -311,7 +318,7 @@ export class OTelGroupCallMembership { } } - public onCallError(error: CallError, call: MatrixCall) { + public onCallError(error: CallError, call: MatrixCall): void { const callTrackingInfo = this.callsByCallId.get(call.callId); if (!callTrackingInfo) { logger.error(`Got error for unknown call ID ${call.callId}`); @@ -321,17 +328,19 @@ export class OTelGroupCallMembership { callTrackingInfo.span.recordException(error); } - public onGroupCallError(error: GroupCallError) { + public onGroupCallError(error: GroupCallError): void { this.callMembershipSpan?.recordException(error); } - public onUndecryptableToDevice(event: MatrixEvent) { + public onUndecryptableToDevice(event: MatrixEvent): void { this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { "sender.userId": event.getSender(), }); } - public onCallFeedStatsReport(report: GroupCallStatsReport) { + public onCallFeedStatsReport( + report: GroupCallStatsReport, + ): void { if (!ElementCallOpenTelemetry.instance) return; let call: OTelCall | undefined; const callId = report.report?.callId; @@ -348,10 +357,10 @@ export class OTelGroupCallMembership { "call.opponentMemberId": report.report?.opponentMemberId ? report.report?.opponentMemberId : "unknown", - } + }, ); logger.error( - `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}` + `Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`, ); return; } else { @@ -361,26 +370,26 @@ export class OTelGroupCallMembership { } public onConnectionStatsReport( - statsReport: GroupCallStatsReport - ) { + statsReport: GroupCallStatsReport, + ): void { this.buildCallStatsSpan( OTelStatsReportType.ConnectionReport, - statsReport.report + statsReport.report, ); } public onByteSentStatsReport( - statsReport: GroupCallStatsReport - ) { + statsReport: GroupCallStatsReport, + ): void { this.buildCallStatsSpan( OTelStatsReportType.ByteSentReport, - statsReport.report + statsReport.report, ); } public buildCallStatsSpan( type: OTelStatsReportType, - report: ByteSentStatsReport | ConnectionStatsReport + report: ByteSentStatsReport | ConnectionStatsReport, ): void { if (!ElementCallOpenTelemetry.instance) return; let call: OTelCall | undefined; @@ -403,7 +412,7 @@ export class OTelGroupCallMembership { const data = ObjectFlattener.flattenReportObject(type, report); const ctx = opentelemetry.trace.setSpan( opentelemetry.context.active(), - call.span + call.span, ); const options = { @@ -417,21 +426,21 @@ export class OTelGroupCallMembership { const span = ElementCallOpenTelemetry.instance.tracer.startSpan( type, options, - ctx + ctx, ); span.setAttribute("matrix.callId", callId ?? "unknown"); span.setAttribute( "matrix.opponentMemberId", - report.opponentMemberId ? report.opponentMemberId : "unknown" + report.opponentMemberId ? report.opponentMemberId : "unknown", ); span.addEvent("matrix.call.connection_stats_event", data); span.end(); } public onSummaryStatsReport( - statsReport: GroupCallStatsReport - ) { + statsReport: GroupCallStatsReport, + ): void { if (!ElementCallOpenTelemetry.instance) return; const type = OTelStatsReportType.SummaryReport; @@ -439,12 +448,12 @@ export class OTelGroupCallMembership { if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { const ctx = setSpan( opentelemetry.context.active(), - this.callMembershipSpan + this.callMembershipSpan, ); const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( "matrix.groupCallMembership.summaryReport", undefined, - ctx + ctx, ); if (span === undefined) { return; @@ -453,7 +462,7 @@ export class OTelGroupCallMembership { span.setAttribute("matrix.userId", this.myUserId); span.setAttribute( "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name" + this.myMember ? this.myMember.name : "unknown-name", ); span.addEvent(type, data); span.end(); diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index 652f9056..76de9e3a 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -25,7 +25,7 @@ import { export class ObjectFlattener { public static flattenReportObject( prefix: string, - report: ConnectionStatsReport | ByteSentStatsReport + report: ConnectionStatsReport | ByteSentStatsReport, ): Attributes { const flatObject = {}; ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0); @@ -33,27 +33,27 @@ export class ObjectFlattener { } public static flattenByteSentStatsReportObject( - statsReport: GroupCallStatsReport + statsReport: GroupCallStatsReport, ): Attributes { const flatObject = {}; ObjectFlattener.flattenObjectRecursive( statsReport.report, flatObject, "matrix.stats.bytesSent.", - 0 + 0, ); return flatObject; } - static flattenSummaryStatsReportObject( - statsReport: GroupCallStatsReport - ) { + public static flattenSummaryStatsReportObject( + statsReport: GroupCallStatsReport, + ): Attributes { const flatObject = {}; ObjectFlattener.flattenObjectRecursive( statsReport.report, flatObject, "matrix.stats.summary.", - 0 + 0, ); return flatObject; } @@ -67,7 +67,7 @@ export class ObjectFlattener { event as unknown as Record, // XXX Types flatObject, "matrix.event.", - 0 + 0, ); return flatObject; @@ -77,12 +77,12 @@ export class ObjectFlattener { obj: Object, flatObject: Attributes, prefix: string, - depth: number + depth: number, ): void { if (depth > 10) throw new Error( "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + - prefix + prefix, ); let entries; if (obj instanceof Map) { @@ -101,7 +101,7 @@ export class ObjectFlattener { v, flatObject, prefix + k + ".", - depth + 1 + depth + 1, ); } } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index d4629591..14c22cb6 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -36,7 +36,7 @@ export class ElementCallOpenTelemetry { private otlpExporter?: OTLPTraceExporter; public readonly rageshakeProcessor?: RageshakeSpanProcessor; - static globalInit(): void { + public static globalInit(): void { const config = Config.get(); // we always enable opentelemetry in general. We only enable the OTLP // collector if a URL is defined (and in future if another setting is defined) @@ -50,18 +50,18 @@ export class ElementCallOpenTelemetry { sharedInstance = new ElementCallOpenTelemetry( config.opentelemetry?.collector_url, - config.rageshake?.submit_url + config.rageshake?.submit_url, ); } } - static get instance(): ElementCallOpenTelemetry { + public static get instance(): ElementCallOpenTelemetry { return sharedInstance; } - constructor( + private constructor( collectorUrl: string | undefined, - rageshakeUrl: string | undefined + rageshakeUrl: string | undefined, ) { // This is how we can make Jaeger show a reasonable service in the dropdown on the left. const providerConfig = { @@ -77,7 +77,7 @@ export class ElementCallOpenTelemetry { url: collectorUrl, }); this._provider.addSpanProcessor( - new SimpleSpanProcessor(this.otlpExporter) + new SimpleSpanProcessor(this.otlpExporter), ); } else { logger.info("OTLP collector disabled"); @@ -93,7 +93,7 @@ export class ElementCallOpenTelemetry { this._tracer = opentelemetry.trace.getTracer( // This is not the serviceName shown in jaeger - "my-element-call-otl-tracer" + "my-element-call-otl-tracer", ); } diff --git a/src/popover/Popover.tsx b/src/popover/Popover.tsx index 02662eb5..36a4ac4a 100644 --- a/src/popover/Popover.tsx +++ b/src/popover/Popover.tsx @@ -40,7 +40,7 @@ export const Popover = forwardRef( shouldCloseOnBlur: true, isDismissable: true, }, - popoverRef + popoverRef, ); return ( @@ -56,5 +56,5 @@ export const Popover = forwardRef(
); - } + }, ); diff --git a/src/popover/PopoverMenu.tsx b/src/popover/PopoverMenu.tsx index faf7fd71..dc38c6d0 100644 --- a/src/popover/PopoverMenu.tsx +++ b/src/popover/PopoverMenu.tsx @@ -43,7 +43,7 @@ export const PopoverMenuTrigger = forwardRef< const { menuTriggerProps, menuProps } = useMenuTrigger( {}, popoverMenuState, - buttonRef + buttonRef, ); const popoverRef = useRef(null); @@ -62,7 +62,7 @@ export const PopoverMenuTrigger = forwardRef< typeof children[1] !== "function" ) { throw new Error( - "PopoverMenu must have two props. The first being a button and the second being a render prop." + "PopoverMenu must have two props. The first being a button and the second being a render prop.", ); } diff --git a/src/profile/useProfile.ts b/src/profile/useProfile.ts index 11c1ec74..eba0420f 100644 --- a/src/profile/useProfile.ts +++ b/src/profile/useProfile.ts @@ -39,7 +39,11 @@ type ProfileSaveCallback = ({ removeAvatar: boolean; }) => Promise; -export function useProfile(client: MatrixClient | undefined) { +interface UseProfile extends ProfileLoadState { + saveProfile: ProfileSaveCallback; +} + +export function useProfile(client: MatrixClient | undefined): UseProfile { const [{ success, loading, displayName, avatarUrl, error }, setState] = useState(() => { let user: User | undefined = undefined; @@ -59,8 +63,8 @@ export function useProfile(client: MatrixClient | undefined) { useEffect(() => { const onChangeUser = ( _event: MatrixEvent | undefined, - { displayName, avatarUrl }: User - ) => { + { displayName, avatarUrl }: User, + ): void => { setState({ success: false, loading: false, @@ -104,9 +108,8 @@ export function useProfile(client: MatrixClient | undefined) { if (removeAvatar) { await client.setAvatarUrl(""); } else if (avatar) { - ({ content_uri: mxcAvatarUrl } = await client.uploadContent( - avatar - )); + ({ content_uri: mxcAvatarUrl } = + await client.uploadContent(avatar)); await client.setAvatarUrl(mxcAvatarUrl); } @@ -131,7 +134,7 @@ export function useProfile(client: MatrixClient | undefined) { logger.error("Client not initialized before calling saveProfile"); } }, - [client] + [client], ); return { diff --git a/src/room/AppSelectionModal.tsx b/src/room/AppSelectionModal.tsx index 8220ffbc..8affa12e 100644 --- a/src/room/AppSelectionModal.tsx +++ b/src/room/AppSelectionModal.tsx @@ -40,14 +40,14 @@ export const AppSelectionModal: FC = ({ roomId }) => { e.stopPropagation(); setOpen(false); }, - [setOpen] + [setOpen], ); const roomSharedKey = useRoomSharedKey(roomId ?? ""); const roomIsEncrypted = useIsRoomE2EE(roomId ?? ""); if (roomIsEncrypted && roomSharedKey === undefined) { logger.error( - "Generating app redirect URL for encrypted room but don't have key available!" + "Generating app redirect URL for encrypted room but don't have key available!", ); } @@ -60,7 +60,7 @@ export const AppSelectionModal: FC = ({ roomId }) => { const url = new URL( roomId === null ? window.location.href - : getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined) + : getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined), ); // Edit the URL to prevent the app selection prompt from appearing a second // time within the app, and to keep the user confined to the current room diff --git a/src/room/CallEndedView.tsx b/src/room/CallEndedView.tsx index 6050b9e9..32b27568 100644 --- a/src/room/CallEndedView.tsx +++ b/src/room/CallEndedView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { FC, FormEventHandler, useCallback, useState } from "react"; +import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Trans, useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; @@ -64,7 +64,7 @@ export const CallEndedView: FC = ({ PosthogAnalytics.instance.eventQualitySurvey.track( endedCallId, feedbackText, - starRating + starRating, ); setSubmitting(true); @@ -83,7 +83,7 @@ export const CallEndedView: FC = ({ }, 1000); }, 1000); }, - [endedCallId, history, isPasswordlessUser, confineToRoom, starRating] + [endedCallId, history, isPasswordlessUser, confineToRoom, starRating], ); const createAccountDialog = isPasswordlessUser && ( @@ -148,7 +148,7 @@ export const CallEndedView: FC = ({ ); - const renderBody = () => { + const renderBody = (): ReactNode => { if (leaveError) { return ( <> diff --git a/src/room/GroupCallLoader.tsx b/src/room/GroupCallLoader.tsx index c61f4ca4..720b6487 100644 --- a/src/room/GroupCallLoader.tsx +++ b/src/room/GroupCallLoader.tsx @@ -47,7 +47,7 @@ export function GroupCallLoader({ ev.preventDefault(); history.push("/"); }, - [history] + [history], ); switch (groupCallState.kind) { @@ -66,7 +66,7 @@ export function GroupCallLoader({ {t("Call not found")} {t( - "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key." + "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.", )} {/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index f834814e..c629a5dc 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHistory } from "react-router-dom"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room, isE2EESupported } from "livekit-client"; @@ -61,14 +61,14 @@ interface Props { rtcSession: MatrixRTCSession; } -export function GroupCallView({ +export const GroupCallView: FC = ({ client, isPasswordlessUser, confineToRoom, preload, hideHeader, rtcSession, -}: Props) { +}) => { const memberships = useMatrixRTCSessionMemberships(rtcSession); const isJoined = useMatrixRTCSessionJoinState(rtcSession); @@ -111,7 +111,7 @@ export function GroupCallView({ // Count each member only once, regardless of how many devices they use const participantCount = useMemo( () => new Set(memberships.map((m) => m.sender!)).size, - [memberships] + [memberships], ); const deviceContext = useMediaDevices(); @@ -125,7 +125,9 @@ export function GroupCallView({ useEffect(() => { if (widget && preload) { // In preload mode, wait for a join action before entering - const onJoin = async (ev: CustomEvent) => { + const onJoin = async ( + ev: CustomEvent, + ): Promise => { // XXX: I think this is broken currently - LiveKit *won't* request // permissions and give you device names unless you specify a kind, but // here we want all kinds of devices. This needs a fix in livekit-client @@ -141,14 +143,14 @@ export function GroupCallView({ const deviceId = await findDeviceByName( audioInput, "audioinput", - devices + devices, ); if (!deviceId) { logger.warn("Unknown audio input: " + audioInput); latestMuteStates.current!.audio.setEnabled?.(false); } else { logger.debug( - `Found audio input ID ${deviceId} for name ${audioInput}` + `Found audio input ID ${deviceId} for name ${audioInput}`, ); latestDevices.current!.audioInput.select(deviceId); latestMuteStates.current!.audio.setEnabled?.(true); @@ -161,14 +163,14 @@ export function GroupCallView({ const deviceId = await findDeviceByName( videoInput, "videoinput", - devices + devices, ); if (!deviceId) { logger.warn("Unknown video input: " + videoInput); latestMuteStates.current!.video.setEnabled?.(false); } else { logger.debug( - `Found video input ID ${deviceId} for name ${videoInput}` + `Found video input ID ${deviceId} for name ${videoInput}`, ); latestDevices.current!.videoInput.select(deviceId); latestMuteStates.current!.video.setEnabled?.(true); @@ -180,7 +182,7 @@ export function GroupCallView({ PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); // we only have room sessions right now, so call ID is the emprty string - we use the room ID PosthogAnalytics.instance.eventCallStarted.track( - rtcSession.room.roomId + rtcSession.room.roomId, ); await Promise.all([ @@ -211,7 +213,7 @@ export function GroupCallView({ PosthogAnalytics.instance.eventCallEnded.track( rtcSession.room.roomId, rtcSession.memberships.length, - sendInstantly + sendInstantly, ); await leaveRTCSession(rtcSession); @@ -235,14 +237,16 @@ export function GroupCallView({ history.push("/"); } }, - [rtcSession, isPasswordlessUser, confineToRoom, history] + [rtcSession, isPasswordlessUser, confineToRoom, history], ); useEffect(() => { if (widget && isJoined) { - const onHangup = async (ev: CustomEvent) => { + const onHangup = async ( + ev: CustomEvent, + ): Promise => { leaveRTCSession(rtcSession); - await widget!.api.transport.reply(ev.detail, {}); + widget!.api.transport.reply(ev.detail, {}); widget!.api.setAlwaysOnScreen(false); }; widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); @@ -256,7 +260,7 @@ export function GroupCallView({ const e2eeConfig = useMemo( () => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined), - [e2eeSharedKey] + [e2eeSharedKey], ); const onReconnect = useCallback(() => { @@ -270,12 +274,12 @@ export function GroupCallView({ const [shareModalOpen, setInviteModalOpen] = useState(false); const onDismissInviteModal = useCallback( () => setInviteModalOpen(false), - [setInviteModalOpen] + [setInviteModalOpen], ); const onShareClickFn = useCallback( () => setInviteModalOpen(true), - [setInviteModalOpen] + [setInviteModalOpen], ); const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null; @@ -284,7 +288,7 @@ export function GroupCallView({ ev.preventDefault(); history.push("/"); }, - [history] + [history], ); const { t } = useTranslation(); @@ -294,7 +298,7 @@ export function GroupCallView({ @@ -305,7 +309,7 @@ export function GroupCallView({ Incompatible Browser {t( - "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117" + "Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117", )} @@ -381,7 +385,7 @@ export function GroupCallView({ client={client} matrixInfo={matrixInfo} muteStates={muteStates} - onEnter={() => enterRTCSession(rtcSession)} + onEnter={(): void => enterRTCSession(rtcSession)} confineToRoom={confineToRoom} hideHeader={hideHeader} participantCount={participantCount} @@ -390,4 +394,4 @@ export function GroupCallView({ ); } -} +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index fbf79438..9dc124f9 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -27,7 +27,16 @@ import { ConnectionState, Room, Track } from "livekit-client"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room"; -import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + FC, + ReactNode, + Ref, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import useMeasure from "react-use-measure"; import { logger } from "matrix-js-sdk/src/logger"; @@ -91,12 +100,12 @@ export interface ActiveCallProps e2eeConfig?: E2EEConfig; } -export function ActiveCall(props: ActiveCallProps) { +export const ActiveCall: FC = (props) => { const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); const { livekitRoom, connState } = useLiveKit( props.muteStates, sfuConfig, - props.e2eeConfig + props.e2eeConfig, ); if (!livekitRoom) { @@ -112,7 +121,7 @@ export function ActiveCall(props: ActiveCallProps) { ); -} +}; export interface InCallViewProps { client: MatrixClient; @@ -128,7 +137,7 @@ export interface InCallViewProps { onShareClick: (() => void) | null; } -export function InCallView({ +export const InCallView: FC = ({ client, matrixInfo, rtcSession, @@ -140,7 +149,7 @@ export function InCallView({ otelGroupCallMembership, connState, onShareClick, -}: InCallViewProps) { +}) => { const { t } = useTranslation(); usePreventScroll(); useWakeLock(); @@ -163,10 +172,10 @@ export function InCallView({ [{ source: Track.Source.ScreenShare, withPlaceholder: false }], { room: livekitRoom, - } + }, ); const { layout, setLayout } = useVideoGridLayout( - screenSharingTracks.length > 0 + screenSharingTracks.length > 0, ); const [showConnectionStats] = useShowConnectionStats(); @@ -179,11 +188,11 @@ export function InCallView({ const toggleMicrophone = useCallback( () => muteStates.audio.setEnabled?.((e) => !e), - [muteStates] + [muteStates], ); const toggleCamera = useCallback( () => muteStates.video.setEnabled?.((e) => !e), - [muteStates] + [muteStates], ); // This function incorrectly assumes that there is a camera and microphone, which is not always the case. @@ -192,7 +201,7 @@ export function InCallView({ containerRef1, toggleMicrophone, toggleCamera, - (muted) => muteStates.audio.setEnabled?.(!muted) + (muted) => muteStates.audio.setEnabled?.(!muted), ); const onLeavePress = useCallback(() => { @@ -204,32 +213,32 @@ export function InCallView({ layout === "grid" ? ElementWidgetActions.TileLayout : ElementWidgetActions.SpotlightLayout, - {} + {}, ); }, [layout]); useEffect(() => { if (widget) { - const onTileLayout = async (ev: CustomEvent) => { + const onTileLayout = (ev: CustomEvent): void => { setLayout("grid"); - await widget!.api.transport.reply(ev.detail, {}); + widget!.api.transport.reply(ev.detail, {}); }; - const onSpotlightLayout = async (ev: CustomEvent) => { + const onSpotlightLayout = (ev: CustomEvent): void => { setLayout("spotlight"); - await widget!.api.transport.reply(ev.detail, {}); + widget!.api.transport.reply(ev.detail, {}); }; widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout); widget.lazyActions.on( ElementWidgetActions.SpotlightLayout, - onSpotlightLayout + onSpotlightLayout, ); return () => { widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout); widget!.lazyActions.off( ElementWidgetActions.SpotlightLayout, - onSpotlightLayout + onSpotlightLayout, ); }; } @@ -252,7 +261,7 @@ export function InCallView({ (noControls ? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null : null), - [fullscreenItem, noControls, items] + [fullscreenItem, noControls, items], ); const Grid = @@ -295,7 +304,7 @@ export function InCallView({ disableAnimations={prefersReducedMotion || isSafari} layoutStates={layoutStates} > - {(props) => ( + {(props): ReactNode => ( setSettingsModalOpen(true), - [setSettingsModalOpen] + [setSettingsModalOpen], ); const closeSettings = useCallback( () => setSettingsModalOpen(false), - [setSettingsModalOpen] + [setSettingsModalOpen], ); const toggleScreensharing = useCallback(async () => { @@ -356,7 +365,7 @@ export function InCallView({ onPress={toggleCamera} disabled={muteStates.video.setEnabled === null} data-testid="incall_videomute" - /> + />, ); if (!reducedControls) { @@ -367,14 +376,18 @@ export function InCallView({ enabled={isScreenShareEnabled} onPress={toggleScreensharing} data-testid="incall_screenshare" - /> + />, ); } buttons.push(); } buttons.push( - + , ); footer = (
@@ -434,11 +447,11 @@ export function InCallView({ />
); -} +}; function findMatrixMember( room: MatrixRoom, - id: string + id: string, ): RoomMember | undefined { if (!id) return undefined; @@ -446,7 +459,7 @@ function findMatrixMember( // must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon if (parts.length < 3) { logger.warn( - "Livekit participants ID doesn't look like a userId:deviceId combination" + "Livekit participants ID doesn't look like a userId:deviceId combination", ); return undefined; } @@ -460,7 +473,7 @@ function findMatrixMember( function useParticipantTiles( livekitRoom: Room, matrixRoom: MatrixRoom, - connState: ECConnectionState + connState: ECConnectionState, ): TileDescriptor[] { const previousTiles = useRef[]>([]); @@ -489,7 +502,7 @@ function useParticipantTiles( // connected, this is fine and we'll be in "all ghosts" mode. if (id !== "" && member === undefined) { logger.warn( - `Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!` + `Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`, ); } allGhosts &&= member === undefined; @@ -533,11 +546,11 @@ function useParticipantTiles( return screenShareTile ? [userMediaTile, screenShareTile] : [userMediaTile]; - } + }, ); PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged( - tiles.length + tiles.length, ); // If every item is a ghost, that probably means we're still connecting and diff --git a/src/room/InviteModal.tsx b/src/room/InviteModal.tsx index 33d6ef97..f9d02f2b 100644 --- a/src/room/InviteModal.tsx +++ b/src/room/InviteModal.tsx @@ -40,7 +40,7 @@ export const InviteModal: FC = ({ room, open, onDismiss }) => { const url = useMemo( () => getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined), - [room, roomSharedKey] + [room, roomSharedKey], ); const [, setCopied] = useClipboard(url); const [toastOpen, setToastOpen] = useState(false); @@ -53,7 +53,7 @@ export const InviteModal: FC = ({ room, open, onDismiss }) => { onDismiss(); setToastOpen(true); }, - [setCopied, onDismiss] + [setCopied, onDismiss], ); return ( diff --git a/src/room/LayoutToggle.tsx b/src/room/LayoutToggle.tsx index 88d9aef7..01738f73 100644 --- a/src/room/LayoutToggle.tsx +++ b/src/room/LayoutToggle.tsx @@ -36,7 +36,7 @@ export const LayoutToggle: FC = ({ layout, setLayout, className }) => { const onChange = useCallback( (e: ChangeEvent) => setLayout(e.target.value as Layout), - [setLayout] + [setLayout], ); const spotlightId = useId(); diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index e49b70cf..7116e74c 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -63,22 +63,22 @@ export const LobbyView: FC = ({ const onAudioPress = useCallback( () => muteStates.audio.setEnabled?.((e) => !e), - [muteStates] + [muteStates], ); const onVideoPress = useCallback( () => muteStates.video.setEnabled?.((e) => !e), - [muteStates] + [muteStates], ); const [settingsModalOpen, setSettingsModalOpen] = useState(false); const openSettings = useCallback( () => setSettingsModalOpen(true), - [setSettingsModalOpen] + [setSettingsModalOpen], ); const closeSettings = useCallback( () => setSettingsModalOpen(false), - [setSettingsModalOpen] + [setSettingsModalOpen], ); const history = useHistory(); diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 8bdc6420..db1fb22a 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -49,18 +49,18 @@ export interface MuteStates { function useMuteState( device: MediaDevice, - enabledByDefault: () => boolean + enabledByDefault: () => boolean, ): MuteState { const [enabled, setEnabled] = useReactiveState( (prev) => device.available.length > 0 && (prev ?? enabledByDefault()), - [device] + [device], ); return useMemo( () => device.available.length === 0 ? deviceUnavailable : { enabled, setEnabled }, - [device, enabled, setEnabled] + [device, enabled, setEnabled], ); } @@ -69,7 +69,7 @@ export function useMuteStates(participantCount: number): MuteStates { const audio = useMuteState( devices.audioInput, - () => participantCount <= MUTE_PARTICIPANT_COUNT + () => participantCount <= MUTE_PARTICIPANT_COUNT, ); const video = useMuteState(devices.videoInput, () => true); diff --git a/src/room/RageshakeRequestModal.tsx b/src/room/RageshakeRequestModal.tsx index ed9acbcb..9bc60cdb 100644 --- a/src/room/RageshakeRequestModal.tsx +++ b/src/room/RageshakeRequestModal.tsx @@ -17,7 +17,7 @@ limitations under the License. import { FC, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Modal, ModalProps } from "../Modal"; +import { Modal, Props as ModalProps } from "../Modal"; import { Button } from "../button"; import { FieldRow, ErrorMessage } from "../input/Input"; import { useSubmitRageshake } from "../settings/submit-rageshake"; @@ -47,13 +47,13 @@ export const RageshakeRequestModal: FC = ({ {t( - "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log." + "Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.", )}