Upgrade eslint-plugin-matrix-org to 1.2.1

This upgrade came with a number of new lints that needed to be fixed across the code base. Primarily: explicit return types on functions, and explicit visibility modifiers on class members.
This commit is contained in:
Robin 2023-09-22 18:05:13 -04:00
parent 444a37224b
commit a7624806b2
88 changed files with 735 additions and 433 deletions

View File

@ -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 = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: [ extends: [
"prettier",
"plugin:matrix-org/react", "plugin:matrix-org/react",
"plugin:matrix-org/a11y", "plugin:matrix-org/a11y",
"plugin:matrix-org/typescript", "plugin:matrix-org/typescript",
"prettier",
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: "latest",
sourceType: "module", sourceType: "module",
project: ["./tsconfig.json"], project: ["./tsconfig.json"],
}, },
@ -15,27 +33,11 @@ module.exports = {
browser: true, browser: true,
node: true, node: true,
}, },
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: { 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
}, },
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",
},
},
],
settings: { settings: {
react: { react: {
version: "detect", version: "detect",

View File

@ -104,11 +104,13 @@
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecate": "^0.8.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1", "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": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-unicorn": "^48.0.1",
"i18next-parser": "^6.6.0", "i18next-parser": "^6.6.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.2.2", "jest": "^29.2.2",
@ -118,6 +120,7 @@
"sass": "^1.42.1", "sass": "^1.42.1",
"storybook-builder-vite": "^0.1.12", "storybook-builder-vite": "^0.1.12",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"typescript-eslint-language-service": "^5.0.5",
"vite": "^4.2.0", "vite": "^4.2.0",
"vite-plugin-html-template": "^1.1.0", "vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^3.2.0" "vite-plugin-svgr": "^3.2.0"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Suspense, useEffect, useState } from "react"; import { FC, Suspense, useEffect, useState } from "react";
import { import {
BrowserRouter as Router, BrowserRouter as Router,
Switch, Switch,
@ -43,7 +43,7 @@ interface BackgroundProviderProps {
children: JSX.Element; children: JSX.Element;
} }
const BackgroundProvider = ({ children }: BackgroundProviderProps) => { const BackgroundProvider: FC<BackgroundProviderProps> = ({ children }) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
useEffect(() => { useEffect(() => {
@ -63,7 +63,7 @@ interface AppProps {
history: History; history: History;
} }
export default function App({ history }: AppProps) { export const App: FC<AppProps> = ({ history }) => {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
@ -116,4 +116,4 @@ export default function App({ history }: AppProps) {
</BackgroundProvider> </BackgroundProvider>
</Router> </Router>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode } from "react"; import { FC, ReactNode } from "react";
import styles from "./Banner.module.css"; import styles from "./Banner.module.css";
@ -22,6 +22,6 @@ interface Props {
children: ReactNode; children: ReactNode;
} }
export const Banner = ({ children }: Props) => { export const Banner: FC<Props> = ({ children }) => {
return <div className={styles.banner}>{children}</div>; return <div className={styles.banner}>{children}</div>;
}; };

View File

@ -82,7 +82,8 @@ export type SetClientParams = {
const ClientContext = createContext<ClientState | undefined>(undefined); const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = () => useContext(ClientContext); export const useClientState = (): ClientState | undefined =>
useContext(ClientContext);
export function useClient(): { export function useClient(): {
client?: MatrixClient; client?: MatrixClient;
@ -408,8 +409,8 @@ export interface Session {
tempPassword?: string; tempPassword?: string;
} }
const clearSession = () => localStorage.removeItem("matrix-auth-store"); const clearSession = (): void => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) => const saveSession = (s: Session): void =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s)); localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => { const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store"); const data = localStorage.getItem("matrix-auth-store");
@ -423,4 +424,5 @@ const loadSession = (): Session | undefined => {
const clientIsDisconnected = ( const clientIsDisconnected = (
syncState: SyncState, syncState: SyncState,
syncData?: ISyncStateData syncData?: ISyncStateData
) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError"; ): boolean =>
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";

View File

@ -15,22 +15,22 @@ limitations under the License.
*/ */
import classNames from "classnames"; import classNames from "classnames";
import { HTMLAttributes, ReactNode } from "react"; import { FC, HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css"; import styles from "./DisconnectedBanner.module.css";
import { ValidClientState, useClientState } from "./ClientContext"; import { ValidClientState, useClientState } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> { interface Props extends HTMLAttributes<HTMLElement> {
children?: ReactNode; children?: ReactNode;
className?: string; className?: string;
} }
export function DisconnectedBanner({ export const DisconnectedBanner: FC<Props> = ({
children, children,
className, className,
...rest ...rest
}: DisconnectedBannerProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const clientState = useClientState(); const clientState = useClientState();
let shouldShowBanner = false; let shouldShowBanner = false;
@ -50,4 +50,4 @@ export function DisconnectedBanner({
)} )}
</> </>
); );
} };

View File

@ -15,13 +15,14 @@ limitations under the License.
*/ */
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { FC } from "react";
import { Banner } from "./Banner"; import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css"; import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg"; import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { useEnableE2EE } from "./settings/useSetting"; import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => { export const E2EEBanner: FC = () => {
const [e2eeEnabled] = useEnableE2EE(); const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null; if (e2eeEnabled) return null;

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { HTMLAttributes } from "react"; import { FC, HTMLAttributes } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -30,14 +30,14 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
size?: Size | number; size?: Size | number;
} }
export function Facepile({ export const Facepile: FC<Props> = ({
className, className,
client, client,
members, members,
max = 3, max = 3,
size = Size.XS, size = Size.XS,
...rest ...rest
}: Props) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const displayedMembers = members.slice(0, max); const displayedMembers = members.slice(0, max);
@ -63,4 +63,4 @@ export function Facepile({
})} })}
</AvatarStack> </AvatarStack>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode, useCallback, useEffect } from "react"; import { FC, ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@ -32,7 +32,10 @@ interface FullScreenViewProps {
children: ReactNode; children: ReactNode;
} }
export function FullScreenView({ className, children }: FullScreenViewProps) { export const FullScreenView: FC<FullScreenViewProps> = ({
className,
children,
}) => {
return ( return (
<div className={classNames(styles.page, className)}> <div className={classNames(styles.page, className)}>
<Header> <Header>
@ -46,13 +49,13 @@ export function FullScreenView({ className, children }: FullScreenViewProps) {
</div> </div>
</div> </div>
); );
} };
interface ErrorViewProps { interface ErrorViewProps {
error: Error; error: Error;
} }
export function ErrorView({ error }: ErrorViewProps) { export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
@ -95,9 +98,9 @@ export function ErrorView({ error }: ErrorViewProps) {
)} )}
</FullScreenView> </FullScreenView>
); );
} };
export function CrashView() { export const CrashView: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const onReload = useCallback(() => { const onReload = useCallback(() => {
@ -126,9 +129,9 @@ export function CrashView() {
</Button> </Button>
</FullScreenView> </FullScreenView>
); );
} };
export function LoadingView() { export const LoadingView: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -136,4 +139,4 @@ export function LoadingView() {
<h1>{t("Loading…")}</h1> <h1>{t("Loading…")}</h1>
</FullScreenView> </FullScreenView>
); );
} };

View File

@ -33,13 +33,13 @@ interface HeaderProps extends HTMLAttributes<HTMLElement> {
className?: string; className?: string;
} }
export function Header({ children, className, ...rest }: HeaderProps) { export const Header: FC<HeaderProps> = ({ children, className, ...rest }) => {
return ( return (
<header className={classNames(styles.header, className)} {...rest}> <header className={classNames(styles.header, className)} {...rest}>
{children} {children}
</header> </header>
); );
} };
interface LeftNavProps extends HTMLAttributes<HTMLElement> { interface LeftNavProps extends HTMLAttributes<HTMLElement> {
children: ReactNode; children: ReactNode;
@ -47,12 +47,12 @@ interface LeftNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean; hideMobile?: boolean;
} }
export function LeftNav({ export const LeftNav: FC<LeftNavProps> = ({
children, children,
className, className,
hideMobile, hideMobile,
...rest ...rest
}: LeftNavProps) { }) => {
return ( return (
<div <div
className={classNames( className={classNames(
@ -66,7 +66,7 @@ export function LeftNav({
{children} {children}
</div> </div>
); );
} };
interface RightNavProps extends HTMLAttributes<HTMLElement> { interface RightNavProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode; children?: ReactNode;
@ -74,12 +74,12 @@ interface RightNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean; hideMobile?: boolean;
} }
export function RightNav({ export const RightNav: FC<RightNavProps> = ({
children, children,
className, className,
hideMobile, hideMobile,
...rest ...rest
}: RightNavProps) { }) => {
return ( return (
<div <div
className={classNames( className={classNames(
@ -93,13 +93,13 @@ export function RightNav({
{children} {children}
</div> </div>
); );
} };
interface HeaderLogoProps { interface HeaderLogoProps {
className?: string; className?: string;
} }
export function HeaderLogo({ className }: HeaderLogoProps) { export const HeaderLogo: FC<HeaderLogoProps> = ({ className }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -111,7 +111,7 @@ export function HeaderLogo({ className }: HeaderLogoProps) {
<Logo /> <Logo />
</Link> </Link>
); );
} };
interface RoomHeaderInfoProps { interface RoomHeaderInfoProps {
id: string; id: string;

View File

@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
import { ListState } from "@react-stately/list"; import { ListState } from "@react-stately/list";
import { Node } from "@react-types/shared"; import { Node } from "@react-types/shared";
@ -35,7 +41,7 @@ export function ListBox<T>({
className, className,
listBoxRef, listBoxRef,
...rest ...rest
}: ListBoxProps<T>) { }: ListBoxProps<T>): ReactNode {
const ref = useRef<HTMLUListElement>(null); const ref = useRef<HTMLUListElement>(null);
const listRef = listBoxRef ?? ref; const listRef = listBoxRef ?? ref;
@ -66,7 +72,7 @@ interface OptionProps<T> {
item: Node<T>; item: Node<T>;
} }
function Option<T>({ item, state, className }: OptionProps<T>) { function Option<T>({ item, state, className }: OptionProps<T>): ReactNode {
const ref = useRef(null); const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption( const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key }, { key: item.key },

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
import { TreeState, useTreeState } from "@react-stately/tree"; import { TreeState, useTreeState } from "@react-stately/tree";
import { mergeProps } from "@react-aria/utils"; import { mergeProps } from "@react-aria/utils";
@ -37,7 +37,7 @@ export function Menu<T extends object>({
onClose, onClose,
label, label,
...rest ...rest
}: MenuProps<T>) { }: MenuProps<T>): ReactNode {
const state = useTreeState<T>({ ...rest, selectionMode: "none" }); const state = useTreeState<T>({ ...rest, selectionMode: "none" });
const menuRef = useRef(null); const menuRef = useRef(null);
const { menuProps } = useMenu<T>(rest, state, menuRef); const { menuProps } = useMenu<T>(rest, state, menuRef);
@ -68,7 +68,12 @@ interface MenuItemProps<T> {
onClose: () => void; onClose: () => void;
} }
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) { function MenuItem<T>({
item,
state,
onAction,
onClose,
}: MenuItemProps<T>): ReactNode {
const ref = useRef(null); const ref = useRef(null);
const { menuItemProps } = useMenuItem( const { menuItemProps } = useMenuItem(
{ {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ReactNode, useCallback } from "react"; import { FC, ReactNode, useCallback } from "react";
import { AriaDialogProps } from "@react-types/dialog"; import { AriaDialogProps } from "@react-types/dialog";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@ -36,7 +36,7 @@ import { useMediaQuery } from "./useMediaQuery";
import { Glass } from "./Glass"; import { Glass } from "./Glass";
// TODO: Support tabs // TODO: Support tabs
export interface ModalProps extends AriaDialogProps { export interface Props extends AriaDialogProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
className?: string; className?: string;
@ -58,14 +58,14 @@ export interface ModalProps extends AriaDialogProps {
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices, * A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
* and a dialog box on desktop. * and a dialog box on desktop.
*/ */
export function Modal({ export const Modal: FC<Props> = ({
title, title,
children, children,
className, className,
open, open,
onDismiss, onDismiss,
...rest ...rest
}: ModalProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Empirically, Chrome on Android can end up not matching (hover: none), but // Empirically, Chrome on Android can end up not matching (hover: none), but
// still matching (pointer: coarse) :/ // still matching (pointer: coarse) :/
@ -140,4 +140,4 @@ export function Modal({
</DialogRoot> </DialogRoot>
); );
} }
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@ -30,7 +30,7 @@ interface DebugLog {
remoteUserIds: string[]; remoteUserIds: string[];
} }
export function SequenceDiagramViewerPage() { export const SequenceDiagramViewerPage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
usePageTitle(t("Inspector")); usePageTitle(t("Inspector"));
@ -69,4 +69,4 @@ export function SequenceDiagramViewerPage() {
)} )}
</div> </div>
); );
} };

View File

@ -37,5 +37,7 @@ class TranslatedErrorImpl extends TranslatedError {}
// i18next-parser can't detect calls to a constructor, so we expose a bare // i18next-parser can't detect calls to a constructor, so we expose a bare
// function instead // function instead
export const translatedError = (messageKey: string, t: typeof i18n.t) => export const translatedError = (
new TranslatedErrorImpl(messageKey, t); messageKey: string,
t: typeof i18n.t
): TranslatedError => new TranslatedErrorImpl(messageKey, t);

View File

@ -134,7 +134,7 @@ class ParamParser {
private fragmentParams: URLSearchParams; private fragmentParams: URLSearchParams;
private queryParams: URLSearchParams; private queryParams: URLSearchParams;
constructor(search: string, hash: string) { public constructor(search: string, hash: string) {
this.queryParams = new URLSearchParams(search); this.queryParams = new URLSearchParams(search);
const fragmentQueryStart = hash.indexOf("?"); const fragmentQueryStart = hash.indexOf("?");
@ -146,18 +146,18 @@ class ParamParser {
// Normally, URL params should be encoded in the fragment so as to avoid // 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 // leaking them to the server. However, we also check the normal query
// string for backwards compatibility with versions that only used that. // 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); return this.fragmentParams.get(name) ?? this.queryParams.get(name);
} }
getAllParams(name: string): string[] { public getAllParams(name: string): string[] {
return [ return [
...this.fragmentParams.getAll(name), ...this.fragmentParams.getAll(name),
...this.queryParams.getAll(name), ...this.queryParams.getAll(name),
]; ];
} }
getFlagParam(name: string, defaultValue = false): boolean { public getFlagParam(name: string, defaultValue = false): boolean {
const param = this.getParam(name); const param = this.getParam(name);
return param === null ? defaultValue : param !== "false"; return param === null ? defaultValue : param !== "false";
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useMemo } from "react"; import { FC, ReactNode, useCallback, useMemo } from "react";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -31,7 +31,7 @@ import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
import { Body } from "./typography/Typography"; import { Body } from "./typography/Typography";
import styles from "./UserMenu.module.css"; import styles from "./UserMenu.module.css";
interface UserMenuProps { interface Props {
preventNavigation: boolean; preventNavigation: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
isPasswordlessUser: boolean; isPasswordlessUser: boolean;
@ -41,7 +41,7 @@ interface UserMenuProps {
onAction: (value: string) => void; onAction: (value: string) => void;
} }
export function UserMenu({ export const UserMenu: FC<Props> = ({
preventNavigation, preventNavigation,
isAuthenticated, isAuthenticated,
isPasswordlessUser, isPasswordlessUser,
@ -49,7 +49,7 @@ export function UserMenu({
displayName, displayName,
avatarUrl, avatarUrl,
onAction, onAction,
}: UserMenuProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
@ -123,7 +123,7 @@ export function UserMenu({
</TooltipTrigger> </TooltipTrigger>
{ {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(props: any) => ( (props: any): ReactNode => (
<Menu {...props} label={t("User menu")} onAction={onAction}> <Menu {...props} label={t("User menu")} onAction={onAction}>
{items.map(({ key, icon: Icon, label, dataTestid }) => ( {items.map(({ key, icon: Icon, label, dataTestid }) => (
<Item key={key} textValue={label}> <Item key={key} textValue={label}>
@ -141,4 +141,4 @@ export function UserMenu({
} }
</PopoverMenuTrigger> </PopoverMenuTrigger>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { useClientLegacy } from "./ClientContext"; import { useClientLegacy } from "./ClientContext";
@ -26,7 +26,7 @@ interface Props {
preventNavigation?: boolean; preventNavigation?: boolean;
} }
export function UserMenuContainer({ preventNavigation = false }: Props) { export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const { client, logout, authenticated, passwordlessUser } = useClientLegacy(); const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
@ -83,4 +83,4 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
)} )}
</> </>
); );
} };

View File

@ -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 { FC } from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";

View File

@ -117,7 +117,7 @@ export class PosthogAnalytics {
return this.internalInstance; return this.internalInstance;
} }
constructor(private readonly posthog: PostHog) { private constructor(private readonly posthog: PostHog) {
const posthogConfig: PosthogSettings = { const posthogConfig: PosthogSettings = {
project_api_key: Config.get().posthog?.api_key, project_api_key: Config.get().posthog?.api_key,
api_host: Config.get().posthog?.api_host, api_host: Config.get().posthog?.api_host,
@ -183,7 +183,7 @@ export class PosthogAnalytics {
return properties; return properties;
}; };
private registerSuperProperties(properties: Properties) { private registerSuperProperties(properties: Properties): void {
if (this.enabled) { if (this.enabled) {
this.posthog.register(properties); this.posthog.register(properties);
} }
@ -202,7 +202,7 @@ export class PosthogAnalytics {
eventName: string, eventName: string,
properties: Properties, properties: Properties,
options?: CaptureOptions options?: CaptureOptions
) { ): void {
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
@ -213,7 +213,7 @@ export class PosthogAnalytics {
return this.enabled; return this.enabled;
} }
setAnonymity(anonymity: Anonymity): void { private setAnonymity(anonymity: Anonymity): void {
// Update this.anonymity. // Update this.anonymity.
// To update the anonymity typically you want to call updateAnonymityFromSettings // To update the anonymity typically you want to call updateAnonymityFromSettings
// to ensure this value is in step with the user's settings. // to ensure this value is in step with the user's settings.
@ -236,7 +236,9 @@ export class PosthogAnalytics {
.join(""); .join("");
} }
private async identifyUser(analyticsIdGenerator: () => string) { private async identifyUser(
analyticsIdGenerator: () => string
): Promise<void> {
if (this.anonymity == Anonymity.Pseudonymous && this.enabled) { 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 // 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. // different devices to send the same ID.
@ -271,7 +273,7 @@ export class PosthogAnalytics {
} }
} }
async getAnalyticsId() { private async getAnalyticsId(): Promise<string | null> {
const client: MatrixClient = window.matrixclient; const client: MatrixClient = window.matrixclient;
let accountAnalyticsId; let accountAnalyticsId;
if (widget) { if (widget) {
@ -291,7 +293,9 @@ export class PosthogAnalytics {
return null; return null;
} }
async hashedEcAnalyticsId(accountAnalyticsId: string): Promise<string> { private async hashedEcAnalyticsId(
accountAnalyticsId: string
): Promise<string> {
const client: MatrixClient = window.matrixclient; const client: MatrixClient = window.matrixclient;
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId(); const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
const bufferForPosthogId = await crypto.subtle.digest( const bufferForPosthogId = await crypto.subtle.digest(
@ -304,7 +308,7 @@ export class PosthogAnalytics {
.join(""); .join("");
} }
async setAccountAnalyticsId(analyticsID: string) { private async setAccountAnalyticsId(analyticsID: string): Promise<void> {
if (!widget) { if (!widget) {
const client = window.matrixclient; const client = window.matrixclient;
@ -335,7 +339,7 @@ export class PosthogAnalytics {
this.updateAnonymityAndIdentifyUser(optInAnalytics); this.updateAnonymityAndIdentifyUser(optInAnalytics);
} }
private updateSuperProperties() { private updateSuperProperties(): void {
// Update super properties in posthog with our platform (app version, platform). // Update super properties in posthog with our platform (app version, platform).
// These properties will be subsequently passed in every event. // These properties will be subsequently passed in every event.
// //

View File

@ -36,18 +36,22 @@ export class CallEndedTracker {
maxParticipantsCount: 0, maxParticipantsCount: 0,
}; };
cacheStartCall(time: Date) { public cacheStartCall(time: Date): void {
this.cache.startTime = time; this.cache.startTime = time;
} }
cacheParticipantCountChanged(count: number) { public cacheParticipantCountChanged(count: number): void {
this.cache.maxParticipantsCount = Math.max( this.cache.maxParticipantsCount = Math.max(
count, 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<CallEnded>( PosthogAnalytics.instance.trackEvent<CallEnded>(
{ {
eventName: "CallEnded", eventName: "CallEnded",
@ -67,7 +71,7 @@ interface CallStarted extends IPosthogEvent {
} }
export class CallStartedTracker { export class CallStartedTracker {
track(callId: string) { public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<CallStarted>({ PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted", eventName: "CallStarted",
callId: callId, callId: callId,
@ -86,19 +90,19 @@ export class SignupTracker {
signupEnd: new Date(0), signupEnd: new Date(0),
}; };
cacheSignupStart(time: Date) { public cacheSignupStart(time: Date): void {
this.cache.signupStart = time; this.cache.signupStart = time;
} }
getSignupEndTime() { public getSignupEndTime(): Date {
return this.cache.signupEnd; return this.cache.signupEnd;
} }
cacheSignupEnd(time: Date) { public cacheSignupEnd(time: Date): void {
this.cache.signupEnd = time; this.cache.signupEnd = time;
} }
track() { public track(): void {
PosthogAnalytics.instance.trackEvent<Signup>({ PosthogAnalytics.instance.trackEvent<Signup>({
eventName: "Signup", eventName: "Signup",
signupDuration: Date.now() - this.cache.signupStart.getTime(), signupDuration: Date.now() - this.cache.signupStart.getTime(),
@ -112,7 +116,7 @@ interface Login extends IPosthogEvent {
} }
export class LoginTracker { export class LoginTracker {
track() { public track(): void {
PosthogAnalytics.instance.trackEvent<Login>({ PosthogAnalytics.instance.trackEvent<Login>({
eventName: "Login", eventName: "Login",
}); });
@ -127,7 +131,7 @@ interface MuteMicrophone {
} }
export class MuteMicrophoneTracker { export class MuteMicrophoneTracker {
track(targetIsMute: boolean, callId: string) { public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({ PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
eventName: "MuteMicrophone", eventName: "MuteMicrophone",
targetMuteState: targetIsMute ? "mute" : "unmute", targetMuteState: targetIsMute ? "mute" : "unmute",
@ -143,7 +147,7 @@ interface MuteCamera {
} }
export class MuteCameraTracker { export class MuteCameraTracker {
track(targetIsMute: boolean, callId: string) { public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteCamera>({ PosthogAnalytics.instance.trackEvent<MuteCamera>({
eventName: "MuteCamera", eventName: "MuteCamera",
targetMuteState: targetIsMute ? "mute" : "unmute", targetMuteState: targetIsMute ? "mute" : "unmute",
@ -158,7 +162,7 @@ interface UndecryptableToDeviceEvent {
} }
export class UndecryptableToDeviceEventTracker { export class UndecryptableToDeviceEventTracker {
track(callId: string) { public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({ PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
eventName: "UndecryptableToDeviceEvent", eventName: "UndecryptableToDeviceEvent",
callId, callId,
@ -174,7 +178,7 @@ interface QualitySurveyEvent {
} }
export class QualitySurveyEventTracker { export class QualitySurveyEventTracker {
track(callId: string, feedbackText: string, stars: number) { public track(callId: string, feedbackText: string, stars: number): void {
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({ PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
eventName: "QualitySurvey", eventName: "QualitySurvey",
callId, callId,
@ -190,7 +194,7 @@ interface CallDisconnectedEvent {
} }
export class CallDisconnectedEventTracker { export class CallDisconnectedEventTracker {
track(reason?: DisconnectReason) { public track(reason?: DisconnectReason): void {
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({ PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
eventName: "CallDisconnected", eventName: "CallDisconnected",
reason, reason,

View File

@ -39,9 +39,9 @@ const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
* Span processor that extracts certain metrics from spans to send to PostHog * Span processor that extracts certain metrics from spans to send to PostHog
*/ */
export class PosthogSpanProcessor implements SpanProcessor { export class PosthogSpanProcessor implements SpanProcessor {
async forceFlush(): Promise<void> {} public async forceFlush(): Promise<void> {}
onStart(span: Span): void { public onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing // Hack: Yield to allow attributes to be set before processing
Promise.resolve().then(() => { Promise.resolve().then(() => {
switch (span.name) { switch (span.name) {
@ -55,7 +55,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
}); });
} }
onEnd(span: ReadableSpan): void { public onEnd(span: ReadableSpan): void {
switch (span.name) { switch (span.name) {
case "matrix.groupCallMembership": case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span); this.onGroupCallMembershipEnd(span);
@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
/** /**
* Shutdown the processor. * Shutdown the processor.
*/ */
shutdown(): Promise<void> { public shutdown(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
} }

View File

@ -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 { hrTimeToMicroseconds } from "@opentelemetry/core";
import { import {
SpanProcessor, SpanProcessor,
@ -6,7 +22,21 @@ import {
Span, Span,
} from "@opentelemetry/sdk-trace-base"; } 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]) => ({ Object.entries(attr).map(([key, value]) => ({
key, key,
type: typeof value, type: typeof value,
@ -20,13 +50,13 @@ const dumpAttributes = (attr: Attributes) =>
export class RageshakeSpanProcessor implements SpanProcessor { export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = []; private readonly spans: ReadableSpan[] = [];
async forceFlush(): Promise<void> {} public async forceFlush(): Promise<void> {}
onStart(span: Span): void { public onStart(span: Span): void {
this.spans.push(span); this.spans.push(span);
} }
onEnd(): void {} public onEnd(): void {}
/** /**
* Dumps the spans collected so far as Jaeger-compatible JSON. * Dumps the spans collected so far as Jaeger-compatible JSON.
@ -110,5 +140,5 @@ export class RageshakeSpanProcessor implements SpanProcessor {
}); });
} }
async shutdown(): Promise<void> {} public async shutdown(): Promise<void> {}
} }

View File

@ -68,7 +68,7 @@ export const RegisterPage: FC = () => {
if (password !== passwordConfirmation) return; if (password !== passwordConfirmation) return;
const submit = async () => { const submit = async (): Promise<void> => {
setRegistering(true); setRegistering(true);
const recaptchaResponse = await execute(); const recaptchaResponse = await execute();
@ -183,7 +183,7 @@ export const RegisterPage: FC = () => {
required required
name="password" name="password"
type="password" type="password"
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setPassword(e.target.value) setPassword(e.target.value)
} }
value={password} value={password}
@ -197,7 +197,7 @@ export const RegisterPage: FC = () => {
required required
type="password" type="password"
name="passwordConfirmation" name="passwordConfirmation"
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setPasswordConfirmation(e.target.value) setPasswordConfirmation(e.target.value)
} }
value={passwordConfirmation} value={passwordConfirmation}

View File

@ -21,8 +21,12 @@ import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
import { initClient } from "../matrix-utils"; import { initClient } from "../matrix-utils";
import { Session } from "../ClientContext"; import { Session } from "../ClientContext";
export const useInteractiveLogin = () => export function useInteractiveLogin(): (
useCallback< homeserver: string,
username: string,
password: string
) => Promise<[MatrixClient, Session]> {
return useCallback<
( (
homeserver: string, homeserver: string,
username: string, username: string,
@ -41,8 +45,8 @@ export const useInteractiveLogin = () =>
}, },
password, password,
}), }),
stateUpdated: (...args) => {}, stateUpdated: (): void => {},
requestEmailToken: (...args): Promise<{ sid: string }> => { requestEmailToken: (): Promise<{ sid: string }> => {
return Promise.resolve({ sid: "" }); return Promise.resolve({ sid: "" });
}, },
}); });
@ -69,6 +73,6 @@ export const useInteractiveLogin = () =>
false false
); );
/* eslint-enable camelcase */ /* eslint-enable camelcase */
return [client, session]; return [client, session];
}, []); }, []);
}

View File

@ -72,7 +72,7 @@ export const useInteractiveRegistration = (): {
password, password,
auth: auth || undefined, auth: auth || undefined,
}), }),
stateUpdated: (nextStage, status) => { stateUpdated: (nextStage, status): void => {
if (status.error) { if (status.error) {
throw new Error(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" }); return Promise.resolve({ sid: "dummy" });
}, },
}); });

View File

@ -34,7 +34,11 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void; reject: (error: Error) => void;
} }
export const useRecaptcha = (sitekey?: string) => { export function useRecaptcha(sitekey?: string): {
execute: () => Promise<string>;
reset: () => void;
recaptchaId: string;
} {
const { t } = useTranslation(); const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16)); const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>(); const promiseRef = useRef<RecaptchaPromiseRef>();
@ -42,7 +46,7 @@ export const useRecaptcha = (sitekey?: string) => {
useEffect(() => { useEffect(() => {
if (!sitekey) return; if (!sitekey) return;
const onRecaptchaLoaded = () => { const onRecaptchaLoaded = (): void => {
if (!document.getElementById(recaptchaId)) return; if (!document.getElementById(recaptchaId)) return;
window.grecaptcha.render(recaptchaId, { window.grecaptcha.render(recaptchaId, {
@ -90,11 +94,11 @@ export const useRecaptcha = (sitekey?: string) => {
}); });
promiseRef.current = { promiseRef.current = {
resolve: (value) => { resolve: (value): void => {
resolve(value); resolve(value);
observer.disconnect(); observer.disconnect();
}, },
reject: (error) => { reject: (error): void => {
reject(error); reject(error);
observer.disconnect(); observer.disconnect();
}, },
@ -119,4 +123,4 @@ export const useRecaptcha = (sitekey?: string) => {
}, []); }, []);
return { execute, reset, recaptchaId }; return { execute, reset, recaptchaId };
}; }

View File

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { forwardRef } from "react"; import { FC, forwardRef } from "react";
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import classNames from "classnames"; import classNames from "classnames";
import { useButton } from "@react-aria/button"; import { useButton } from "@react-aria/button";
@ -135,14 +135,11 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
} }
); );
export function MicButton({ export const MicButton: FC<{
muted,
...rest
}: {
muted: boolean; muted: boolean;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ muted, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon; const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
const label = muted ? t("Unmute microphone") : t("Mute microphone"); const label = muted ? t("Unmute microphone") : t("Mute microphone");
@ -154,16 +151,13 @@ export function MicButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function VideoButton({ export const VideoButton: FC<{
muted,
...rest
}: {
muted: boolean; muted: boolean;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ muted, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = muted ? VideoCallOffIcon : VideoCallIcon; const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
const label = muted ? t("Start video") : t("Stop video"); const label = muted ? t("Start video") : t("Stop video");
@ -175,18 +169,14 @@ export function VideoButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function ScreenshareButton({ export const ScreenshareButton: FC<{
enabled,
className,
...rest
}: {
enabled: boolean; enabled: boolean;
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ enabled, className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const label = enabled ? t("Sharing screen") : t("Share screen"); const label = enabled ? t("Sharing screen") : t("Share screen");
@ -197,16 +187,13 @@ export function ScreenshareButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function HangupButton({ export const HangupButton: FC<{
className,
...rest
}: {
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -220,16 +207,13 @@ export function HangupButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
export function SettingsButton({ export const SettingsButton: FC<{
className,
...rest
}: {
className?: string; className?: string;
// TODO: add all props for <Button> // TODO: add all props for <Button>
[index: string]: unknown; [index: string]: unknown;
}) { }> = ({ className, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -239,7 +223,7 @@ export function SettingsButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
interface AudioButtonProps extends Omit<Props, "variant"> { interface AudioButtonProps extends Omit<Props, "variant"> {
/** /**
@ -248,7 +232,7 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
volume: number; volume: number;
} }
export function AudioButton({ volume, ...rest }: AudioButtonProps) { export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -258,16 +242,16 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) {
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };
interface FullscreenButtonProps extends Omit<Props, "variant"> { interface FullscreenButtonProps extends Omit<Props, "variant"> {
fullscreen?: boolean; fullscreen?: boolean;
} }
export function FullscreenButton({ export const FullscreenButton: FC<FullscreenButtonProps> = ({
fullscreen, fullscreen,
...rest ...rest
}: FullscreenButtonProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const Icon = fullscreen ? FullscreenExit : Fullscreen; const Icon = fullscreen ? FullscreenExit : Fullscreen;
const label = fullscreen ? t("Exit full screen") : t("Full screen"); const label = fullscreen ? t("Exit full screen") : t("Full screen");
@ -279,4 +263,4 @@ export function FullscreenButton({
</Button> </Button>
</Tooltip> </Tooltip>
); );
} };

View File

@ -16,6 +16,7 @@ limitations under the License.
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useClipboard from "react-use-clipboard"; import useClipboard from "react-use-clipboard";
import { FC } from "react";
import { ReactComponent as CheckIcon } from "../icons/Check.svg"; import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import { ReactComponent as CopyIcon } from "../icons/Copy.svg"; import { ReactComponent as CopyIcon } from "../icons/Copy.svg";
@ -28,14 +29,15 @@ interface Props {
variant?: ButtonVariant; variant?: ButtonVariant;
copiedMessage?: string; copiedMessage?: string;
} }
export function CopyButton({
export const CopyButton: FC<Props> = ({
value, value,
children, children,
className, className,
variant, variant,
copiedMessage, copiedMessage,
...rest ...rest
}: Props) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 }); const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
@ -62,4 +64,4 @@ export function CopyButton({
)} )}
</Button> </Button>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { HTMLAttributes } from "react"; import { FC, HTMLAttributes } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import * as H from "history"; import * as H from "history";
@ -34,14 +34,14 @@ interface Props extends HTMLAttributes<HTMLAnchorElement> {
className?: string; className?: string;
} }
export function LinkButton({ export const LinkButton: FC<Props> = ({
children, children,
to, to,
size, size,
variant, variant,
className, className,
...rest ...rest
}: Props) { }) => {
return ( return (
<Link <Link
className={classNames( className={classNames(
@ -55,4 +55,4 @@ export function LinkButton({
{children} {children}
</Link> </Link>
); );
} };

View File

@ -18,6 +18,7 @@ import { Link } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { FC } from "react";
import { CopyButton } from "../button"; import { CopyButton } from "../button";
import { Avatar, Size } from "../Avatar"; import { Avatar, Size } from "../Avatar";
@ -31,7 +32,8 @@ interface CallListProps {
rooms: GroupCallRoom[]; rooms: GroupCallRoom[];
client: MatrixClient; client: MatrixClient;
} }
export function CallList({ rooms, client }: CallListProps) {
export const CallList: FC<CallListProps> = ({ rooms, client }) => {
return ( return (
<> <>
<div className={styles.callList}> <div className={styles.callList}>
@ -54,7 +56,7 @@ export function CallList({ rooms, client }: CallListProps) {
</div> </div>
</> </>
); );
} };
interface CallTileProps { interface CallTileProps {
name: string; name: string;
avatarUrl: string; avatarUrl: string;
@ -62,7 +64,8 @@ interface CallTileProps {
participants: RoomMember[]; participants: RoomMember[];
client: MatrixClient; client: MatrixClient;
} }
function CallTile({ name, avatarUrl, room }: CallTileProps) {
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
const roomSharedKey = useRoomSharedKey(room.roomId); const roomSharedKey = useRoomSharedKey(room.roomId);
return ( return (
@ -92,4 +95,4 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) {
/> />
</div> </div>
); );
} };

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC } from "react";
import { useClientState } from "../ClientContext"; import { useClientState } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
@ -22,7 +23,7 @@ import { UnauthenticatedView } from "./UnauthenticatedView";
import { RegisteredView } from "./RegisteredView"; import { RegisteredView } from "./RegisteredView";
import { usePageTitle } from "../usePageTitle"; import { usePageTitle } from "../usePageTitle";
export function HomePage() { export const HomePage: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
usePageTitle(t("Home")); usePageTitle(t("Home"));
@ -39,4 +40,4 @@ export function HomePage() {
<UnauthenticatedView /> <UnauthenticatedView />
); );
} }
} };

View File

@ -16,6 +16,7 @@ limitations under the License.
import { PressEvent } from "@react-types/shared"; import { PressEvent } from "@react-types/shared";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FC } from "react";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
@ -28,7 +29,11 @@ interface Props {
onJoin: (e: PressEvent) => void; onJoin: (e: PressEvent) => void;
} }
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) { export const JoinExistingCallModal: FC<Props> = ({
onJoin,
open,
onDismiss,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -42,4 +47,4 @@ export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
</FieldRow> </FieldRow>
</Modal> </Modal>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
@ -48,7 +48,7 @@ interface Props {
client: MatrixClient; client: MatrixClient;
} }
export function RegisteredView({ client }: Props) { export const RegisteredView: FC<Props> = ({ client }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics(); const [optInAnalytics] = useOptInAnalytics();
@ -72,7 +72,7 @@ export function RegisteredView({ client }: Props) {
? sanitiseRoomNameInput(roomNameData) ? sanitiseRoomNameInput(roomNameData)
: ""; : "";
async function submit() { async function submit(): Promise<void> {
setError(undefined); setError(undefined);
setLoading(true); setLoading(true);
@ -176,4 +176,4 @@ export function RegisteredView({ client }: Props) {
/> />
</> </>
); );
} };

View File

@ -73,7 +73,7 @@ export const UnauthenticatedView: FC = () => {
const roomName = sanitiseRoomNameInput(data.get("callName") as string); const roomName = sanitiseRoomNameInput(data.get("callName") as string);
const displayName = data.get("displayName") as string; const displayName = data.get("displayName") as string;
async function submit() { async function submit(): Promise<void> {
setError(undefined); setError(undefined);
setLoading(true); setLoading(true);
const recaptchaResponse = await execute(); const recaptchaResponse = await execute();

View File

@ -31,7 +31,7 @@ export interface GroupCallRoom {
} }
const tsCache: { [index: string]: number } = {}; const tsCache: { [index: string]: number } = {};
function getLastTs(client: MatrixClient, r: Room) { function getLastTs(client: MatrixClient, r: Room): number {
if (tsCache[r.roomId]) { if (tsCache[r.roomId]) {
return tsCache[r.roomId]; return tsCache[r.roomId];
} }
@ -82,7 +82,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState<GroupCallRoom[]>([]); const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
useEffect(() => { useEffect(() => {
function updateRooms() { function updateRooms(): void {
if (!client.groupCallEventHandler) { if (!client.groupCallEventHandler) {
return; return;
} }

View File

@ -35,11 +35,11 @@ enum LoadState {
class DependencyLoadStates { class DependencyLoadStates {
// TODO: decide where olm should be initialized (see TODO comment below) // TODO: decide where olm should be initialized (see TODO comment below)
// olm: LoadState = LoadState.None; // olm: LoadState = LoadState.None;
config: LoadState = LoadState.None; public config: LoadState = LoadState.None;
sentry: LoadState = LoadState.None; public sentry: LoadState = LoadState.None;
openTelemetry: LoadState = LoadState.None; public openTelemetry: LoadState = LoadState.None;
allDepsAreLoaded() { public allDepsAreLoaded(): boolean {
return !Object.values(this).some((s) => s !== LoadState.Loaded); return !Object.values(this).some((s) => s !== LoadState.Loaded);
} }
} }
@ -52,7 +52,7 @@ export class Initializer {
return Initializer.internalInstance?.isInitialized; return Initializer.internalInstance?.isInitialized;
} }
public static initBeforeReact() { public static initBeforeReact(): void {
// this maybe also needs to return a promise in the future, // this maybe also needs to return a promise in the future,
// if we have to do async inits before showing the loading screen // if we have to do async inits before showing the loading screen
// but this should be avioded if possible // but this should be avioded if possible
@ -126,9 +126,9 @@ export class Initializer {
return Initializer.internalInstance.initPromise; return Initializer.internalInstance.initPromise;
} }
loadStates = new DependencyLoadStates(); private loadStates = new DependencyLoadStates();
initStep(resolve: (value: void | PromiseLike<void>) => void) { private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`) // 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 // we need to decide if we want to init it here or keep it in initClient
// if (this.loadStates.olm === LoadState.None) { // if (this.loadStates.olm === LoadState.None) {

View File

@ -64,7 +64,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
useEffect(() => { useEffect(() => {
const currentInput = fileInputRef.current; const currentInput = fileInputRef.current;
const onChange = (e: Event) => { const onChange = (e: Event): void => {
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>; const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
if (inputEvent.target.files && inputEvent.target.files.length > 0) { if (inputEvent.target.files && inputEvent.target.files.length > 0) {
setObjUrl(URL.createObjectURL(inputEvent.target.files[0])); setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
@ -76,7 +76,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
currentInput.addEventListener("change", onChange); currentInput.addEventListener("change", onChange);
return () => { return (): void => {
currentInput?.removeEventListener("change", onChange); currentInput?.removeEventListener("change", onChange);
}; };
}); });

View File

@ -41,8 +41,8 @@ export function StarRatingInput({
return ( return (
<div <div
className={styles.inputContainer} className={styles.inputContainer}
onMouseEnter={() => setHover(index)} onMouseEnter={(): void => setHover(index)}
onMouseLeave={() => setHover(rating)} onMouseLeave={(): void => setHover(rating)}
key={index} key={index}
> >
<input <input
@ -51,7 +51,7 @@ export function StarRatingInput({
id={"starInput" + String(index)} id={"starInput" + String(index)}
value={String(index) + "Star"} value={String(index) + "Star"}
name="star rating" name="star rating"
onChange={(_ev) => { onChange={(_ev): void => {
setRating(index); setRating(index);
onChange(index); onChange(index);
}} }}

View File

@ -52,7 +52,7 @@ export interface MediaDevices {
function useObservableState<T>( function useObservableState<T>(
observable: Observable<T> | undefined, observable: Observable<T> | undefined,
startWith: T startWith: T
) { ): T {
const [state, setState] = useState<T>(startWith); const [state, setState] = useState<T>(startWith);
useEffect(() => { useEffect(() => {
// observable state doesn't run in SSR // observable state doesn't run in SSR
@ -207,7 +207,8 @@ export const MediaDevicesProvider: FC<Props> = ({ 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 * 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 * default because it may involve requesting additional permissions from the
* user. * user.
*/ */
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) => export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true
): void =>
useEffect(() => { useEffect(() => {
if (enabled) { if (enabled) {
context.startUsingDeviceNames(); context.startUsingDeviceNames();

View File

@ -43,13 +43,13 @@ export type OpenIDClientParts = Pick<
export function useOpenIDSFU( export function useOpenIDSFU(
client: OpenIDClientParts, client: OpenIDClientParts,
rtcSession: MatrixRTCSession rtcSession: MatrixRTCSession
) { ): SFUConfig | undefined {
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined); const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
const activeFocus = useActiveFocus(rtcSession); const activeFocus = useActiveFocus(rtcSession);
useEffect(() => { useEffect(() => {
(async () => { (async (): Promise<void> => {
const sfuConfig = activeFocus const sfuConfig = activeFocus
? await getSFUConfigWithOpenID(client, activeFocus) ? await getSFUConfigWithOpenID(client, activeFocus)
: undefined; : undefined;

View File

@ -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 { import {
AudioPresets, AudioPresets,
DefaultReconnectPolicy, DefaultReconnectPolicy,

View File

@ -119,7 +119,7 @@ export function useECConnectionState(
`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<void> => {
setSwitchingFocus(true); setSwitchingFocus(true);
await livekitRoom?.disconnect(); await livekitRoom?.disconnect();
setIsInDoConnect(true); setIsInDoConnect(true);

View File

@ -156,7 +156,7 @@ export function useLiveKit(
useEffect(() => { useEffect(() => {
// Sync the requested devices with LiveKit's devices // Sync the requested devices with LiveKit's devices
if (room !== undefined && connectionState === ConnectionState.Connected) { if (room !== undefined && connectionState === ConnectionState.Connected) {
const syncDevice = (kind: MediaDeviceKind, device: MediaDevice) => { const syncDevice = (kind: MediaDeviceKind, device: MediaDevice): void => {
const id = device.selectedId; const id = device.selectedId;
if (id !== undefined && room.getActiveDevice(kind) !== id) { if (id !== undefined && room.getActiveDevice(kind) !== id) {
room room

View File

@ -26,7 +26,7 @@ import { createBrowserHistory } from "history";
import "./index.css"; import "./index.css";
import { setLogLevel as setLKLogLevel } from "livekit-client"; import { setLogLevel as setLKLogLevel } from "livekit-client";
import App from "./App"; import { App } from "./App";
import { init as initRageshake } from "./settings/rageshake"; import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer"; import { Initializer } from "./initializer";

View File

@ -40,7 +40,7 @@ export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
export class CryptoStoreIntegrityError extends Error { export class CryptoStoreIntegrityError extends Error {
constructor() { public constructor() {
super("Crypto store data was expected, but none was found"); super("Crypto store data was expected, but none was found");
} }
} }
@ -52,13 +52,13 @@ const SYNC_STORE_NAME = "element-call-sync";
// (It's a good opportunity to make the database names consistent.) // (It's a good opportunity to make the database names consistent.)
const CRYPTO_STORE_NAME = "element-call-crypto"; const CRYPTO_STORE_NAME = "element-call-crypto";
function waitForSync(client: MatrixClient) { function waitForSync(client: MatrixClient): Promise<void> {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const onSync = ( const onSync = (
state: SyncState, state: SyncState,
_old: SyncState | null, _old: SyncState | null,
data?: ISyncStateData data?: ISyncStateData
) => { ): void => {
if (state === "PREPARED") { if (state === "PREPARED") {
client.removeListener(ClientEvent.Sync, onSync); client.removeListener(ClientEvent.Sync, onSync);
resolve(); resolve();
@ -108,7 +108,7 @@ export async function initClient(
// Chrome supports it. (It bundles them fine in production mode.) // Chrome supports it. (It bundles them fine in production mode.)
workerFactory: import.meta.env.DEV workerFactory: import.meta.env.DEV
? undefined ? undefined
: () => new IndexedDBWorker(), : (): Worker => new IndexedDBWorker(),
}); });
} else if (localStorage) { } else if (localStorage) {
baseOpts.store = new MemoryStore({ localStorage }); baseOpts.store = new MemoryStore({ localStorage });
@ -307,7 +307,7 @@ export async function createRoom(
// Wait for the room to arrive // Wait for the room to arrive
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const onRoom = async (room: Room) => { const onRoom = async (room: Room): Promise<void> => {
if (room.roomId === (await createPromise).room_id) { if (room.roomId === (await createPromise).room_id) {
resolve(); resolve();
cleanUp(); cleanUp();
@ -318,7 +318,7 @@ export async function createRoom(
cleanUp(); cleanUp();
}); });
const cleanUp = () => { const cleanUp = (): void => {
client.off(ClientEvent.Room, onRoom); client.off(ClientEvent.Room, onRoom);
}; };
client.on(ClientEvent.Room, onRoom); client.on(ClientEvent.Room, onRoom);

View File

@ -44,7 +44,7 @@ export class OTelCall {
OTelCallAbstractMediaStreamSpan OTelCallAbstractMediaStreamSpan
>(); >();
constructor( public constructor(
public userId: string, public userId: string,
public deviceId: string, public deviceId: string,
public call: MatrixCall, public call: MatrixCall,
@ -60,7 +60,7 @@ export class OTelCall {
} }
} }
public dispose() { public dispose(): void {
this.call.peerConn?.removeEventListener( this.call.peerConn?.removeEventListener(
"connectionstatechange", "connectionstatechange",
this.onCallConnectionStateChanged this.onCallConnectionStateChanged

View File

@ -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 opentelemetry, { Span } from "@opentelemetry/api";
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
@ -14,8 +30,8 @@ export abstract class OTelCallAbstractMediaStreamSpan {
public readonly span; public readonly span;
public constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
protected readonly type: string protected readonly type: string
) { ) {
const ctx = opentelemetry.trace.setSpan( const ctx = opentelemetry.trace.setSpan(
@ -32,7 +48,7 @@ export abstract class OTelCallAbstractMediaStreamSpan {
this.span = oTel.tracer.startSpan(this.type, options, ctx); this.span = oTel.tracer.startSpan(this.type, options, ctx);
} }
protected upsertTrackSpans(tracks: TrackStats[]) { protected upsertTrackSpans(tracks: TrackStats[]): void {
let prvTracks: TrackId[] = [...this.trackSpans.keys()]; let prvTracks: TrackId[] = [...this.trackSpans.keys()];
tracks.forEach((t) => { tracks.forEach((t) => {
if (!this.trackSpans.has(t.id)) { if (!this.trackSpans.has(t.id)) {

View File

@ -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 { Span } from "@opentelemetry/api";
import { import {
CallFeedStats, CallFeedStats,
@ -10,9 +26,9 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan { export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean }; private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
callFeed: CallFeedStats callFeed: CallFeedStats
) { ) {
const postFix = const postFix =

View File

@ -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 { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
import opentelemetry, { Span } from "@opentelemetry/api"; import opentelemetry, { Span } from "@opentelemetry/api";
@ -8,8 +24,8 @@ export class OTelCallMediaStreamTrackSpan {
private prev: TrackStats; private prev: TrackStats;
public constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly streamSpan: Span, protected readonly streamSpan: Span,
data: TrackStats data: TrackStats
) { ) {
const ctx = opentelemetry.trace.setSpan( const ctx = opentelemetry.trace.setSpan(

View File

@ -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 { Span } from "@opentelemetry/api";
import { import {
TrackStats, TrackStats,
@ -13,9 +29,9 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr
currentDirection: string; currentDirection: string;
}; };
constructor( public constructor(
readonly oTel: ElementCallOpenTelemetry, protected readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span, protected readonly callSpan: Span,
stats: TransceiverStats stats: TransceiverStats
) { ) {
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`); super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);

View File

@ -62,7 +62,7 @@ export class OTelGroupCallMembership {
}; };
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>(); private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
constructor(private groupCall: GroupCall, client: MatrixClient) { public constructor(private groupCall: GroupCall, client: MatrixClient) {
const clientId = client.getUserId(); const clientId = client.getUserId();
if (clientId) { if (clientId) {
this.myUserId = clientId; this.myUserId = clientId;
@ -76,14 +76,14 @@ export class OTelGroupCallMembership {
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
} }
dispose() { public dispose(): void {
this.groupCall.removeListener( this.groupCall.removeListener(
GroupCallEvent.CallsChanged, GroupCallEvent.CallsChanged,
this.onCallsChanged this.onCallsChanged
); );
} }
public onJoinCall() { public onJoinCall(): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) { if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started"); logger.warn("Call membership span is already started");
@ -114,7 +114,7 @@ export class OTelGroupCallMembership {
this.callMembershipSpan?.addEvent("matrix.joinCall"); this.callMembershipSpan?.addEvent("matrix.joinCall");
} }
public onLeaveCall() { public onLeaveCall(): void {
if (this.callMembershipSpan === undefined) { if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended"); logger.warn("Call membership span is already ended");
return; return;
@ -127,7 +127,7 @@ export class OTelGroupCallMembership {
this.groupCallContext = undefined; this.groupCallContext = undefined;
} }
public onUpdateRoomState(event: MatrixEvent) { public onUpdateRoomState(event: MatrixEvent): void {
if ( if (
!event || !event ||
(!event.getType().startsWith("m.call") && (!event.getType().startsWith("m.call") &&
@ -142,7 +142,7 @@ export class OTelGroupCallMembership {
); );
} }
public onCallsChanged = (calls: CallsByUserAndDevice) => { public onCallsChanged(calls: CallsByUserAndDevice): void {
for (const [userId, userCalls] of calls.entries()) { for (const [userId, userCalls] of calls.entries()) {
for (const [deviceId, call] of userCalls.entries()) { for (const [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) { if (!this.callsByCallId.has(call.callId)) {
@ -179,9 +179,9 @@ export class OTelGroupCallMembership {
this.callsByCallId.delete(callTrackingInfo.call.callId); 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); const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) { if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`); logger.error(`Got call state change for unknown call ID ${call.callId}`);
@ -193,7 +193,7 @@ export class OTelGroupCallMembership {
}); });
} }
public onSendEvent(call: MatrixCall, event: VoipEvent) { public onSendEvent(call: MatrixCall, event: VoipEvent): void {
const eventType = event.eventType as string; const eventType = event.eventType as string;
if ( if (
!eventType.startsWith("m.call") && !eventType.startsWith("m.call") &&
@ -220,7 +220,7 @@ export class OTelGroupCallMembership {
} }
} }
public onReceivedVoipEvent(event: MatrixEvent) { public onReceivedVoipEvent(event: MatrixEvent): void {
// These come straight from CallEventHandler so don't have // These come straight from CallEventHandler so don't have
// a call already associated (in principle we could receive // a call already associated (in principle we could receive
// events for calls we don't know about). // events for calls we don't know about).
@ -251,37 +251,41 @@ export class OTelGroupCallMembership {
}); });
} }
public onToggleMicrophoneMuted(newValue: boolean) { public onToggleMicrophoneMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
"matrix.microphone.muted": newValue, "matrix.microphone.muted": newValue,
}); });
} }
public onSetMicrophoneMuted(setMuted: boolean) { public onSetMicrophoneMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", { this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted, "matrix.microphone.muted": setMuted,
}); });
} }
public onToggleLocalVideoMuted(newValue: boolean) { public onToggleLocalVideoMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue, "matrix.video.muted": newValue,
}); });
} }
public onSetLocalVideoMuted(setMuted: boolean) { public onSetLocalVideoMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", { this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted, "matrix.video.muted": setMuted,
}); });
} }
public onToggleScreensharing(newValue: boolean) { public onToggleScreensharing(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", { this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue, "matrix.screensharing.enabled": newValue,
}); });
} }
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) { public onSpeaking(
member: RoomMember,
deviceId: string,
speaking: boolean
): void {
if (speaking) { if (speaking) {
// Ensure that there's an audio activity span for this speaker // Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member); let deviceMap = this.speakingSpans.get(member);
@ -311,7 +315,7 @@ export class OTelGroupCallMembership {
} }
} }
public onCallError(error: CallError, call: MatrixCall) { public onCallError(error: CallError, call: MatrixCall): void {
const callTrackingInfo = this.callsByCallId.get(call.callId); const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) { if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`); logger.error(`Got error for unknown call ID ${call.callId}`);
@ -321,17 +325,19 @@ export class OTelGroupCallMembership {
callTrackingInfo.span.recordException(error); callTrackingInfo.span.recordException(error);
} }
public onGroupCallError(error: GroupCallError) { public onGroupCallError(error: GroupCallError): void {
this.callMembershipSpan?.recordException(error); this.callMembershipSpan?.recordException(error);
} }
public onUndecryptableToDevice(event: MatrixEvent) { public onUndecryptableToDevice(event: MatrixEvent): void {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(), "sender.userId": event.getSender(),
}); });
} }
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) { public onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>
): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined; let call: OTelCall | undefined;
const callId = report.report?.callId; const callId = report.report?.callId;
@ -362,7 +368,7 @@ export class OTelGroupCallMembership {
public onConnectionStatsReport( public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport> statsReport: GroupCallStatsReport<ConnectionStatsReport>
) { ): void {
this.buildCallStatsSpan( this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport, OTelStatsReportType.ConnectionReport,
statsReport.report statsReport.report
@ -371,7 +377,7 @@ export class OTelGroupCallMembership {
public onByteSentStatsReport( public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport> statsReport: GroupCallStatsReport<ByteSentStatsReport>
) { ): void {
this.buildCallStatsSpan( this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport, OTelStatsReportType.ByteSentReport,
statsReport.report statsReport.report
@ -431,7 +437,7 @@ export class OTelGroupCallMembership {
public onSummaryStatsReport( public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport> statsReport: GroupCallStatsReport<SummaryStatsReport>
) { ): void {
if (!ElementCallOpenTelemetry.instance) return; if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport; const type = OTelStatsReportType.SummaryReport;

View File

@ -45,9 +45,9 @@ export class ObjectFlattener {
return flatObject; return flatObject;
} }
static flattenSummaryStatsReportObject( public static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport> statsReport: GroupCallStatsReport<SummaryStatsReport>
) { ): Attributes {
const flatObject = {}; const flatObject = {};
ObjectFlattener.flattenObjectRecursive( ObjectFlattener.flattenObjectRecursive(
statsReport.report, statsReport.report,

View File

@ -36,7 +36,7 @@ export class ElementCallOpenTelemetry {
private otlpExporter?: OTLPTraceExporter; private otlpExporter?: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor; public readonly rageshakeProcessor?: RageshakeSpanProcessor;
static globalInit(): void { public static globalInit(): void {
const config = Config.get(); const config = Config.get();
// we always enable opentelemetry in general. We only enable the OTLP // 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) // collector if a URL is defined (and in future if another setting is defined)
@ -55,11 +55,11 @@ export class ElementCallOpenTelemetry {
} }
} }
static get instance(): ElementCallOpenTelemetry { public static get instance(): ElementCallOpenTelemetry {
return sharedInstance; return sharedInstance;
} }
constructor( private constructor(
collectorUrl: string | undefined, collectorUrl: string | undefined,
rageshakeUrl: string | undefined rageshakeUrl: string | undefined
) { ) {

View File

@ -38,7 +38,11 @@ type ProfileSaveCallback = ({
removeAvatar: boolean; removeAvatar: boolean;
}) => Promise<void>; }) => Promise<void>;
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] = const [{ success, loading, displayName, avatarUrl, error }, setState] =
useState<ProfileLoadState>(() => { useState<ProfileLoadState>(() => {
let user: User | undefined = undefined; let user: User | undefined = undefined;
@ -59,7 +63,7 @@ export function useProfile(client: MatrixClient | undefined) {
const onChangeUser = ( const onChangeUser = (
_event: MatrixEvent | undefined, _event: MatrixEvent | undefined,
{ displayName, avatarUrl }: User { displayName, avatarUrl }: User
) => { ): void => {
setState({ setState({
success: false, success: false,
loading: false, loading: false,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -148,7 +148,7 @@ export const CallEndedView: FC<Props> = ({
</div> </div>
); );
const renderBody = () => { const renderBody = (): ReactNode => {
if (leaveError) { if (leaveError) {
return ( return (
<> <>

View File

@ -28,7 +28,8 @@ import {
useContext, useContext,
Dispatch, Dispatch,
SetStateAction, SetStateAction,
ReactNode, FC,
PropsWithChildren,
} from "react"; } from "react";
import ReactJson, { CollapsedFieldProps } from "react-json-view"; import ReactJson, { CollapsedFieldProps } from "react-json-view";
import mermaid from "mermaid"; import mermaid from "mermaid";
@ -72,11 +73,11 @@ const defaultCollapsedFields = [
"content", "content",
]; ];
function shouldCollapse({ name }: CollapsedFieldProps) { function shouldCollapse({ name }: CollapsedFieldProps): boolean {
return name ? defaultCollapsedFields.includes(name) : false; return name ? defaultCollapsedFields.includes(name) : false;
} }
function getUserName(userId: string) { function getUserName(userId: string): string {
const match = userId.match(/@([^:]+):/); const match = userId.match(/@([^:]+):/);
return match && match.length > 0 return match && match.length > 0
@ -84,7 +85,7 @@ function getUserName(userId: string) {
: userId.replace(/\W/g, ""); : userId.replace(/\W/g, "");
} }
function formatContent(type: string, content: CallEventContent) { function formatContent(type: string, content: CallEventContent): string {
if (type === "m.call.hangup") { if (type === "m.call.hangup") {
return `callId: ${content.call_id.slice(-4)} reason: ${ return `callId: ${content.call_id.slice(-4)} reason: ${
content.reason content.reason
@ -123,7 +124,7 @@ const dateFormatter = new Intl.DateTimeFormat([], {
fractionalSecondDigits: 3, fractionalSecondDigits: 3,
}); });
function formatTimestamp(timestamp: number | Date) { function formatTimestamp(timestamp: number | Date): string {
return dateFormatter.format(timestamp); return dateFormatter.format(timestamp);
} }
@ -145,11 +146,9 @@ export const InspectorContext =
[InspectorContextState, Dispatch<SetStateAction<InspectorContextState>>] [InspectorContextState, Dispatch<SetStateAction<InspectorContextState>>]
>(undefined); >(undefined);
export function InspectorContextProvider({ export const InspectorContextProvider: FC<PropsWithChildren<{}>> = ({
children, children,
}: { }) => {
children: ReactNode;
}) {
// We take the tuple of [currentState, setter] and stick // We take the tuple of [currentState, setter] and stick
// it straight into the context for other things to call // it straight into the context for other things to call
// the setState method... this feels like a fairly severe // the setState method... this feels like a fairly severe
@ -161,7 +160,7 @@ export function InspectorContextProvider({
{children} {children}
</InspectorContext.Provider> </InspectorContext.Provider>
); );
} };
type CallEventContent = { type CallEventContent = {
["m.calls"]: { ["m.calls"]: {
@ -192,13 +191,13 @@ interface SequenceDiagramViewerProps {
events: SequenceDiagramMatrixEvent[]; events: SequenceDiagramMatrixEvent[];
} }
export function SequenceDiagramViewer({ export const SequenceDiagramViewer: FC<SequenceDiagramViewerProps> = ({
localUserId, localUserId,
remoteUserIds, remoteUserIds,
selectedUserId, selectedUserId,
onSelectUserId, onSelectUserId,
events, events,
}: SequenceDiagramViewerProps) { }) => {
const mermaidElRef = useRef<HTMLDivElement>(null); const mermaidElRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -232,7 +231,7 @@ export function SequenceDiagramViewer({
className={styles.selectInput} className={styles.selectInput}
label="Remote User" label="Remote User"
selectedKey={selectedUserId} selectedKey={selectedUserId}
onSelectionChange={(key) => onSelectUserId(key.toString())} onSelectionChange={(key): void => onSelectUserId(key.toString())}
> >
{remoteUserIds.map((userId) => ( {remoteUserIds.map((userId) => (
<Item key={userId}>{userId}</Item> <Item key={userId}>{userId}</Item>
@ -243,7 +242,7 @@ export function SequenceDiagramViewer({
</div> </div>
</div> </div>
); );
} };
function reducer( function reducer(
state: InspectorContextState, state: InspectorContextState,
@ -254,7 +253,7 @@ function reducer(
callStateEvent?: MatrixEvent; callStateEvent?: MatrixEvent;
memberStateEvents?: MatrixEvent[]; memberStateEvents?: MatrixEvent[];
} }
) { ): InspectorContextState {
switch (action.type) { switch (action.type) {
case RoomStateEvent.Events: { case RoomStateEvent.Events: {
const { event, callStateEvent, memberStateEvents } = action; const { event, callStateEvent, memberStateEvents } = action;
@ -380,7 +379,7 @@ function useGroupCallState(
}); });
useEffect(() => { useEffect(() => {
function onUpdateRoomState(event?: MatrixEvent) { function onUpdateRoomState(event?: MatrixEvent): void {
const callStateEvent = groupCall.room.currentState.getStateEvents( const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call", "org.matrix.msc3401.call",
groupCall.groupCallId groupCall.groupCallId
@ -400,13 +399,13 @@ function useGroupCallState(
otelGroupCallMembership?.onUpdateRoomState(event); otelGroupCallMembership?.onUpdateRoomState(event);
} }
function onReceivedVoipEvent(event: MatrixEvent) { function onReceivedVoipEvent(event: MatrixEvent): void {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
otelGroupCallMembership?.onReceivedVoipEvent(event); otelGroupCallMembership?.onReceivedVoipEvent(event);
} }
function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { function onSendVoipEvent(event: VoipEvent, call: MatrixCall): void {
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
otelGroupCallMembership?.onSendEvent(call, event); otelGroupCallMembership?.onSendEvent(call, event);
@ -416,19 +415,19 @@ function useGroupCallState(
newState: CallState, newState: CallState,
_: CallState, _: CallState,
call: MatrixCall call: MatrixCall
) { ): void {
otelGroupCallMembership?.onCallStateChange(call, newState); otelGroupCallMembership?.onCallStateChange(call, newState);
} }
function onCallError(error: CallError, call: MatrixCall) { function onCallError(error: CallError, call: MatrixCall): void {
otelGroupCallMembership.onCallError(error, call); otelGroupCallMembership.onCallError(error, call);
} }
function onGroupCallError(error: GroupCallError) { function onGroupCallError(error: GroupCallError): void {
otelGroupCallMembership.onGroupCallError(error); otelGroupCallMembership.onGroupCallError(error);
} }
function onUndecryptableToDevice(event: MatrixEvent) { function onUndecryptableToDevice(event: MatrixEvent): void {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
Sentry.captureMessage("Undecryptable to-device Event"); Sentry.captureMessage("Undecryptable to-device Event");
@ -478,12 +477,12 @@ interface GroupCallInspectorProps {
show: boolean; show: boolean;
} }
export function GroupCallInspector({ export const GroupCallInspector: FC<GroupCallInspectorProps> = ({
client, client,
groupCall, groupCall,
otelGroupCallMembership, otelGroupCallMembership,
show, show,
}: GroupCallInspectorProps) { }) => {
const [currentTab, setCurrentTab] = useState("sequence-diagrams"); const [currentTab, setCurrentTab] = useState("sequence-diagrams");
const [selectedUserId, setSelectedUserId] = useState<string>(); const [selectedUserId, setSelectedUserId] = useState<string>();
const state = useGroupCallState(client, groupCall, otelGroupCallMembership); const state = useGroupCallState(client, groupCall, otelGroupCallMembership);
@ -506,10 +505,12 @@ export function GroupCallInspector({
className={styles.inspector} className={styles.inspector}
> >
<div className={styles.toolbar}> <div className={styles.toolbar}>
<button onClick={() => setCurrentTab("sequence-diagrams")}> <button onClick={(): void => setCurrentTab("sequence-diagrams")}>
Sequence Diagrams Sequence Diagrams
</button> </button>
<button onClick={() => setCurrentTab("inspector")}>Inspector</button> <button onClick={(): void => setCurrentTab("inspector")}>
Inspector
</button>
</div> </div>
{currentTab === "sequence-diagrams" && {currentTab === "sequence-diagrams" &&
state.localUserId && state.localUserId &&
@ -539,4 +540,4 @@ export function GroupCallInspector({
)} )}
</Resizable> </Resizable>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, isE2EESupported } from "livekit-client"; import { Room, isE2EESupported } from "livekit-client";
@ -64,14 +64,14 @@ interface Props {
rtcSession: MatrixRTCSession; rtcSession: MatrixRTCSession;
} }
export function GroupCallView({ export const GroupCallView: FC<Props> = ({
client, client,
isPasswordlessUser, isPasswordlessUser,
confineToRoom, confineToRoom,
preload, preload,
hideHeader, hideHeader,
rtcSession, rtcSession,
}: Props) { }) => {
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession); const isJoined = useMatrixRTCSessionJoinState(rtcSession);
@ -135,7 +135,9 @@ export function GroupCallView({
useEffect(() => { useEffect(() => {
if (widget && preload) { if (widget && preload) {
// In preload mode, wait for a join action before entering // In preload mode, wait for a join action before entering
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => { const onJoin = async (
ev: CustomEvent<IWidgetApiRequest>
): Promise<void> => {
// XXX: I think this is broken currently - LiveKit *won't* request // XXX: I think this is broken currently - LiveKit *won't* request
// permissions and give you device names unless you specify a kind, but // 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 // here we want all kinds of devices. This needs a fix in livekit-client
@ -247,9 +249,11 @@ export function GroupCallView({
useEffect(() => { useEffect(() => {
if (widget && isJoined) { if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => { const onHangup = async (
ev: CustomEvent<IWidgetApiRequest>
): Promise<void> => {
leaveRTCSession(rtcSession); leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false); widget!.api.setAlwaysOnScreen(false);
}; };
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup); widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
@ -388,7 +392,7 @@ export function GroupCallView({
client={client} client={client}
matrixInfo={matrixInfo} matrixInfo={matrixInfo}
muteStates={muteStates} muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)} onEnter={(): void => enterRTCSession(rtcSession)}
confineToRoom={confineToRoom} confineToRoom={confineToRoom}
hideHeader={hideHeader} hideHeader={hideHeader}
participatingMembers={participatingMembers} participatingMembers={participatingMembers}
@ -397,4 +401,4 @@ export function GroupCallView({
</> </>
); );
} }
} };

View File

@ -27,7 +27,16 @@ import { ConnectionState, Room, Track } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room"; 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 { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -91,7 +100,7 @@ export interface ActiveCallProps
e2eeConfig?: E2EEConfig; e2eeConfig?: E2EEConfig;
} }
export function ActiveCall(props: ActiveCallProps) { export const ActiveCall: FC<ActiveCallProps> = (props) => {
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
const { livekitRoom, connState } = useLiveKit( const { livekitRoom, connState } = useLiveKit(
props.muteStates, props.muteStates,
@ -112,7 +121,7 @@ export function ActiveCall(props: ActiveCallProps) {
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} /> <InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider> </RoomContext.Provider>
); );
} };
export interface InCallViewProps { export interface InCallViewProps {
client: MatrixClient; client: MatrixClient;
@ -128,7 +137,7 @@ export interface InCallViewProps {
onShareClick: (() => void) | null; onShareClick: (() => void) | null;
} }
export function InCallView({ export const InCallView: FC<InCallViewProps> = ({
client, client,
matrixInfo, matrixInfo,
rtcSession, rtcSession,
@ -140,7 +149,7 @@ export function InCallView({
otelGroupCallMembership, otelGroupCallMembership,
connState, connState,
onShareClick, onShareClick,
}: InCallViewProps) { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
usePreventScroll(); usePreventScroll();
useWakeLock(); useWakeLock();
@ -211,13 +220,13 @@ export function InCallView({
useEffect(() => { useEffect(() => {
if (widget) { if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLayout("grid"); setLayout("grid");
await widget!.api.transport.reply(ev.detail, {}); widget!.api.transport.reply(ev.detail, {});
}; };
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => { const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLayout("spotlight"); 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.TileLayout, onTileLayout);
@ -296,7 +305,7 @@ export function InCallView({
disableAnimations={prefersReducedMotion || isSafari} disableAnimations={prefersReducedMotion || isSafari}
layoutStates={layoutStates} layoutStates={layoutStates}
> >
{(props) => ( {(props): ReactNode => (
<VideoTile <VideoTile
maximised={false} maximised={false}
fullscreen={false} fullscreen={false}
@ -444,7 +453,7 @@ export function InCallView({
/> />
</div> </div>
); );
} };
function findMatrixMember( function findMatrixMember(
room: MatrixRoom, room: MatrixRoom,

View File

@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Modal, ModalProps } from "../Modal"; import { Modal, Props as ModalProps } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input"; import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake"; import { useSubmitRageshake } from "../settings/submit-rageshake";
@ -52,8 +52,8 @@ export const RageshakeRequestModal: FC<Props> = ({
</Body> </Body>
<FieldRow> <FieldRow>
<Button <Button
onPress={() => onPress={(): void =>
submitRageshake({ void submitRageshake({
sendLogs: true, sendLogs: true,
rageshakeRequestId, rageshakeRequestId,
roomId, roomId,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useState } from "react"; import { FC, useCallback, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@ -28,7 +28,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
export function RoomAuthView() { export const RoomAuthView: FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
@ -121,4 +121,4 @@ export function RoomAuthView() {
</div> </div>
</> </>
); );
} };

View File

@ -22,11 +22,11 @@ import { TileDescriptor } from "../video-grid/VideoGrid";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
import { useEventTarget } from "../useEvents"; import { useEventTarget } from "../useEvents";
const isFullscreen = () => const isFullscreen = (): boolean =>
Boolean(document.fullscreenElement) || Boolean(document.fullscreenElement) ||
Boolean(document.webkitFullscreenElement); Boolean(document.webkitFullscreenElement);
function enterFullscreen() { function enterFullscreen(): void {
if (document.body.requestFullscreen) { if (document.body.requestFullscreen) {
document.body.requestFullscreen(); document.body.requestFullscreen();
} else if (document.body.webkitRequestFullscreen) { } else if (document.body.webkitRequestFullscreen) {
@ -36,7 +36,7 @@ function enterFullscreen() {
} }
} }
function exitFullscreen() { function exitFullscreen(): void {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
} else if (document.webkitExitFullscreen) { } else if (document.webkitExitFullscreen) {
@ -46,7 +46,7 @@ function exitFullscreen() {
} }
} }
function useFullscreenChange(onFullscreenChange: () => void) { function useFullscreenChange(onFullscreenChange: () => void): void {
useEventTarget(document.body, "fullscreenchange", onFullscreenChange); useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange); useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
} }

View File

@ -15,12 +15,14 @@ limitations under the License.
*/ */
import { useCallback } from "react"; import { useCallback } from "react";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import type { Room } from "matrix-js-sdk/src/models/room"; import type { Room } from "matrix-js-sdk/src/models/room";
import { useRoomState } from "./useRoomState"; import { useRoomState } from "./useRoomState";
export const useJoinRule = (room: Room) => export function useJoinRule(room: Room): JoinRule {
useRoomState( return useRoomState(
room, room,
useCallback((state) => state.getJoinRule(), []) useCallback((state) => state.getJoinRule(), [])
); );
}

View File

@ -107,13 +107,13 @@ export const useLoadGroupCall = (
return rtcSession; return rtcSession;
}; };
const waitForClientSyncing = async () => { const waitForClientSyncing = async (): Promise<void> => {
if (client.getSyncState() !== SyncState.Syncing) { if (client.getSyncState() !== SyncState.Syncing) {
logger.debug( logger.debug(
"useLoadGroupCall: waiting for client to start syncing..." "useLoadGroupCall: waiting for client to start syncing..."
); );
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
const onSync = () => { const onSync = (): void => {
if (client.getSyncState() === SyncState.Syncing) { if (client.getSyncState() === SyncState.Syncing) {
client.off(ClientEvent.Sync, onSync); client.off(ClientEvent.Sync, onSync);
return resolve(); return resolve();

View File

@ -18,11 +18,11 @@ import { useEffect } from "react";
import { platform } from "../Platform"; import { platform } from "../Platform";
export function usePageUnload(callback: () => void) { export function usePageUnload(callback: () => void): void {
useEffect(() => { useEffect(() => {
let pageVisibilityTimeout: ReturnType<typeof setTimeout>; let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
function onBeforeUnload(event: PageTransitionEvent) { function onBeforeUnload(event: PageTransitionEvent): void {
if (event.type === "visibilitychange") { if (event.type === "visibilitychange") {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
clearTimeout(pageVisibilityTimeout); clearTimeout(pageVisibilityTimeout);

View File

@ -19,8 +19,9 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { useRoomState } from "./useRoomState"; import { useRoomState } from "./useRoomState";
export const useRoomAvatar = (room: Room) => export function useRoomAvatar(room: Room): string | null {
useRoomState( return useRoomState(
room, room,
useCallback(() => room.getMxcAvatarUrl(), [room]) useCallback(() => room.getMxcAvatarUrl(), [room])
); );
}

View File

@ -33,7 +33,7 @@ function makeFocus(livekitAlias: string): LivekitFocus {
}; };
} }
export function enterRTCSession(rtcSession: MatrixRTCSession) { export function enterRTCSession(rtcSession: MatrixRTCSession): void {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId); PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
@ -47,7 +47,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession) {
rtcSession.joinRoomSession([makeFocus(livekitAlias)]); rtcSession.joinRoomSession([makeFocus(livekitAlias)]);
} }
export function leaveRTCSession(rtcSession: MatrixRTCSession) { export function leaveRTCSession(rtcSession: MatrixRTCSession): void {
//groupCallOTelMembership?.onLeaveCall(); //groupCallOTelMembership?.onLeaveCall();
rtcSession.leaveRoomSession(); rtcSession.leaveRoomSession();
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback } from "react"; import { FC, useCallback } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring"; import { randomString } from "matrix-js-sdk/src/randomstring";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -29,7 +29,7 @@ interface Props {
roomId?: string; roomId?: string;
} }
export function FeedbackSettingsTab({ roomId }: Props) { export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest(); const sendRageshakeRequest = useRageshakeRequest();
@ -104,4 +104,4 @@ export function FeedbackSettingsTab({ roomId }: Props) {
</form> </form>
</div> </div>
); );
} };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { useCallback, useEffect, useMemo, useRef } from "react"; import { FC, useCallback, useEffect, useMemo, useRef } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -26,7 +26,7 @@ import styles from "./ProfileSettingsTab.module.css";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
} }
export function ProfileSettingsTab({ client }: Props) { export const ProfileSettingsTab: FC<Props> = ({ client }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { error, displayName, avatarUrl, saveProfile } = useProfile(client); const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
const userId = useMemo(() => client.getUserId(), [client]); const userId = useMemo(() => client.getUserId(), [client]);
@ -120,4 +120,4 @@ export function ProfileSettingsTab({ client }: Props) {
)} )}
</form> </form>
); );
} };

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useCallback } from "react"; import { FC, useCallback } from "react";
import { Button } from "../button"; import { Button } from "../button";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
@ -26,7 +26,7 @@ interface Props {
description: string; description: string;
} }
export const RageshakeButton = ({ description }: Props) => { export const RageshakeButton: FC<Props> = ({ description }) => {
const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ChangeEvent, Key, useCallback, useState } from "react"; import { ChangeEvent, FC, Key, ReactNode, useCallback, useState } from "react";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk";
@ -59,7 +59,7 @@ interface Props {
defaultTab?: string; defaultTab?: string;
} }
export const SettingsModal = (props: Props) => { export const SettingsModal: FC<Props> = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showInspector, setShowInspector] = useShowInspector(); const [showInspector, setShowInspector] = useShowInspector();
@ -73,7 +73,10 @@ export const SettingsModal = (props: Props) => {
const downloadDebugLog = useDownloadDebugLog(); const downloadDebugLog = useDownloadDebugLog();
// Generate a `SelectInput` with a list of devices for a given device kind. // Generate a `SelectInput` with a list of devices for a given device kind.
const generateDeviceSelection = (devices: MediaDevice, caption: string) => { const generateDeviceSelection = (
devices: MediaDevice,
caption: string
): ReactNode => {
if (devices.available.length == 0) return null; if (devices.available.length == 0) return null;
return ( return (
@ -84,7 +87,7 @@ export const SettingsModal = (props: Props) => {
? "default" ? "default"
: devices.selectedId : devices.selectedId
} }
onSelectionChange={(id) => devices.select(id.toString())} onSelectionChange={(id): void => devices.select(id.toString())}
> >
{devices.available.map(({ deviceId, label }, index) => ( {devices.available.map(({ deviceId, label }, index) => (
<Item key={deviceId}> <Item key={deviceId}>
@ -197,7 +200,7 @@ export const SettingsModal = (props: Props) => {
checked={developerSettingsTab} checked={developerSettingsTab}
label={t("Developer Settings")} label={t("Developer Settings")}
description={t("Expose developer settings in the settings window.")} description={t("Expose developer settings in the settings window.")}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>): void =>
setDeveloperSettingsTab(event.target.checked) setDeveloperSettingsTab(event.target.checked)
} }
/> />
@ -209,7 +212,7 @@ export const SettingsModal = (props: Props) => {
type="checkbox" type="checkbox"
checked={optInAnalytics ?? undefined} checked={optInAnalytics ?? undefined}
description={optInDescription} description={optInDescription}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>): void => {
setOptInAnalytics?.(event.target.checked); setOptInAnalytics?.(event.target.checked);
}} }}
/> />
@ -241,7 +244,7 @@ export const SettingsModal = (props: Props) => {
label={t("Show call inspector")} label={t("Show call inspector")}
type="checkbox" type="checkbox"
checked={showInspector} checked={showInspector}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setShowInspector(e.target.checked) setShowInspector(e.target.checked)
} }
/> />
@ -253,7 +256,7 @@ export const SettingsModal = (props: Props) => {
label={t("Show connection stats")} label={t("Show connection stats")}
type="checkbox" type="checkbox"
checked={showConnectionStats} checked={showConnectionStats}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setShowConnectionStats(e.target.checked) setShowConnectionStats(e.target.checked)
} }
/> />
@ -270,7 +273,7 @@ export const SettingsModal = (props: Props) => {
disabled={!setEnableE2EE} disabled={!setEnableE2EE}
type="checkbox" type="checkbox"
checked={enableE2EE ?? undefined} checked={enableE2EE ?? undefined}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setEnableE2EE?.(e.target.checked) setEnableE2EE?.(e.target.checked)
} }
/> />

View File

@ -89,7 +89,7 @@ class ConsoleLogger extends EventEmitter {
this.originalFunctions[name] = originalFn; this.originalFunctions[name] = originalFn;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
consoleObj[name] = (...args) => { consoleObj[name] = (...args): void => {
this.log(level, ...args); this.log(level, ...args);
originalFn(...args); originalFn(...args);
}; };
@ -158,7 +158,7 @@ class IndexedDBLogStore {
private flushAgainPromise?: Promise<void>; private flushAgainPromise?: Promise<void>;
private id: string; private id: string;
constructor( public constructor(
private indexedDB: IDBFactory, private indexedDB: IDBFactory,
private loggerInstance: ConsoleLogger private loggerInstance: ConsoleLogger
) { ) {
@ -174,20 +174,20 @@ class IndexedDBLogStore {
public connect(): Promise<void> { public connect(): Promise<void> {
const req = this.indexedDB.open("logs"); const req = this.indexedDB.open("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = () => { req.onsuccess = (): void => {
this.db = req.result; this.db = req.result;
resolve(); resolve();
}; };
req.onerror = () => { req.onerror = (): void => {
const err = "Failed to open log database: " + req?.error?.name; const err = "Failed to open log database: " + req?.error?.name;
logger.error(err); logger.error(err);
reject(new Error(err)); reject(new Error(err));
}; };
// First time: Setup the object store // First time: Setup the object store
req.onupgradeneeded = () => { req.onupgradeneeded = (): void => {
const db = req.result; const db = req.result;
// This is the log entries themselves. Each entry is a chunk of // This is the log entries themselves. Each entry is a chunk of
// logs (ie multiple lines). 'id' is the instance ID (so logs with // logs (ie multiple lines). 'id' is the instance ID (so logs with
@ -218,7 +218,7 @@ class IndexedDBLogStore {
}); });
} }
private onLoggerLog = () => { private onLoggerLog = (): void => {
if (!this.db) return; if (!this.db) return;
this.throttledFlush(); this.throttledFlush();
@ -289,10 +289,10 @@ class IndexedDBLogStore {
} }
const txn = this.db.transaction(["logs", "logslastmod"], "readwrite"); const txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
const objStore = txn.objectStore("logs"); const objStore = txn.objectStore("logs");
txn.oncomplete = () => { txn.oncomplete = (): void => {
resolve(); resolve();
}; };
txn.onerror = (event) => { txn.onerror = (event): void => {
logger.error("Failed to flush logs : ", event); logger.error("Failed to flush logs : ", event);
reject(new Error("Failed to write logs: " + txn?.error?.message)); reject(new Error("Failed to write logs: " + txn?.error?.message));
}; };
@ -333,10 +333,10 @@ class IndexedDBLogStore {
.index("id") .index("id")
.openCursor(IDBKeyRange.only(id), "prev"); .openCursor(IDBKeyRange.only(id), "prev");
let lines = ""; let lines = "";
query.onerror = () => { query.onerror = (): void => {
reject(new Error("Query failed: " + query?.error?.message)); reject(new Error("Query failed: " + query?.error?.message));
}; };
query.onsuccess = () => { query.onsuccess = (): void => {
const cursor = query.result; const cursor = query.result;
if (!cursor) { if (!cursor) {
resolve(lines); resolve(lines);
@ -379,7 +379,7 @@ class IndexedDBLogStore {
const o = txn.objectStore("logs"); const o = txn.objectStore("logs");
// only load the key path, not the data which may be huge // only load the key path, not the data which may be huge
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
query.onsuccess = () => { query.onsuccess = (): void => {
const cursor = query.result; const cursor = query.result;
if (!cursor) { if (!cursor) {
return; return;
@ -387,10 +387,10 @@ class IndexedDBLogStore {
o.delete(cursor.primaryKey); o.delete(cursor.primaryKey);
cursor.continue(); cursor.continue();
}; };
txn.oncomplete = () => { txn.oncomplete = (): void => {
resolve(); resolve();
}; };
txn.onerror = () => { txn.onerror = (): void => {
reject( reject(
new Error( new Error(
"Failed to delete logs for " + `'${id}' : ${txn?.error?.message}` "Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`
@ -477,11 +477,11 @@ function selectQuery<T>(
const query = store.openCursor(keyRange); const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results: T[] = []; const results: T[] = [];
query.onerror = () => { query.onerror = (): void => {
reject(new Error("Query failed: " + query?.error?.message)); reject(new Error("Query failed: " + query?.error?.message));
}; };
// collect results // collect results
query.onsuccess = () => { query.onsuccess = (): void => {
const cursor = query.result; const cursor = query.result;
if (!cursor) { if (!cursor) {
resolve(results); resolve(results);

View File

@ -360,7 +360,7 @@ export function useRageshakeRequestModal(
useEffect(() => { useEffect(() => {
if (!client) return; if (!client) return;
const onEvent = (event: MatrixEvent) => { const onEvent = (event: MatrixEvent): void => {
const type = event.getType(); const type = event.getType();
if ( if (

View File

@ -55,15 +55,15 @@ export const getSetting = <T>(name: string, defaultValue: T): T => {
return item === null ? defaultValue : JSON.parse(item); return item === null ? defaultValue : JSON.parse(item);
}; };
export const setSetting = <T>(name: string, newValue: T) => export const setSetting = <T>(name: string, newValue: T): void =>
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue)); setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
export const isFirefox = () => { export const isFirefox = (): boolean => {
const { userAgent } = navigator; const { userAgent } = navigator;
return userAgent.includes("Firefox"); return userAgent.includes("Firefox");
}; };
const canEnableSpatialAudio = () => { const canEnableSpatialAudio = (): boolean => {
// Spatial audio means routing audio through audio contexts. On Chrome, // Spatial audio means routing audio through audio contexts. On Chrome,
// this bypasses the AEC processor and so breaks echo cancellation. // this bypasses the AEC processor and so breaks echo cancellation.
// We only allow spatial audio to be enabled on Firefox which we know // We only allow spatial audio to be enabled on Firefox which we know
@ -83,7 +83,8 @@ export const useSpatialAudio = (): DisableableSetting<boolean> => {
return [false, null]; return [false, null];
}; };
export const useShowInspector = () => useSetting("show-inspector", false); export const useShowInspector = (): Setting<boolean> =>
useSetting("show-inspector", false);
// null = undecided // null = undecided
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => { export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
@ -103,15 +104,15 @@ export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
return [false, null]; return [false, null];
}; };
export const useDeveloperSettingsTab = () => export const useDeveloperSettingsTab = (): Setting<boolean> =>
useSetting("developer-settings-tab", false); useSetting("developer-settings-tab", false);
export const useShowConnectionStats = () => export const useShowConnectionStats = (): Setting<boolean> =>
useSetting("show-connection-stats", false); useSetting("show-connection-stats", false);
export const useAudioInput = () => export const useAudioInput = (): Setting<string | undefined> =>
useSetting<string | undefined>("audio-input", undefined); useSetting<string | undefined>("audio-input", undefined);
export const useAudioOutput = () => export const useAudioOutput = (): Setting<string | undefined> =>
useSetting<string | undefined>("audio-output", undefined); useSetting<string | undefined>("audio-output", undefined);
export const useVideoInput = () => export const useVideoInput = (): Setting<string | undefined> =>
useSetting<string | undefined>("video-input", undefined); useSetting<string | undefined>("video-input", undefined);

View File

@ -35,7 +35,7 @@ export function useCallViewKeyboardShortcuts(
toggleMicrophoneMuted: () => void, toggleMicrophoneMuted: () => void,
toggleLocalVideoMuted: () => void, toggleLocalVideoMuted: () => void,
setMicrophoneMuted: (muted: boolean) => void setMicrophoneMuted: (muted: boolean) => void
) { ): void {
const spacebarHeld = useRef(false); const spacebarHeld = useRef(false);
// These event handlers are set on the window because we want users to be able // These event handlers are set on the window because we want users to be able

View File

@ -24,12 +24,12 @@ import type {
} from "matrix-js-sdk/src/models/typed-event-emitter"; } from "matrix-js-sdk/src/models/typed-event-emitter";
// Shortcut for registering a listener on an EventTarget // Shortcut for registering a listener on an EventTarget
export const useEventTarget = <T extends Event>( export function useEventTarget<T extends Event>(
target: EventTarget | null | undefined, target: EventTarget | null | undefined,
eventType: string, eventType: string,
listener: (event: T) => void, listener: (event: T) => void,
options?: AddEventListenerOptions options?: AddEventListenerOptions
) => { ): void {
useEffect(() => { useEffect(() => {
if (target) { if (target) {
target.addEventListener(eventType, listener as EventListener, options); target.addEventListener(eventType, listener as EventListener, options);
@ -41,10 +41,10 @@ export const useEventTarget = <T extends Event>(
); );
} }
}, [target, eventType, listener, options]); }, [target, eventType, listener, options]);
}; }
// Shortcut for registering a listener on a TypedEventEmitter // Shortcut for registering a listener on a TypedEventEmitter
export const useTypedEventEmitter = < export function useTypedEventEmitter<
Events extends string, Events extends string,
Arguments extends ListenerMap<Events>, Arguments extends ListenerMap<Events>,
T extends Events T extends Events
@ -52,28 +52,28 @@ export const useTypedEventEmitter = <
emitter: TypedEventEmitter<Events, Arguments>, emitter: TypedEventEmitter<Events, Arguments>,
eventType: T, eventType: T,
listener: Listener<Events, Arguments, T> listener: Listener<Events, Arguments, T>
) => { ): void {
useEffect(() => { useEffect(() => {
emitter.on(eventType, listener); emitter.on(eventType, listener);
return () => { return () => {
emitter.off(eventType, listener); emitter.off(eventType, listener);
}; };
}, [emitter, eventType, listener]); }, [emitter, eventType, listener]);
}; }
// Shortcut for registering a listener on an eventemitter3 EventEmitter (ie. what the LiveKit SDK uses) // Shortcut for registering a listener on an eventemitter3 EventEmitter (ie. what the LiveKit SDK uses)
export const useEventEmitterThree = < export function useEventEmitterThree<
EventType extends keyof T, EventType extends keyof T,
T extends EventMap T extends EventMap
>( >(
emitter: EventEmitter<T>, emitter: EventEmitter<T>,
eventType: EventType, eventType: EventType,
listener: T[EventType] listener: T[EventType]
) => { ): void {
useEffect(() => { useEffect(() => {
emitter.on(eventType, listener); emitter.on(eventType, listener);
return () => { return () => {
emitter.off(eventType, listener); emitter.off(eventType, listener);
}; };
}, [emitter, eventType, listener]); }, [emitter, eventType, listener]);
}; }

View File

@ -21,7 +21,7 @@ import { useEventTarget } from "./useEvents";
/** /**
* React hook that tracks whether the given media query matches. * React hook that tracks whether the given media query matches.
*/ */
export const useMediaQuery = (query: string) => { export function useMediaQuery(query: string): boolean {
const mediaQuery = useMemo(() => matchMedia(query), [query]); const mediaQuery = useMemo(() => matchMedia(query), [query]);
const [numChanges, setNumChanges] = useState(0); const [numChanges, setNumChanges] = useState(0);
@ -34,4 +34,4 @@ export const useMediaQuery = (query: string) => {
// We want any change to the update counter to trigger an update here // We want any change to the update counter to trigger an update here
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]); return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
}; }

View File

@ -19,5 +19,5 @@ import { useMediaQuery } from "./useMediaQuery";
/** /**
* @returns Whether the user has requested reduced motion. * @returns Whether the user has requested reduced motion.
*/ */
export const usePrefersReducedMotion = () => export const usePrefersReducedMotion = (): boolean =>
useMediaQuery("(prefers-reduced-motion)"); useMediaQuery("(prefers-reduced-motion)");

View File

@ -20,7 +20,7 @@ import { useEffect } from "react";
/** /**
* React hook that inhibits the device from automatically going to sleep. * React hook that inhibits the device from automatically going to sleep.
*/ */
export const useWakeLock = () => { export function useWakeLock(): void {
useEffect(() => { useEffect(() => {
if ("wakeLock" in navigator) { if ("wakeLock" in navigator) {
let mounted = true; let mounted = true;
@ -28,7 +28,7 @@ export const useWakeLock = () => {
// The lock is automatically released whenever the window goes invisible, // The lock is automatically released whenever the window goes invisible,
// so we need to reacquire it on visiblity changes // so we need to reacquire it on visiblity changes
const onVisiblityChange = async () => { const onVisiblityChange = async (): Promise<void> => {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
try { try {
lock = await navigator.wakeLock.request("screen"); lock = await navigator.wakeLock.request("screen");
@ -57,4 +57,4 @@ export const useWakeLock = () => {
}; };
} }
}, []); }, []);
}; }

View File

@ -78,7 +78,7 @@ export interface SparseGrid {
export function getPaths( export function getPaths(
g: SparseGrid, g: SparseGrid,
dest: number, dest: number,
avoid: (cell: number) => boolean = () => false avoid: (cell: number) => boolean = (): boolean => false
): (number | null)[] { ): (number | null)[] {
const destRow = row(dest, g); const destRow = row(dest, g);
const destColumn = column(dest, g); const destColumn = column(dest, g);
@ -91,7 +91,7 @@ export function getPaths(
edges[dest] = null; edges[dest] = null;
const heap = new TinyQueue([dest], (i) => distances[i]); const heap = new TinyQueue([dest], (i) => distances[i]);
const visit = (curr: number, via: number, distanceVia: number) => { const visit = (curr: number, via: number, distanceVia: number): void => {
if (distanceVia < distances[curr]) { if (distanceVia < distances[curr]) {
distances[curr] = distanceVia; distances[curr] = distanceVia;
edges[curr] = via; edges[curr] = via;
@ -128,7 +128,7 @@ export function getPaths(
return edges; return edges;
} }
const is1By1 = (c: Cell) => c.columns === 1 && c.rows === 1; const is1By1 = (c: Cell): boolean => c.columns === 1 && c.rows === 1;
const findLast1By1Index = (g: SparseGrid): number | null => const findLast1By1Index = (g: SparseGrid): number | null =>
findLastIndex(g.cells, (c) => c !== undefined && is1By1(c)); findLastIndex(g.cells, (c) => c !== undefined && is1By1(c));
@ -257,7 +257,7 @@ function getNextGap(
* along the way. * along the way.
* Precondition: the destination area must consist of only 1×1 tiles. * Precondition: the destination area must consist of only 1×1 tiles.
*/ */
function moveTileUnchecked(g: SparseGrid, from: number, to: number) { function moveTileUnchecked(g: SparseGrid, from: number, to: number): void {
const tile = g.cells[from]!; const tile = g.cells[from]!;
const fromEnd = areaEnd(from, tile.columns, tile.rows, g); const fromEnd = areaEnd(from, tile.columns, tile.rows, g);
const toEnd = areaEnd(to, tile.columns, tile.rows, g); const toEnd = areaEnd(to, tile.columns, tile.rows, g);
@ -333,7 +333,7 @@ function pushTileUp(
g: SparseGrid, g: SparseGrid,
from: number, from: number,
rows: number, rows: number,
avoid: (cell: number) => boolean = () => false avoid: (cell: number) => boolean = (): boolean => false
): number { ): number {
const tile = g.cells[from]!; const tile = g.cells[from]!;
@ -359,7 +359,7 @@ function pushTileUp(
return 0; return 0;
} }
function trimTrailingGaps(g: SparseGrid) { function trimTrailingGaps(g: SparseGrid): void {
// Shrink the array to remove trailing gaps // Shrink the array to remove trailing gaps
const newLength = (findLastIndex(g.cells, (c) => c !== undefined) ?? -1) + 1; const newLength = (findLastIndex(g.cells, (c) => c !== undefined) ?? -1) + 1;
if (newLength !== g.cells.length) g.cells = g.cells.slice(0, newLength); if (newLength !== g.cells.length) g.cells = g.cells.slice(0, newLength);
@ -485,7 +485,7 @@ export function fillGaps(
export function fillGaps( export function fillGaps(
g: SparseGrid, g: SparseGrid,
packLargeTiles = true, packLargeTiles = true,
ignoreGap: (cell: number) => boolean = () => false ignoreGap: (cell: number) => boolean = (): boolean => false
): SparseGrid { ): SparseGrid {
const lastGap = findLastIndex( const lastGap = findLastIndex(
g.cells, g.cells,
@ -785,7 +785,11 @@ export function setTileSize<G extends Grid | SparseGrid>(
gridWithoutTile.cells[i] = undefined; gridWithoutTile.cells[i] = undefined;
}); });
const placeTile = (to: number, toEnd: number, grid: Grid | SparseGrid) => { const placeTile = (
to: number,
toEnd: number,
grid: Grid | SparseGrid
): void => {
forEachCellInArea(to, toEnd, grid, (_c, i) => { forEachCellInArea(to, toEnd, grid, (_c, i) => {
grid.cells[i] = { grid.cells[i] = {
item: fromCell.item, item: fromCell.item,
@ -904,7 +908,7 @@ export function resize(g: Grid, columns: number): Grid {
/** /**
* Promotes speakers to the first page of the grid. * Promotes speakers to the first page of the grid.
*/ */
export function promoteSpeakers(g: SparseGrid) { export function promoteSpeakers(g: SparseGrid): void {
// This is all a bit of a hack right now, because we don't know if the designs // This is all a bit of a hack right now, because we don't know if the designs
// will stick with this approach in the long run // will stick with this approach in the long run
// We assume that 4 rows are probably about 1 page // We assume that 4 rows are probably about 1 page

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ComponentType, useCallback, useMemo, useRef } from "react"; import { ComponentType, ReactNode, useCallback, useMemo, useRef } from "react";
import type { RectReadOnly } from "react-use-measure"; import type { RectReadOnly } from "react-use-measure";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
@ -98,16 +98,33 @@ export const useLayoutStates = (): LayoutStatesMap => {
return layoutStates.current as LayoutStatesMap; return layoutStates.current as LayoutStatesMap;
}; };
interface UseLayout<State, T> {
state: State;
orderedItems: TileDescriptor<T>[];
generation: number;
canDragTile: (tile: TileDescriptor<T>) => boolean;
dragTile: (
from: TileDescriptor<T>,
to: TileDescriptor<T>,
xPositionOnFrom: number,
yPositionOnFrom: number,
xPositionOnTo: number,
yPositionOnTo: number
) => void;
toggleFocus: ((tile: TileDescriptor<T>) => void) | undefined;
slots: ReactNode;
}
/** /**
* Hook which uses the provided layout system to arrange a set of items into a * Hook which uses the provided layout system to arrange a set of items into a
* concrete layout state, and provides callbacks for user interaction. * concrete layout state, and provides callbacks for user interaction.
*/ */
export const useLayout = <State, T>( export function useLayout<State, T>(
layout: Layout<State>, layout: Layout<State>,
items: TileDescriptor<T>[], items: TileDescriptor<T>[],
bounds: RectReadOnly, bounds: RectReadOnly,
layoutStates: LayoutStatesMap layoutStates: LayoutStatesMap
) => { ): UseLayout<State, T> {
const prevLayout = useRef<Layout<unknown>>(); const prevLayout = useRef<Layout<unknown>>();
const prevState = layoutStates.get(layout); const prevState = layoutStates.get(layout);
@ -169,10 +186,10 @@ export const useLayout = <State, T>(
toggleFocus: useMemo( toggleFocus: useMemo(
() => () =>
layout.toggleFocus && layout.toggleFocus &&
((tile: TileDescriptor<T>) => ((tile: TileDescriptor<T>): void =>
setState((s) => layout.toggleFocus!(s, tile))), setState((s) => layout.toggleFocus!(s, tile))),
[layout, setState] [layout, setState]
), ),
slots: <layout.Slots s={state} />, slots: <layout.Slots s={state} />,
}; };
}; }

View File

@ -81,7 +81,7 @@ export function NewVideoGrid<T>({
disableAnimations, disableAnimations,
layoutStates, layoutStates,
children, children,
}: Props<T>) { }: Props<T>): ReactNode {
// Overview: This component lays out tiles by rendering an invisible template // Overview: This component lays out tiles by rendering an invisible template
// grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to // grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to
// get the dimensions of each slot, feeding these numbers back into // get the dimensions of each slot, feeding these numbers back into
@ -210,7 +210,7 @@ export function NewVideoGrid<T>({
springRef.start(); springRef.start();
}, [tiles, springRef]); }, [tiles, springRef]);
const animateDraggedTile = (endOfGesture: boolean) => { const animateDraggedTile = (endOfGesture: boolean): void => {
const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!; const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!;
const tile = tiles.find((t) => t.item.id === tileId)!; const tile = tiles.find((t) => t.item.id === tileId)!;
@ -226,7 +226,8 @@ export function NewVideoGrid<T>({
y: tile.y, y: tile.y,
width: tile.width, width: tile.width,
height: tile.height, height: tile.height,
immediate: disableAnimations || ((key) => key === "zIndex"), immediate:
disableAnimations || ((key): boolean => key === "zIndex"),
// Allow the tile's position to settle before pushing its // Allow the tile's position to settle before pushing its
// z-index back down // z-index back down
delay: (key) => (key === "zIndex" ? 500 : 0), delay: (key) => (key === "zIndex" ? 500 : 0),
@ -239,7 +240,8 @@ export function NewVideoGrid<T>({
y: tileY, y: tileY,
immediate: immediate:
disableAnimations || disableAnimations ||
((key) => key === "zIndex" || key === "x" || key === "y"), ((key): boolean =>
key === "zIndex" || key === "x" || key === "y"),
} }
); );
@ -286,7 +288,7 @@ export function NewVideoGrid<T>({
// @ts-ignore // @ts-ignore
last, last,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0] }: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => { ): void => {
if (tap) { if (tap) {
const now = Date.now(); const now = Date.now();

View File

@ -17,6 +17,7 @@ limitations under the License.
import { import {
ComponentProps, ComponentProps,
Key, Key,
MutableRefObject,
ReactNode, ReactNode,
Ref, Ref,
useCallback, useCallback,
@ -112,7 +113,7 @@ export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
const GAP = 8; const GAP = 8;
function useIsMounted() { function useIsMounted(): MutableRefObject<boolean> {
const isMountedRef = useRef<boolean>(false); const isMountedRef = useRef<boolean>(false);
useEffect(() => { useEffect(() => {
@ -478,7 +479,7 @@ function centerTiles(
gridHeight: number, gridHeight: number,
offsetLeft: number, offsetLeft: number,
offsetTop: number offsetTop: number
) { ): TilePosition[] {
const bounds = getSubGridBoundingBox(positions); const bounds = getSubGridBoundingBox(positions);
const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft; const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
@ -493,7 +494,7 @@ function applyTileOffsets(
positions: TilePosition[], positions: TilePosition[],
leftOffset: number, leftOffset: number,
topOffset: number topOffset: number
) { ): TilePosition[] {
for (const position of positions) { for (const position of positions) {
position.x += leftOffset; position.x += leftOffset;
position.y += topOffset; position.y += topOffset;
@ -623,7 +624,7 @@ function getSubGridPositions(
tileAspectRatio: number, tileAspectRatio: number,
gridWidth: number, gridWidth: number,
gridHeight: number gridHeight: number
) { ): TilePosition[] {
if (tileCount === 0) { if (tileCount === 0) {
return []; return [];
} }
@ -726,7 +727,11 @@ function displayedTileCount(
// Sets the 'order' property on tiles based on the layout param and // Sets the 'order' property on tiles based on the layout param and
// other properties of the tiles, eg. 'focused' and 'presenter' // other properties of the tiles, eg. 'focused' and 'presenter'
function reorderTiles<T>(tiles: Tile<T>[], layout: Layout, displayedTile = -1) { function reorderTiles<T>(
tiles: Tile<T>[],
layout: Layout,
displayedTile = -1
): void {
// We use a special layout for 1:1 to always put the local tile first. // We use a special layout for 1:1 to always put the local tile first.
// We only do this if there are two tiles (obviously) and exactly one // We only do this if there are two tiles (obviously) and exactly one
// of them is local: during startup we can have tiles from other users // of them is local: during startup we can have tiles from other users
@ -841,7 +846,7 @@ export function VideoGrid<T>({
layout, layout,
disableAnimations, disableAnimations,
children, children,
}: VideoGridProps<T>) { }: VideoGridProps<T>): ReactNode {
// Place the PiP in the bottom right corner by default // Place the PiP in the bottom right corner by default
const [pipXRatio, setPipXRatio] = useState(1); const [pipXRatio, setPipXRatio] = useState(1);
const [pipYRatio, setPipYRatio] = useState(1); const [pipYRatio, setPipYRatio] = useState(1);
@ -1208,7 +1213,7 @@ export function VideoGrid<T>({
// @ts-ignore // @ts-ignore
event, event,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0] }: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => { ): void => {
event.preventDefault(); event.preventDefault();
if (tap) { if (tap) {

View File

@ -97,12 +97,12 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
); );
useEffect(() => { useEffect(() => {
if (member) { if (member) {
const updateName = () => { const updateName = (): void => {
setDisplayName(member.rawDisplayName); setDisplayName(member.rawDisplayName);
}; };
member!.on(RoomMemberEvent.Name, updateName); member!.on(RoomMemberEvent.Name, updateName);
return () => { return (): void => {
member!.removeListener(RoomMemberEvent.Name, updateName); member!.removeListener(RoomMemberEvent.Name, updateName);
}; };
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ChangeEvent, useState } from "react"; import { ChangeEvent, FC, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { RemoteParticipant, Track } from "livekit-client"; import { RemoteParticipant, Track } from "livekit-client";
@ -29,7 +29,7 @@ interface LocalVolumeProps {
content: TileContent; content: TileContent;
} }
const LocalVolume: React.FC<LocalVolumeProps> = ({ const LocalVolume: FC<LocalVolumeProps> = ({
participant, participant,
content, content,
}: LocalVolumeProps) => { }: LocalVolumeProps) => {
@ -42,7 +42,7 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
participant.getVolume(source) ?? 0 participant.getVolume(source) ?? 0
); );
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => { const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>): void => {
const value: number = +event.target.value; const value: number = +event.target.value;
setLocalVolume(value); setLocalVolume(value);
participant.setVolume(value, source); participant.setVolume(value, source);
@ -72,7 +72,11 @@ interface Props {
onDismiss: () => void; onDismiss: () => void;
} }
export const VideoTileSettingsModal = ({ data, open, onDismiss }: Props) => { export const VideoTileSettingsModal: FC<Props> = ({
data,
open,
onDismiss,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

View File

@ -68,7 +68,7 @@ interface WidgetHelpers {
* is declared and initialized on the top level because the widget messaging * is declared and initialized on the top level because the widget messaging
* needs to be set up ASAP on load to ensure it doesn't miss any requests. * needs to be set up ASAP on load to ensure it doesn't miss any requests.
*/ */
export const widget: WidgetHelpers | null = (() => { export const widget = ((): WidgetHelpers | null => {
try { try {
const query = new URLSearchParams(window.location.search); const query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId"); const widgetId = query.get("widgetId");
@ -161,7 +161,7 @@ export const widget: WidgetHelpers | null = (() => {
); );
const clientPromise = new Promise<MatrixClient>((resolve) => { const clientPromise = new Promise<MatrixClient>((resolve) => {
(async () => { (async (): Promise<void> => {
// wait for the config file to be ready (we load very early on so it might not // wait for the config file to be ready (we load very early on so it might not
// be otherwise) // be otherwise)
await Config.init(); await Config.init();

View File

@ -13,7 +13,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"moduleResolution": "node", "moduleResolution": "node",
"declaration": true "declaration": true,
// TODO: Enable the following options later. // TODO: Enable the following options later.
// "forceConsistentCasingInFileNames": true, // "forceConsistentCasingInFileNames": true,
@ -23,6 +23,8 @@
// "noPropertyAccessFromIndexSignature": true, // "noPropertyAccessFromIndexSignature": true,
// "noUncheckedIndexedAccess": true, // "noUncheckedIndexedAccess": true,
// "noUnusedParameters": true, // "noUnusedParameters": true,
"plugins": [{ "name": "typescript-eslint-language-service" }]
}, },
"include": [ "include": [
"./node_modules/matrix-js-sdk/src/@types/*.d.ts", "./node_modules/matrix-js-sdk/src/@types/*.d.ts",

View File

@ -6341,6 +6341,11 @@ bufrw@^1.2.1:
hexer "^1.5.0" hexer "^1.5.0"
xtend "^4.0.0" xtend "^4.0.0"
builtin-modules@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
builtin-status-codes@^3.0.0: builtin-status-codes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@ -6627,7 +6632,7 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
ci-info@^3.2.0: ci-info@^3.2.0, ci-info@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
@ -6667,6 +6672,13 @@ clean-css@^4.2.3:
dependencies: dependencies:
source-map "~0.6.0" source-map "~0.6.0"
clean-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7"
integrity sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==
dependencies:
escape-string-regexp "^1.0.5"
clean-stack@^2.0.0: clean-stack@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@ -8516,6 +8528,11 @@ eslint-module-utils@^2.8.0:
dependencies: dependencies:
debug "^3.2.7" debug "^3.2.7"
eslint-plugin-deprecate@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-deprecate/-/eslint-plugin-deprecate-0.8.2.tgz#2399730da3d72aa6d3704400995932f52f0482cf"
integrity sha512-THs60MUqJoHtrF6F8eNUnyU0ER6p4wUX7yyoUZQdBDPFiE9kzZTo4CgRKZicUVj5cjXLT76nW+QdSZwZKtjLIA==
eslint-plugin-import@^2.26.0: eslint-plugin-import@^2.26.0:
version "2.28.1" version "2.28.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4"
@ -8561,10 +8578,10 @@ eslint-plugin-jsx-a11y@^6.5.1:
object.fromentries "^2.0.6" object.fromentries "^2.0.6"
semver "^6.3.0" semver "^6.3.0"
eslint-plugin-matrix-org@^0.4.0: eslint-plugin-matrix-org@^1.2.1:
version "0.4.0" version "1.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.4.0.tgz#de2d2db1cd471d637728133ce9a2b921690e5cd1" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-1.2.1.tgz#76d1505daa93fb99ba4156008b9b32f57682c9b1"
integrity sha512-yVkNwtc33qtrQB4PPzpU+PUdFzdkENPan3JF4zhtAQJRUYXyvKEXnYSrXLUWYRXoYFxs9LbyI2CnhJL/RnHJaQ== integrity sha512-A3cDjhG7RHwfCS8o3bOip8hSCsxtmgk2ahvqE5v/Ic2kPEZxixY6w8zLj7hFGsrRmPSEpLWqkVLt8uvQBapiQA==
eslint-plugin-react-hooks@^4.5.0: eslint-plugin-react-hooks@^4.5.0:
version "4.6.0" version "4.6.0"
@ -8593,6 +8610,27 @@ eslint-plugin-react@^7.29.4:
semver "^6.3.1" semver "^6.3.1"
string.prototype.matchall "^4.0.8" string.prototype.matchall "^4.0.8"
eslint-plugin-unicorn@^48.0.1:
version "48.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-48.0.1.tgz#a6573bc1687ae8db7121fdd8f92394b6549a6959"
integrity sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw==
dependencies:
"@babel/helper-validator-identifier" "^7.22.5"
"@eslint-community/eslint-utils" "^4.4.0"
ci-info "^3.8.0"
clean-regexp "^1.0.0"
esquery "^1.5.0"
indent-string "^4.0.0"
is-builtin-module "^3.2.1"
jsesc "^3.0.2"
lodash "^4.17.21"
pluralize "^8.0.0"
read-pkg-up "^7.0.1"
regexp-tree "^0.1.27"
regjsparser "^0.10.0"
semver "^7.5.4"
strip-indent "^3.0.0"
eslint-scope@5.1.1: eslint-scope@5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -8679,7 +8717,7 @@ esprima@^4.0.0, esprima@^4.0.1:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.2: esquery@^1.4.2, esquery@^1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
@ -10283,6 +10321,13 @@ is-buffer@^2.0.0:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
is-builtin-module@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169"
integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==
dependencies:
builtin-modules "^3.3.0"
is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
@ -11187,6 +11232,11 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
jsesc@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==
jsesc@~0.5.0: jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
@ -12850,6 +12900,11 @@ pkg-dir@^7.0.0:
dependencies: dependencies:
find-up "^6.3.0" find-up "^6.3.0"
pluralize@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
pnp-webpack-plugin@1.6.4: pnp-webpack-plugin@1.6.4:
version "1.6.4" version "1.6.4"
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@ -13857,6 +13912,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2" extend-shallow "^3.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
regexp-tree@^0.1.27:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==
regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e"
@ -13878,6 +13938,13 @@ regexpu-core@^5.3.1:
unicode-match-property-ecmascript "^2.0.0" unicode-match-property-ecmascript "^2.0.0"
unicode-match-property-value-ecmascript "^2.1.0" unicode-match-property-value-ecmascript "^2.1.0"
regjsparser@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.10.0.tgz#b1ed26051736b436f22fdec1c8f72635f9f44892"
integrity sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==
dependencies:
jsesc "~0.5.0"
regjsparser@^0.9.1: regjsparser@^0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709"
@ -15454,6 +15521,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-eslint-language-service@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/typescript-eslint-language-service/-/typescript-eslint-language-service-5.0.5.tgz#b0f06290df01c55771f2674d261512d17e7a39ad"
integrity sha512-b7gWXpwSTqMVKpPX3WttNZEyVAMKs/2jsHKF79H+qaD6mjzCyU5jboJe/lOZgLJD+QRsXCr0GjIVxvl5kI1NMw==
typescript@^4.2.4: typescript@^4.2.4:
version "4.8.4" version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"