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 = {
plugins: ["matrix-org"],
extends: [
"prettier",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
"prettier",
],
parserOptions: {
ecmaVersion: 2018,
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json"],
},
@ -15,27 +33,11 @@ module.exports = {
browser: true,
node: true,
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"jsx-a11y/media-has-caption": ["off"],
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
"jsx-a11y/media-has-caption": "off",
"deprecate/import": "off", // Disabled because it crashes the linter
},
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: {
react: {
version: "detect",

View File

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

View File

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

View File

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

View File

@ -82,7 +82,8 @@ export type SetClientParams = {
const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = () => useContext(ClientContext);
export const useClientState = (): ClientState | undefined =>
useContext(ClientContext);
export function useClient(): {
client?: MatrixClient;
@ -408,8 +409,8 @@ export interface Session {
tempPassword?: string;
}
const clearSession = () => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) =>
const clearSession = (): void => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session): void =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store");
@ -423,4 +424,5 @@ const loadSession = (): Session | undefined => {
const clientIsDisconnected = (
syncState: SyncState,
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 { HTMLAttributes, ReactNode } from "react";
import { FC, HTMLAttributes, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import styles from "./DisconnectedBanner.module.css";
import { ValidClientState, useClientState } from "./ClientContext";
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> {
interface Props extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
className?: string;
}
export function DisconnectedBanner({
export const DisconnectedBanner: FC<Props> = ({
children,
className,
...rest
}: DisconnectedBannerProps) {
}) => {
const { t } = useTranslation();
const clientState = useClientState();
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 { FC } from "react";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import { ReactComponent as LockOffIcon } from "./icons/LockOff.svg";
import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => {
export const E2EEBanner: FC = () => {
const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,18 +36,22 @@ export class CallEndedTracker {
maxParticipantsCount: 0,
};
cacheStartCall(time: Date) {
public cacheStartCall(time: Date): void {
this.cache.startTime = time;
}
cacheParticipantCountChanged(count: number) {
public cacheParticipantCountChanged(count: number): void {
this.cache.maxParticipantsCount = Math.max(
count,
this.cache.maxParticipantsCount
);
}
track(callId: string, callParticipantsNow: number, sendInstantly: boolean) {
public track(
callId: string,
callParticipantsNow: number,
sendInstantly: boolean
): void {
PosthogAnalytics.instance.trackEvent<CallEnded>(
{
eventName: "CallEnded",
@ -67,7 +71,7 @@ interface CallStarted extends IPosthogEvent {
}
export class CallStartedTracker {
track(callId: string) {
public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted",
callId: callId,
@ -86,19 +90,19 @@ export class SignupTracker {
signupEnd: new Date(0),
};
cacheSignupStart(time: Date) {
public cacheSignupStart(time: Date): void {
this.cache.signupStart = time;
}
getSignupEndTime() {
public getSignupEndTime(): Date {
return this.cache.signupEnd;
}
cacheSignupEnd(time: Date) {
public cacheSignupEnd(time: Date): void {
this.cache.signupEnd = time;
}
track() {
public track(): void {
PosthogAnalytics.instance.trackEvent<Signup>({
eventName: "Signup",
signupDuration: Date.now() - this.cache.signupStart.getTime(),
@ -112,7 +116,7 @@ interface Login extends IPosthogEvent {
}
export class LoginTracker {
track() {
public track(): void {
PosthogAnalytics.instance.trackEvent<Login>({
eventName: "Login",
});
@ -127,7 +131,7 @@ interface MuteMicrophone {
}
export class MuteMicrophoneTracker {
track(targetIsMute: boolean, callId: string) {
public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
eventName: "MuteMicrophone",
targetMuteState: targetIsMute ? "mute" : "unmute",
@ -143,7 +147,7 @@ interface MuteCamera {
}
export class MuteCameraTracker {
track(targetIsMute: boolean, callId: string) {
public track(targetIsMute: boolean, callId: string): void {
PosthogAnalytics.instance.trackEvent<MuteCamera>({
eventName: "MuteCamera",
targetMuteState: targetIsMute ? "mute" : "unmute",
@ -158,7 +162,7 @@ interface UndecryptableToDeviceEvent {
}
export class UndecryptableToDeviceEventTracker {
track(callId: string) {
public track(callId: string): void {
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
eventName: "UndecryptableToDeviceEvent",
callId,
@ -174,7 +178,7 @@ interface QualitySurveyEvent {
}
export class QualitySurveyEventTracker {
track(callId: string, feedbackText: string, stars: number) {
public track(callId: string, feedbackText: string, stars: number): void {
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
eventName: "QualitySurvey",
callId,
@ -190,7 +194,7 @@ interface CallDisconnectedEvent {
}
export class CallDisconnectedEventTracker {
track(reason?: DisconnectReason) {
public track(reason?: DisconnectReason): void {
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
eventName: "CallDisconnected",
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
*/
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
Promise.resolve().then(() => {
switch (span.name) {
@ -55,7 +55,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
});
}
onEnd(span: ReadableSpan): void {
public onEnd(span: ReadableSpan): void {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span);
@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
/**
* Shutdown the processor.
*/
shutdown(): Promise<void> {
public shutdown(): Promise<void> {
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 {
SpanProcessor,
@ -6,7 +22,21 @@ import {
Span,
} from "@opentelemetry/sdk-trace-base";
const dumpAttributes = (attr: Attributes) =>
const dumpAttributes = (
attr: Attributes
): {
key: string;
type:
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
value: AttributeValue | undefined;
}[] =>
Object.entries(attr).map(([key, value]) => ({
key,
type: typeof value,
@ -20,13 +50,13 @@ const dumpAttributes = (attr: Attributes) =>
export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = [];
async forceFlush(): Promise<void> {}
public async forceFlush(): Promise<void> {}
onStart(span: Span): void {
public onStart(span: Span): void {
this.spans.push(span);
}
onEnd(): void {}
public onEnd(): void {}
/**
* Dumps the spans collected so far as Jaeger-compatible JSON.
@ -110,5 +140,5 @@ export class RageshakeSpanProcessor implements SpanProcessor {
});
}
async shutdown(): Promise<void> {}
public async shutdown(): Promise<void> {}
}

View File

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

View File

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

View File

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

View File

@ -34,7 +34,11 @@ interface RecaptchaPromiseRef {
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 [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>();
@ -42,7 +46,7 @@ export const useRecaptcha = (sitekey?: string) => {
useEffect(() => {
if (!sitekey) return;
const onRecaptchaLoaded = () => {
const onRecaptchaLoaded = (): void => {
if (!document.getElementById(recaptchaId)) return;
window.grecaptcha.render(recaptchaId, {
@ -90,11 +94,11 @@ export const useRecaptcha = (sitekey?: string) => {
});
promiseRef.current = {
resolve: (value) => {
resolve: (value): void => {
resolve(value);
observer.disconnect();
},
reject: (error) => {
reject: (error): void => {
reject(error);
observer.disconnect();
},
@ -119,4 +123,4 @@ export const useRecaptcha = (sitekey?: string) => {
}, []);
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
limitations under the License.
*/
import { forwardRef } from "react";
import { FC, forwardRef } from "react";
import { PressEvent } from "@react-types/shared";
import classNames from "classnames";
import { useButton } from "@react-aria/button";
@ -135,14 +135,11 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
}
);
export function MicButton({
muted,
...rest
}: {
export const MicButton: FC<{
muted: boolean;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
}> = ({ muted, ...rest }) => {
const { t } = useTranslation();
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
const label = muted ? t("Unmute microphone") : t("Mute microphone");
@ -154,16 +151,13 @@ export function MicButton({
</Button>
</Tooltip>
);
}
};
export function VideoButton({
muted,
...rest
}: {
export const VideoButton: FC<{
muted: boolean;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
}> = ({ muted, ...rest }) => {
const { t } = useTranslation();
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
const label = muted ? t("Start video") : t("Stop video");
@ -175,18 +169,14 @@ export function VideoButton({
</Button>
</Tooltip>
);
}
};
export function ScreenshareButton({
enabled,
className,
...rest
}: {
export const ScreenshareButton: FC<{
enabled: boolean;
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
}> = ({ enabled, className, ...rest }) => {
const { t } = useTranslation();
const label = enabled ? t("Sharing screen") : t("Share screen");
@ -197,16 +187,13 @@ export function ScreenshareButton({
</Button>
</Tooltip>
);
}
};
export function HangupButton({
className,
...rest
}: {
export const HangupButton: FC<{
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
}> = ({ className, ...rest }) => {
const { t } = useTranslation();
return (
@ -220,16 +207,13 @@ export function HangupButton({
</Button>
</Tooltip>
);
}
};
export function SettingsButton({
className,
...rest
}: {
export const SettingsButton: FC<{
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
}> = ({ className, ...rest }) => {
const { t } = useTranslation();
return (
@ -239,7 +223,7 @@ export function SettingsButton({
</Button>
</Tooltip>
);
}
};
interface AudioButtonProps extends Omit<Props, "variant"> {
/**
@ -248,7 +232,7 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
volume: number;
}
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
const { t } = useTranslation();
return (
@ -258,16 +242,16 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) {
</Button>
</Tooltip>
);
}
};
interface FullscreenButtonProps extends Omit<Props, "variant"> {
fullscreen?: boolean;
}
export function FullscreenButton({
export const FullscreenButton: FC<FullscreenButtonProps> = ({
fullscreen,
...rest
}: FullscreenButtonProps) {
}) => {
const { t } = useTranslation();
const Icon = fullscreen ? FullscreenExit : Fullscreen;
const label = fullscreen ? t("Exit full screen") : t("Full screen");
@ -279,4 +263,4 @@ export function FullscreenButton({
</Button>
</Tooltip>
);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ export interface MediaDevices {
function useObservableState<T>(
observable: Observable<T> | undefined,
startWith: T
) {
): T {
const [state, setState] = useState<T>(startWith);
useEffect(() => {
// 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
@ -215,7 +216,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
* default because it may involve requesting additional permissions from the
* user.
*/
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true
): void =>
useEffect(() => {
if (enabled) {
context.startUsingDeviceNames();

View File

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

View File

@ -119,7 +119,7 @@ export function useECConnectionState(
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`
);
(async () => {
(async (): Promise<void> => {
setSwitchingFocus(true);
await livekitRoom?.disconnect();
setIsInDoConnect(true);

View File

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

View File

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

View File

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

View File

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

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 {
CallFeedStats,
@ -10,9 +26,9 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
callFeed: CallFeedStats
) {
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 opentelemetry, { Span } from "@opentelemetry/api";
@ -8,8 +24,8 @@ export class OTelCallMediaStreamTrackSpan {
private prev: TrackStats;
public constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly streamSpan: Span,
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly streamSpan: Span,
data: TrackStats
) {
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 {
TrackStats,
@ -13,9 +29,9 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr
currentDirection: string;
};
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
stats: TransceiverStats
) {
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>>();
constructor(private groupCall: GroupCall, client: MatrixClient) {
public constructor(private groupCall: GroupCall, client: MatrixClient) {
const clientId = client.getUserId();
if (clientId) {
this.myUserId = clientId;
@ -76,14 +76,14 @@ export class OTelGroupCallMembership {
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
}
dispose() {
public dispose(): void {
this.groupCall.removeListener(
GroupCallEvent.CallsChanged,
this.onCallsChanged
);
}
public onJoinCall() {
public onJoinCall(): void {
if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started");
@ -114,7 +114,7 @@ export class OTelGroupCallMembership {
this.callMembershipSpan?.addEvent("matrix.joinCall");
}
public onLeaveCall() {
public onLeaveCall(): void {
if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended");
return;
@ -127,7 +127,7 @@ export class OTelGroupCallMembership {
this.groupCallContext = undefined;
}
public onUpdateRoomState(event: MatrixEvent) {
public onUpdateRoomState(event: MatrixEvent): void {
if (
!event ||
(!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 [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) {
@ -179,9 +179,9 @@ export class OTelGroupCallMembership {
this.callsByCallId.delete(callTrackingInfo.call.callId);
}
}
};
}
public onCallStateChange(call: MatrixCall, newState: CallState) {
public onCallStateChange(call: MatrixCall, newState: CallState): void {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`);
@ -193,7 +193,7 @@ export class OTelGroupCallMembership {
});
}
public onSendEvent(call: MatrixCall, event: VoipEvent) {
public onSendEvent(call: MatrixCall, event: VoipEvent): void {
const eventType = event.eventType as string;
if (
!eventType.startsWith("m.call") &&
@ -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
// a call already associated (in principle we could receive
// 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", {
"matrix.microphone.muted": newValue,
});
}
public onSetMicrophoneMuted(setMuted: boolean) {
public onSetMicrophoneMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted,
});
}
public onToggleLocalVideoMuted(newValue: boolean) {
public onToggleLocalVideoMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue,
});
}
public onSetLocalVideoMuted(setMuted: boolean) {
public onSetLocalVideoMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted,
});
}
public onToggleScreensharing(newValue: boolean) {
public onToggleScreensharing(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue,
});
}
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) {
public onSpeaking(
member: RoomMember,
deviceId: string,
speaking: boolean
): void {
if (speaking) {
// Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member);
@ -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);
if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`);
@ -321,17 +325,19 @@ export class OTelGroupCallMembership {
callTrackingInfo.span.recordException(error);
}
public onGroupCallError(error: GroupCallError) {
public onGroupCallError(error: GroupCallError): void {
this.callMembershipSpan?.recordException(error);
}
public onUndecryptableToDevice(event: MatrixEvent) {
public onUndecryptableToDevice(event: MatrixEvent): void {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(),
});
}
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) {
public onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report.report?.callId;
@ -362,7 +368,7 @@ export class OTelGroupCallMembership {
public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>
) {
): void {
this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport,
statsReport.report
@ -371,7 +377,7 @@ export class OTelGroupCallMembership {
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>
) {
): void {
this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport,
statsReport.report
@ -431,7 +437,7 @@ export class OTelGroupCallMembership {
public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport>
) {
): void {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, isE2EESupported } from "livekit-client";
@ -64,14 +64,14 @@ interface Props {
rtcSession: MatrixRTCSession;
}
export function GroupCallView({
export const GroupCallView: FC<Props> = ({
client,
isPasswordlessUser,
confineToRoom,
preload,
hideHeader,
rtcSession,
}: Props) {
}) => {
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
@ -135,7 +135,9 @@ export function GroupCallView({
useEffect(() => {
if (widget && preload) {
// 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
// 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
@ -247,9 +249,11 @@ export function GroupCallView({
useEffect(() => {
if (widget && isJoined) {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
const onHangup = async (
ev: CustomEvent<IWidgetApiRequest>
): Promise<void> => {
leaveRTCSession(rtcSession);
await widget!.api.transport.reply(ev.detail, {});
widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false);
};
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
@ -388,7 +392,7 @@ export function GroupCallView({
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => enterRTCSession(rtcSession)}
onEnter={(): void => enterRTCSession(rtcSession)}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
FC,
ReactNode,
Ref,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { logger } from "matrix-js-sdk/src/logger";
@ -91,7 +100,7 @@ export interface ActiveCallProps
e2eeConfig?: E2EEConfig;
}
export function ActiveCall(props: ActiveCallProps) {
export const ActiveCall: FC<ActiveCallProps> = (props) => {
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
const { livekitRoom, connState } = useLiveKit(
props.muteStates,
@ -112,7 +121,7 @@ export function ActiveCall(props: ActiveCallProps) {
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider>
);
}
};
export interface InCallViewProps {
client: MatrixClient;
@ -128,7 +137,7 @@ export interface InCallViewProps {
onShareClick: (() => void) | null;
}
export function InCallView({
export const InCallView: FC<InCallViewProps> = ({
client,
matrixInfo,
rtcSession,
@ -140,7 +149,7 @@ export function InCallView({
otelGroupCallMembership,
connState,
onShareClick,
}: InCallViewProps) {
}) => {
const { t } = useTranslation();
usePreventScroll();
useWakeLock();
@ -211,13 +220,13 @@ export function InCallView({
useEffect(() => {
if (widget) {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
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");
await widget!.api.transport.reply(ev.detail, {});
widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
@ -296,7 +305,7 @@ export function InCallView({
disableAnimations={prefersReducedMotion || isSafari}
layoutStates={layoutStates}
>
{(props) => (
{(props): ReactNode => (
<VideoTile
maximised={false}
fullscreen={false}
@ -444,7 +453,7 @@ export function InCallView({
/>
</div>
);
}
};
function findMatrixMember(
room: MatrixRoom,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,8 +19,9 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { useRoomState } from "./useRoomState";
export const useRoomAvatar = (room: Room) =>
useRoomState(
export function useRoomAvatar(room: Room): string | null {
return useRoomState(
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.eventCallStarted.track(rtcSession.room.roomId);
@ -47,7 +47,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession) {
rtcSession.joinRoomSession([makeFocus(livekitAlias)]);
}
export function leaveRTCSession(rtcSession: MatrixRTCSession) {
export function leaveRTCSession(rtcSession: MatrixRTCSession): void {
//groupCallOTelMembership?.onLeaveCall();
rtcSession.leaveRoomSession();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,15 +55,15 @@ export const getSetting = <T>(name: string, defaultValue: T): T => {
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));
export const isFirefox = () => {
export const isFirefox = (): boolean => {
const { userAgent } = navigator;
return userAgent.includes("Firefox");
};
const canEnableSpatialAudio = () => {
const canEnableSpatialAudio = (): boolean => {
// Spatial audio means routing audio through audio contexts. On Chrome,
// this bypasses the AEC processor and so breaks echo cancellation.
// 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];
};
export const useShowInspector = () => useSetting("show-inspector", false);
export const useShowInspector = (): Setting<boolean> =>
useSetting("show-inspector", false);
// null = undecided
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
@ -103,15 +104,15 @@ export const useEnableE2EE = (): DisableableSetting<boolean | null> => {
return [false, null];
};
export const useDeveloperSettingsTab = () =>
export const useDeveloperSettingsTab = (): Setting<boolean> =>
useSetting("developer-settings-tab", false);
export const useShowConnectionStats = () =>
export const useShowConnectionStats = (): Setting<boolean> =>
useSetting("show-connection-stats", false);
export const useAudioInput = () =>
export const useAudioInput = (): Setting<string | undefined> =>
useSetting<string | undefined>("audio-input", undefined);
export const useAudioOutput = () =>
export const useAudioOutput = (): Setting<string | undefined> =>
useSetting<string | undefined>("audio-output", undefined);
export const useVideoInput = () =>
export const useVideoInput = (): Setting<string | undefined> =>
useSetting<string | undefined>("video-input", undefined);

View File

@ -35,7 +35,7 @@ export function useCallViewKeyboardShortcuts(
toggleMicrophoneMuted: () => void,
toggleLocalVideoMuted: () => void,
setMicrophoneMuted: (muted: boolean) => void
) {
): void {
const spacebarHeld = useRef(false);
// 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";
// Shortcut for registering a listener on an EventTarget
export const useEventTarget = <T extends Event>(
export function useEventTarget<T extends Event>(
target: EventTarget | null | undefined,
eventType: string,
listener: (event: T) => void,
options?: AddEventListenerOptions
) => {
): void {
useEffect(() => {
if (target) {
target.addEventListener(eventType, listener as EventListener, options);
@ -41,10 +41,10 @@ export const useEventTarget = <T extends Event>(
);
}
}, [target, eventType, listener, options]);
};
}
// Shortcut for registering a listener on a TypedEventEmitter
export const useTypedEventEmitter = <
export function useTypedEventEmitter<
Events extends string,
Arguments extends ListenerMap<Events>,
T extends Events
@ -52,28 +52,28 @@ export const useTypedEventEmitter = <
emitter: TypedEventEmitter<Events, Arguments>,
eventType: T,
listener: Listener<Events, Arguments, T>
) => {
): void {
useEffect(() => {
emitter.on(eventType, listener);
return () => {
emitter.off(eventType, listener);
};
}, [emitter, eventType, listener]);
};
}
// 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,
T extends EventMap
>(
emitter: EventEmitter<T>,
eventType: EventType,
listener: T[EventType]
) => {
): void {
useEffect(() => {
emitter.on(eventType, listener);
return () => {
emitter.off(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.
*/
export const useMediaQuery = (query: string) => {
export function useMediaQuery(query: string): boolean {
const mediaQuery = useMemo(() => matchMedia(query), [query]);
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
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => mediaQuery.matches, [mediaQuery, numChanges]);
};
}

View File

@ -19,5 +19,5 @@ import { useMediaQuery } from "./useMediaQuery";
/**
* @returns Whether the user has requested reduced motion.
*/
export const usePrefersReducedMotion = () =>
export const usePrefersReducedMotion = (): boolean =>
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.
*/
export const useWakeLock = () => {
export function useWakeLock(): void {
useEffect(() => {
if ("wakeLock" in navigator) {
let mounted = true;
@ -28,7 +28,7 @@ export const useWakeLock = () => {
// The lock is automatically released whenever the window goes invisible,
// so we need to reacquire it on visiblity changes
const onVisiblityChange = async () => {
const onVisiblityChange = async (): Promise<void> => {
if (document.visibilityState === "visible") {
try {
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(
g: SparseGrid,
dest: number,
avoid: (cell: number) => boolean = () => false
avoid: (cell: number) => boolean = (): boolean => false
): (number | null)[] {
const destRow = row(dest, g);
const destColumn = column(dest, g);
@ -91,7 +91,7 @@ export function getPaths(
edges[dest] = null;
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]) {
distances[curr] = distanceVia;
edges[curr] = via;
@ -128,7 +128,7 @@ export function getPaths(
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 =>
findLastIndex(g.cells, (c) => c !== undefined && is1By1(c));
@ -257,7 +257,7 @@ function getNextGap(
* along the way.
* 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 fromEnd = areaEnd(from, tile.columns, tile.rows, g);
const toEnd = areaEnd(to, tile.columns, tile.rows, g);
@ -333,7 +333,7 @@ function pushTileUp(
g: SparseGrid,
from: number,
rows: number,
avoid: (cell: number) => boolean = () => false
avoid: (cell: number) => boolean = (): boolean => false
): number {
const tile = g.cells[from]!;
@ -359,7 +359,7 @@ function pushTileUp(
return 0;
}
function trimTrailingGaps(g: SparseGrid) {
function trimTrailingGaps(g: SparseGrid): void {
// Shrink the array to remove trailing gaps
const newLength = (findLastIndex(g.cells, (c) => c !== undefined) ?? -1) + 1;
if (newLength !== g.cells.length) g.cells = g.cells.slice(0, newLength);
@ -485,7 +485,7 @@ export function fillGaps(
export function fillGaps(
g: SparseGrid,
packLargeTiles = true,
ignoreGap: (cell: number) => boolean = () => false
ignoreGap: (cell: number) => boolean = (): boolean => false
): SparseGrid {
const lastGap = findLastIndex(
g.cells,
@ -785,7 +785,11 @@ export function setTileSize<G extends Grid | SparseGrid>(
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) => {
grid.cells[i] = {
item: fromCell.item,
@ -904,7 +908,7 @@ export function resize(g: Grid, columns: number): 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
// will stick with this approach in the long run
// 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.
*/
import { ComponentType, useCallback, useMemo, useRef } from "react";
import { ComponentType, ReactNode, useCallback, useMemo, useRef } from "react";
import type { RectReadOnly } from "react-use-measure";
import { useReactiveState } from "../useReactiveState";
@ -98,16 +98,33 @@ export const useLayoutStates = (): 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
* concrete layout state, and provides callbacks for user interaction.
*/
export const useLayout = <State, T>(
export function useLayout<State, T>(
layout: Layout<State>,
items: TileDescriptor<T>[],
bounds: RectReadOnly,
layoutStates: LayoutStatesMap
) => {
): UseLayout<State, T> {
const prevLayout = useRef<Layout<unknown>>();
const prevState = layoutStates.get(layout);
@ -169,10 +186,10 @@ export const useLayout = <State, T>(
toggleFocus: useMemo(
() =>
layout.toggleFocus &&
((tile: TileDescriptor<T>) =>
((tile: TileDescriptor<T>): void =>
setState((s) => layout.toggleFocus!(s, tile))),
[layout, setState]
),
slots: <layout.Slots s={state} />,
};
};
}

View File

@ -81,7 +81,7 @@ export function NewVideoGrid<T>({
disableAnimations,
layoutStates,
children,
}: Props<T>) {
}: Props<T>): ReactNode {
// 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
// get the dimensions of each slot, feeding these numbers back into
@ -210,7 +210,7 @@ export function NewVideoGrid<T>({
springRef.start();
}, [tiles, springRef]);
const animateDraggedTile = (endOfGesture: boolean) => {
const animateDraggedTile = (endOfGesture: boolean): void => {
const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!;
const tile = tiles.find((t) => t.item.id === tileId)!;
@ -226,7 +226,8 @@ export function NewVideoGrid<T>({
y: tile.y,
width: tile.width,
height: tile.height,
immediate: disableAnimations || ((key) => key === "zIndex"),
immediate:
disableAnimations || ((key): boolean => key === "zIndex"),
// Allow the tile's position to settle before pushing its
// z-index back down
delay: (key) => (key === "zIndex" ? 500 : 0),
@ -239,7 +240,8 @@ export function NewVideoGrid<T>({
y: tileY,
immediate:
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
last,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => {
): void => {
if (tap) {
const now = Date.now();

View File

@ -17,6 +17,7 @@ limitations under the License.
import {
ComponentProps,
Key,
MutableRefObject,
ReactNode,
Ref,
useCallback,
@ -112,7 +113,7 @@ export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
const GAP = 8;
function useIsMounted() {
function useIsMounted(): MutableRefObject<boolean> {
const isMountedRef = useRef<boolean>(false);
useEffect(() => {
@ -478,7 +479,7 @@ function centerTiles(
gridHeight: number,
offsetLeft: number,
offsetTop: number
) {
): TilePosition[] {
const bounds = getSubGridBoundingBox(positions);
const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
@ -493,7 +494,7 @@ function applyTileOffsets(
positions: TilePosition[],
leftOffset: number,
topOffset: number
) {
): TilePosition[] {
for (const position of positions) {
position.x += leftOffset;
position.y += topOffset;
@ -623,7 +624,7 @@ function getSubGridPositions(
tileAspectRatio: number,
gridWidth: number,
gridHeight: number
) {
): TilePosition[] {
if (tileCount === 0) {
return [];
}
@ -726,7 +727,11 @@ function displayedTileCount(
// Sets the 'order' property on tiles based on the layout param and
// 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 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
@ -841,7 +846,7 @@ export function VideoGrid<T>({
layout,
disableAnimations,
children,
}: VideoGridProps<T>) {
}: VideoGridProps<T>): ReactNode {
// Place the PiP in the bottom right corner by default
const [pipXRatio, setPipXRatio] = useState(1);
const [pipYRatio, setPipYRatio] = useState(1);
@ -1208,7 +1213,7 @@ export function VideoGrid<T>({
// @ts-ignore
event,
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0]
) => {
): void => {
event.preventDefault();
if (tap) {

View File

@ -97,12 +97,12 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
);
useEffect(() => {
if (member) {
const updateName = () => {
const updateName = (): void => {
setDisplayName(member.rawDisplayName);
};
member!.on(RoomMemberEvent.Name, updateName);
return () => {
return (): void => {
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.
*/
import React, { ChangeEvent, useState } from "react";
import { ChangeEvent, FC, useState } from "react";
import { useTranslation } from "react-i18next";
import { RemoteParticipant, Track } from "livekit-client";
@ -29,7 +29,7 @@ interface LocalVolumeProps {
content: TileContent;
}
const LocalVolume: React.FC<LocalVolumeProps> = ({
const LocalVolume: FC<LocalVolumeProps> = ({
participant,
content,
}: LocalVolumeProps) => {
@ -42,7 +42,7 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
participant.getVolume(source) ?? 0
);
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>) => {
const onLocalVolumeChanged = (event: ChangeEvent<HTMLInputElement>): void => {
const value: number = +event.target.value;
setLocalVolume(value);
participant.setVolume(value, source);
@ -72,7 +72,11 @@ interface Props {
onDismiss: () => void;
}
export const VideoTileSettingsModal = ({ data, open, onDismiss }: Props) => {
export const VideoTileSettingsModal: FC<Props> = ({
data,
open,
onDismiss,
}) => {
const { t } = useTranslation();
return (

View File

@ -68,7 +68,7 @@ interface WidgetHelpers {
* 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.
*/
export const widget: WidgetHelpers | null = (() => {
export const widget = ((): WidgetHelpers | null => {
try {
const query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId");
@ -161,7 +161,7 @@ export const widget: WidgetHelpers | null = (() => {
);
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
// be otherwise)
await Config.init();

View File

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

View File

@ -6341,6 +6341,11 @@ bufrw@^1.2.1:
hexer "^1.5.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:
version "3.0.0"
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"
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"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
@ -6667,6 +6672,13 @@ clean-css@^4.2.3:
dependencies:
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:
version "2.2.0"
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:
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:
version "2.28.1"
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"
semver "^6.3.0"
eslint-plugin-matrix-org@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.4.0.tgz#de2d2db1cd471d637728133ce9a2b921690e5cd1"
integrity sha512-yVkNwtc33qtrQB4PPzpU+PUdFzdkENPan3JF4zhtAQJRUYXyvKEXnYSrXLUWYRXoYFxs9LbyI2CnhJL/RnHJaQ==
eslint-plugin-matrix-org@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-1.2.1.tgz#76d1505daa93fb99ba4156008b9b32f57682c9b1"
integrity sha512-A3cDjhG7RHwfCS8o3bOip8hSCsxtmgk2ahvqE5v/Ic2kPEZxixY6w8zLj7hFGsrRmPSEpLWqkVLt8uvQBapiQA==
eslint-plugin-react-hooks@^4.5.0:
version "4.6.0"
@ -8593,6 +8610,27 @@ eslint-plugin-react@^7.29.4:
semver "^6.3.1"
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:
version "5.1.1"
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"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.2:
esquery@^1.4.2, esquery@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
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"
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:
version "1.2.7"
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"
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:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
@ -12850,6 +12900,11 @@ pkg-dir@^7.0.0:
dependencies:
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:
version "1.6.4"
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"
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:
version "1.5.1"
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-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:
version "0.9.1"
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"
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:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"