Compare commits

..

2 Commits

Author SHA1 Message Date
Timo K
7dcda403b0 add explanation for the odd await
Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-11 19:28:37 +02:00
Timo K
1e995c4061 fix sync loop by adding a 20ms break for the next mute sync
Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-11 19:01:00 +02:00
138 changed files with 1536 additions and 1879 deletions

View File

@@ -1,31 +1,13 @@
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: "latest",
ecmaVersion: 2018,
sourceType: "module",
project: ["./tsconfig.json"],
},
@@ -33,12 +15,29 @@ module.exports = {
browser: true,
node: true,
},
rules: {
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
"jsx-a11y/media-has-caption": "off",
// We should use the js-sdk logger, never console directly.
"no-console": ["error"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"jsx-a11y/media-has-caption": ["off"],
},
overrides: [
{
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
extends: [
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
"prettier",
],
rules: {
// We're aiming to convert this code to strict mode
"@typescript-eslint/no-non-null-assertion": "off",
// We should use the js-sdk logger, never console directly.
"no-console": ["error"],
},
},
],
settings: {
react: {
version: "detect",

View File

@@ -75,7 +75,7 @@ jobs:
uses: docker/setup-buildx-action@dedd61cf5d839122591f5027c89bf3ad27691d18
- name: Build and push Docker image
uses: docker/build-push-action@8d2cf95286e3dd3d016d478187b93b57af29fd2d
uses: docker/build-push-action@0f847266c302569530c95bfa228489494c43b002
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -14,7 +14,7 @@ module.exports = {
Array.isArray(item) &&
item.length > 0 &&
item[0].name === "vite-plugin-mdx"
),
)
);
config.plugins.push(svgrPlugin());
config.resolve = config.resolve || {};

View File

@@ -105,22 +105,19 @@
"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": "^1.2.1",
"eslint-plugin-matrix-org": "^0.4.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-unicorn": "^48.0.1",
"i18next-parser": "^8.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.3.1",
"jest-mock": "^29.5.0",
"prettier": "^3.0.0",
"prettier": "^2.6.2",
"sass": "^1.42.1",
"typescript": "^5.1.6",
"typescript-eslint-language-service": "^5.0.5",
"vite": "^4.2.0",
"vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^4.0.0"

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@@ -36,8 +36,11 @@
"Developer Settings": "Developer Settings",
"Display name": "Display name",
"Element Call Home": "Element Call Home",
"Element Call is temporarily not end-to-end encrypted while we test scalability.": "Element Call is temporarily not end-to-end encrypted while we test scalability.",
"Enable end-to-end encryption (password protected calls)": "Enable end-to-end encryption (password protected calls)",
"Encrypted": "Encrypted",
"End call": "End call",
"End-to-end encryption isn't supported on your browser.": "End-to-end encryption isn't supported on your browser.",
"Exit full screen": "Exit full screen",
"Expose developer settings in the settings window.": "Expose developer settings in the settings window.",
"Feedback": "Feedback",

View File

@@ -114,10 +114,5 @@
"Call not found": "Nie znaleziono połączenia",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.",
"You": "Ty",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117",
"Invite": "Zaproś",
"Link copied to clipboard": "Skopiowano link do schowka",
"Participants": "Uczestnicy",
"Copy link": "Kopiuj link",
"Invite to this call": "Zaproś do połączenia"
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117"
}

View File

@@ -1,8 +1 @@
{
"{{count}} stars|one": "{{count}} stjärna",
"{{count}} stars|other": "{{count}} stjärnor",
"{{count, number}}|one": "{{count, number}}",
"{{count, number}}|other": "{{count, number}}",
"{{displayName}} is presenting": "{{displayName}} presenterar",
"{{displayName}}, your call has ended.": "{{displayName}}, ditt samtal har avslutats."
}
{}

View File

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

View File

@@ -58,7 +58,7 @@ export const Avatar: FC<Props> = ({
Object.values(Size).includes(size as Size)
? sizes.get(size as Size)
: (size as number),
[size],
[size]
);
const resolvedSrc = useMemo(() => {

View File

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

View File

@@ -82,8 +82,7 @@ export type SetClientParams = {
const ClientContext = createContext<ClientState | undefined>(undefined);
export const useClientState = (): ClientState | undefined =>
useContext(ClientContext);
export const useClientState = () => useContext(ClientContext);
export function useClient(): {
client?: MatrixClient;
@@ -190,7 +189,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
user: session.user_id,
password: session.tempPassword,
},
password,
password
);
saveSession({ ...session, passwordlessUser: false });
@@ -200,7 +199,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
passwordlessUser: false,
});
},
[initClientState?.client],
[initClientState?.client]
);
const setClient = useCallback(
@@ -222,7 +221,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
setInitClientState(null);
}
},
[initClientState?.client],
[initClientState?.client]
);
const logout = useCallback(async () => {
@@ -250,7 +249,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}, []);
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
undefined,
undefined
);
useEventTarget(
loadChannel,
@@ -258,9 +257,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
useCallback(() => {
initClientState?.client.stopClient();
setAlreadyOpenedErr(
translatedError("This application has been opened in another tab.", t),
translatedError("This application has been opened in another tab.", t)
);
}, [initClientState?.client, setAlreadyOpenedErr, t]),
}, [initClientState?.client, setAlreadyOpenedErr, t])
);
const [isDisconnected, setIsDisconnected] = useState(false);
@@ -301,7 +300,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
setIsDisconnected(clientIsDisconnected(state, data));
},
[],
[]
);
useEffect(() => {
@@ -387,7 +386,7 @@ async function loadClient(): Promise<InitResult | null> {
logger.warn(
"The previous session was lost, and we couldn't log it out, " +
err +
"either",
"either"
);
}
}
@@ -409,8 +408,8 @@ export interface Session {
tempPassword?: string;
}
const clearSession = (): void => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session): void =>
const clearSession = () => localStorage.removeItem("matrix-auth-store");
const saveSession = (s: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
const loadSession = (): Session | undefined => {
const data = localStorage.getItem("matrix-auth-store");
@@ -423,6 +422,5 @@ const loadSession = (): Session | undefined => {
const clientIsDisconnected = (
syncState: SyncState,
syncData?: ISyncStateData,
): boolean =>
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
syncData?: ISyncStateData
) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError";

View File

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

23
src/E2EEBanner.module.css Normal file
View File

@@ -0,0 +1,23 @@
/*
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.
*/
.e2eeBanner {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
font-size: var(--font-size-caption);
}

39
src/E2EEBanner.tsx Normal file
View File

@@ -0,0 +1,39 @@
/*
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 { Trans } from "react-i18next";
import { Banner } from "./Banner";
import styles from "./E2EEBanner.module.css";
import LockOffIcon from "./icons/LockOff.svg?react";
import { useEnableE2EE } from "./settings/useSetting";
export const E2EEBanner = () => {
const [e2eeEnabled] = useEnableE2EE();
if (e2eeEnabled) return null;
return (
<Banner>
<div className={styles.e2eeBanner}>
<LockOffIcon width={24} height={24} />
<Trans>
Element Call is temporarily not end-to-end encrypted while we test
scalability.
</Trans>
</div>
</Banner>
);
};

View File

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

View File

@@ -48,5 +48,5 @@ export const Glass = forwardRef<HTMLDivElement, Props>(
>
{Children.only(children)}
</div>
),
)
);

View File

@@ -32,13 +32,13 @@ interface HeaderProps extends HTMLAttributes<HTMLElement> {
className?: string;
}
export const Header: FC<HeaderProps> = ({ children, className, ...rest }) => {
export function Header({ children, className, ...rest }: HeaderProps) {
return (
<header className={classNames(styles.header, className)} {...rest}>
{children}
</header>
);
};
}
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
@@ -46,26 +46,26 @@ interface LeftNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean;
}
export const LeftNav: FC<LeftNavProps> = ({
export function LeftNav({
children,
className,
hideMobile,
...rest
}) => {
}: LeftNavProps) {
return (
<div
className={classNames(
styles.nav,
styles.leftNav,
{ [styles.hideMobile]: hideMobile },
className,
className
)}
{...rest}
>
{children}
</div>
);
};
}
interface RightNavProps extends HTMLAttributes<HTMLElement> {
children?: ReactNode;
@@ -73,32 +73,32 @@ interface RightNavProps extends HTMLAttributes<HTMLElement> {
hideMobile?: boolean;
}
export const RightNav: FC<RightNavProps> = ({
export function RightNav({
children,
className,
hideMobile,
...rest
}) => {
}: RightNavProps) {
return (
<div
className={classNames(
styles.nav,
styles.rightNav,
{ [styles.hideMobile]: hideMobile },
className,
className
)}
{...rest}
>
{children}
</div>
);
};
}
interface HeaderLogoProps {
className?: string;
}
export const HeaderLogo: FC<HeaderLogoProps> = ({ className }) => {
export function HeaderLogo({ className }: HeaderLogoProps) {
const { t } = useTranslation();
return (
@@ -110,7 +110,7 @@ export const HeaderLogo: FC<HeaderLogoProps> = ({ className }) => {
<Logo />
</Link>
);
};
}
interface RoomHeaderInfoProps {
id: string;

View File

@@ -63,7 +63,7 @@ export class LazyEventEmitter extends EventEmitter {
public addListener(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: any[]) => void,
listener: (...args: any[]) => void
): this {
return this.on(type, listener);
}

View File

@@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
MutableRefObject,
PointerEvent,
ReactNode,
useCallback,
useRef,
} from "react";
import { MutableRefObject, PointerEvent, useCallback, useRef } from "react";
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
import { ListState } from "@react-stately/list";
import { Node } from "@react-types/shared";
@@ -41,7 +35,7 @@ export function ListBox<T>({
className,
listBoxRef,
...rest
}: ListBoxProps<T>): ReactNode {
}: ListBoxProps<T>) {
const ref = useRef<HTMLUListElement>(null);
const listRef = listBoxRef ?? ref;
@@ -72,12 +66,12 @@ interface OptionProps<T> {
item: Node<T>;
}
function Option<T>({ item, state, className }: OptionProps<T>): ReactNode {
function Option<T>({ item, state, className }: OptionProps<T>) {
const ref = useRef(null);
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key },
state,
ref,
ref
);
// Hack: remove the onPointerUp event handler and re-wire it to
@@ -97,7 +91,7 @@ function Option<T>({ item, state, className }: OptionProps<T>): ReactNode {
// @ts-ignore
origPointerUp(e as unknown as PointerEvent<HTMLElement>);
},
[origPointerUp],
[origPointerUp]
);
return (

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Key, ReactNode, useRef, useState } from "react";
import { Key, 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>): ReactNode {
}: MenuProps<T>) {
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
const menuRef = useRef(null);
const { menuProps } = useMenu<T>(rest, state, menuRef);
@@ -68,12 +68,7 @@ interface MenuItemProps<T> {
onClose: () => void;
}
function MenuItem<T>({
item,
state,
onAction,
onClose,
}: MenuItemProps<T>): ReactNode {
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
const ref = useRef(null);
const { menuItemProps } = useMenuItem(
{
@@ -82,7 +77,7 @@ function MenuItem<T>({
onClose,
},
state,
ref,
ref
);
const [isFocused, setFocused] = useState(false);

View File

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

View File

@@ -70,7 +70,7 @@ export const Toast: FC<Props> = ({
(open: boolean) => {
if (!open) onDismiss();
},
[onDismiss],
[onDismiss]
);
useEffect(() => {
@@ -91,7 +91,7 @@ export const Toast: FC<Props> = ({
className={classNames(
overlayStyles.overlay,
overlayStyles.animate,
styles.toast,
styles.toast
)}
>
<DialogTitle asChild>

View File

@@ -43,7 +43,7 @@ interface TooltipProps {
const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
(
{ state, className, children, ...rest }: TooltipProps,
ref: ForwardedRef<HTMLDivElement>,
ref: ForwardedRef<HTMLDivElement>
) => {
const { tooltipProps } = useTooltip(rest, state);
@@ -56,7 +56,7 @@ const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
{children}
</div>
);
},
}
);
interface TooltipTriggerProps {
@@ -69,7 +69,7 @@ interface TooltipTriggerProps {
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
(
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
ref: ForwardedRef<HTMLElement>,
ref: ForwardedRef<HTMLElement>
) => {
const tooltipTriggerProps = { delay: 250, ...rest };
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
@@ -78,7 +78,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
const { triggerProps, tooltipProps } = useTooltipTrigger(
tooltipTriggerProps,
tooltipState,
triggerRef,
triggerRef
);
const { overlayProps } = useOverlayPosition({
@@ -94,7 +94,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
<children.type
{...mergeProps<typeof children.props | typeof rest>(
children.props,
rest,
rest
)}
/>
{tooltipState.isOpen && (
@@ -110,5 +110,5 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
)}
</FocusableProvider>
);
},
}
);

View File

@@ -37,7 +37,5 @@ 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,
): TranslatedError => new TranslatedErrorImpl(messageKey, t);
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
new TranslatedErrorImpl(messageKey, t);

View File

@@ -119,17 +119,17 @@ interface UrlParams {
// file.
export function editFragmentQuery(
hash: string,
edit: (params: URLSearchParams) => URLSearchParams,
edit: (params: URLSearchParams) => URLSearchParams
): string {
const fragmentQueryStart = hash.indexOf("?");
const fragmentParams = edit(
new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
),
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
)
);
return `${hash.substring(
0,
fragmentQueryStart,
fragmentQueryStart
)}?${fragmentParams.toString()}`;
}
@@ -137,30 +137,30 @@ class ParamParser {
private fragmentParams: URLSearchParams;
private queryParams: URLSearchParams;
public constructor(search: string, hash: string) {
constructor(search: string, hash: string) {
this.queryParams = new URLSearchParams(search);
const fragmentQueryStart = hash.indexOf("?");
this.fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
);
}
// Normally, URL params should be encoded in the fragment so as to avoid
// leaking them to the server. However, we also check the normal query
// string for backwards compatibility with versions that only used that.
public getParam(name: string): string | null {
getParam(name: string): string | null {
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
}
public getAllParams(name: string): string[] {
getAllParams(name: string): string[] {
return [
...this.fragmentParams.getAll(name),
...this.queryParams.getAll(name),
];
}
public getFlagParam(name: string, defaultValue = false): boolean {
getFlagParam(name: string, defaultValue = false): boolean {
const param = this.getParam(name);
return param === null ? defaultValue : param !== "false";
}
@@ -174,7 +174,7 @@ class ParamParser {
*/
export const getUrlParams = (
search = window.location.search,
hash = window.location.hash,
hash = window.location.hash
): UrlParams => {
const parser = new ParamParser(search, hash);
@@ -221,7 +221,7 @@ export const useUrlParams = (): UrlParams => {
export function getRoomIdentifierFromUrl(
pathname: string,
search: string,
hash: string,
hash: string
): RoomIdentifier {
let roomAlias: string | null = null;
pathname = pathname.substring(1); // Strip the "/"
@@ -281,6 +281,6 @@ export const useRoomIdentifier = (): RoomIdentifier => {
const { pathname, search, hash } = useLocation();
return useMemo(
() => getRoomIdentifierFromUrl(pathname, search, hash),
[pathname, search, hash],
[pathname, search, hash]
);
};

View File

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

View File

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

View File

@@ -1,19 +1,3 @@
/*
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;
}
private constructor(private readonly posthog: PostHog) {
constructor(private readonly posthog: PostHog) {
const posthogConfig: PosthogSettings = {
project_api_key: Config.get().posthog?.api_key,
api_host: Config.get().posthog?.api_host,
@@ -146,7 +146,7 @@ export class PosthogAnalytics {
this.enabled = true;
} else {
logger.info(
"Posthog is not enabled because there is no api key or no host given in the config",
"Posthog is not enabled because there is no api key or no host given in the config"
);
this.enabled = false;
}
@@ -157,7 +157,7 @@ export class PosthogAnalytics {
private sanitizeProperties = (
properties: Properties,
_eventName: string,
_eventName: string
): Properties => {
// Callback from posthog to sanitize properties before sending them to the server.
// Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
@@ -183,7 +183,7 @@ export class PosthogAnalytics {
return properties;
};
private registerSuperProperties(properties: Properties): void {
private registerSuperProperties(properties: Properties) {
if (this.enabled) {
this.posthog.register(properties);
}
@@ -201,8 +201,8 @@ export class PosthogAnalytics {
private capture(
eventName: string,
properties: Properties,
options?: CaptureOptions,
): void {
options?: CaptureOptions
) {
if (!this.enabled) {
return;
}
@@ -213,7 +213,7 @@ export class PosthogAnalytics {
return this.enabled;
}
private setAnonymity(anonymity: Anonymity): void {
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,9 +236,7 @@ export class PosthogAnalytics {
.join("");
}
private async identifyUser(
analyticsIdGenerator: () => string,
): Promise<void> {
private async identifyUser(analyticsIdGenerator: () => string) {
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.
@@ -260,27 +258,27 @@ export class PosthogAnalytics {
// The above could fail due to network requests, but not essential to starting the application,
// so swallow it.
logger.log(
"Unable to identify user for tracking" + (e as Error)?.toString(),
"Unable to identify user for tracking" + (e as Error)?.toString()
);
}
if (analyticsID) {
this.posthog.identify(analyticsID);
} else {
logger.info(
"No analyticsID is availble. Should not try to setup posthog",
"No analyticsID is availble. Should not try to setup posthog"
);
}
}
}
private async getAnalyticsId(): Promise<string | null> {
async getAnalyticsId() {
const client: MatrixClient = window.matrixclient;
let accountAnalyticsId;
if (widget) {
accountAnalyticsId = getUrlParams().analyticsID;
} else {
const accountData = await client.getAccountDataFromServer(
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
PosthogAnalytics.ANALYTICS_EVENT_TYPE
);
accountAnalyticsId = accountData?.id;
}
@@ -293,14 +291,12 @@ export class PosthogAnalytics {
return null;
}
private async hashedEcAnalyticsId(
accountAnalyticsId: string,
): Promise<string> {
async hashedEcAnalyticsId(accountAnalyticsId: string): Promise<string> {
const client: MatrixClient = window.matrixclient;
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
const bufferForPosthogId = await crypto.subtle.digest(
"sha-256",
Buffer.from(posthogIdMaterial, "utf-8"),
Buffer.from(posthogIdMaterial, "utf-8")
);
const view = new Int32Array(bufferForPosthogId);
return Array.from(view)
@@ -308,17 +304,17 @@ export class PosthogAnalytics {
.join("");
}
private async setAccountAnalyticsId(analyticsID: string): Promise<void> {
async setAccountAnalyticsId(analyticsID: string) {
if (!widget) {
const client = window.matrixclient;
// the analytics ID only needs to be set in the standalone version.
const accountData = await client.getAccountDataFromServer(
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
PosthogAnalytics.ANALYTICS_EVENT_TYPE
);
await client.setAccountData(
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
Object.assign({ id: analyticsID }, accountData),
Object.assign({ id: analyticsID }, accountData)
);
}
}
@@ -339,7 +335,7 @@ export class PosthogAnalytics {
this.updateAnonymityAndIdentifyUser(optInAnalytics);
}
private updateSuperProperties(): void {
private updateSuperProperties() {
// Update super properties in posthog with our platform (app version, platform).
// These properties will be subsequently passed in every event.
//
@@ -360,7 +356,7 @@ export class PosthogAnalytics {
}
private async updateAnonymityAndIdentifyUser(
pseudonymousOptIn: boolean,
pseudonymousOptIn: boolean
): Promise<void> {
// Update this.anonymity based on the user's analytics opt-in settings
const anonymity = pseudonymousOptIn
@@ -376,11 +372,11 @@ export class PosthogAnalytics {
this.setRegistrationType(
window.matrixclient.isGuest() || window.passwordlessUser
? RegistrationType.Guest
: RegistrationType.Registered,
: RegistrationType.Registered
);
// store the promise to await posthog-tracking-events until the identification is done.
this.identificationPromise = this.identifyUser(
PosthogAnalytics.getRandomAnalyticsId,
PosthogAnalytics.getRandomAnalyticsId
);
await this.identificationPromise;
if (this.userRegisteredInThisSession()) {
@@ -395,7 +391,7 @@ export class PosthogAnalytics {
public async trackEvent<E extends IPosthogEvent>(
{ eventName, ...properties }: E,
options?: CaptureOptions,
options?: CaptureOptions
): Promise<void> {
if (this.identificationPromise) {
// only make calls to posthog after the identificaion is done

View File

@@ -36,22 +36,18 @@ export class CallEndedTracker {
maxParticipantsCount: 0,
};
public cacheStartCall(time: Date): void {
cacheStartCall(time: Date) {
this.cache.startTime = time;
}
public cacheParticipantCountChanged(count: number): void {
cacheParticipantCountChanged(count: number) {
this.cache.maxParticipantsCount = Math.max(
count,
this.cache.maxParticipantsCount,
this.cache.maxParticipantsCount
);
}
public track(
callId: string,
callParticipantsNow: number,
sendInstantly: boolean,
): void {
track(callId: string, callParticipantsNow: number, sendInstantly: boolean) {
PosthogAnalytics.instance.trackEvent<CallEnded>(
{
eventName: "CallEnded",
@@ -60,7 +56,7 @@ export class CallEndedTracker {
callParticipantsOnLeave: callParticipantsNow,
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
},
{ send_instantly: sendInstantly },
{ send_instantly: sendInstantly }
);
}
}
@@ -71,7 +67,7 @@ interface CallStarted extends IPosthogEvent {
}
export class CallStartedTracker {
public track(callId: string): void {
track(callId: string) {
PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted",
callId: callId,
@@ -90,19 +86,19 @@ export class SignupTracker {
signupEnd: new Date(0),
};
public cacheSignupStart(time: Date): void {
cacheSignupStart(time: Date) {
this.cache.signupStart = time;
}
public getSignupEndTime(): Date {
getSignupEndTime() {
return this.cache.signupEnd;
}
public cacheSignupEnd(time: Date): void {
cacheSignupEnd(time: Date) {
this.cache.signupEnd = time;
}
public track(): void {
track() {
PosthogAnalytics.instance.trackEvent<Signup>({
eventName: "Signup",
signupDuration: Date.now() - this.cache.signupStart.getTime(),
@@ -116,7 +112,7 @@ interface Login extends IPosthogEvent {
}
export class LoginTracker {
public track(): void {
track() {
PosthogAnalytics.instance.trackEvent<Login>({
eventName: "Login",
});
@@ -131,7 +127,7 @@ interface MuteMicrophone {
}
export class MuteMicrophoneTracker {
public track(targetIsMute: boolean, callId: string): void {
track(targetIsMute: boolean, callId: string) {
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
eventName: "MuteMicrophone",
targetMuteState: targetIsMute ? "mute" : "unmute",
@@ -147,7 +143,7 @@ interface MuteCamera {
}
export class MuteCameraTracker {
public track(targetIsMute: boolean, callId: string): void {
track(targetIsMute: boolean, callId: string) {
PosthogAnalytics.instance.trackEvent<MuteCamera>({
eventName: "MuteCamera",
targetMuteState: targetIsMute ? "mute" : "unmute",
@@ -162,7 +158,7 @@ interface UndecryptableToDeviceEvent {
}
export class UndecryptableToDeviceEventTracker {
public track(callId: string): void {
track(callId: string) {
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
eventName: "UndecryptableToDeviceEvent",
callId,
@@ -178,7 +174,7 @@ interface QualitySurveyEvent {
}
export class QualitySurveyEventTracker {
public track(callId: string, feedbackText: string, stars: number): void {
track(callId: string, feedbackText: string, stars: number) {
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
eventName: "QualitySurvey",
callId,
@@ -194,7 +190,7 @@ interface CallDisconnectedEvent {
}
export class CallDisconnectedEventTracker {
public track(reason?: DisconnectReason): void {
track(reason?: DisconnectReason) {
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 {
public async forceFlush(): Promise<void> {}
async forceFlush(): Promise<void> {}
public onStart(span: Span): void {
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 {
});
}
public onEnd(span: ReadableSpan): void {
onEnd(span: ReadableSpan): void {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span);
@@ -148,7 +148,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
},
// Send instantly because the window might be closing
{ send_instantly: true },
{ send_instantly: true }
);
}
}
@@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
/**
* Shutdown the processor.
*/
public shutdown(): Promise<void> {
shutdown(): Promise<void> {
return Promise.resolve();
}
}

View File

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

View File

@@ -22,7 +22,7 @@ limitations under the License.
// Array.prototype.findLastIndex
export function findLastIndex<T>(
array: T[],
predicate: (item: T, index: number) => boolean,
predicate: (item: T, index: number) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i], i)) return i;
@@ -36,9 +36,9 @@ export function findLastIndex<T>(
*/
export const count = <T>(
array: T[],
predicate: (item: T, index: number) => boolean,
predicate: (item: T, index: number) => boolean
): number =>
array.reduce(
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
0,
0
);

View File

@@ -80,7 +80,7 @@ export const LoginPage: FC = () => {
setLoading(false);
});
},
[login, location, history, homeserver, setClient],
[login, location, history, homeserver, setClient]
);
return (

View File

@@ -69,7 +69,7 @@ export const RegisterPage: FC = () => {
if (password !== passwordConfirmation) return;
const submit = async (): Promise<void> => {
const submit = async () => {
setRegistering(true);
const recaptchaResponse = await execute();
@@ -78,7 +78,7 @@ export const RegisterPage: FC = () => {
password,
userName,
recaptchaResponse,
passwordlessUser,
passwordlessUser
);
if (client && client?.groupCallEventHandler && passwordlessUser) {
@@ -135,7 +135,7 @@ export const RegisterPage: FC = () => {
execute,
client,
setClient,
],
]
);
useEffect(() => {
@@ -184,7 +184,7 @@ export const RegisterPage: FC = () => {
required
name="password"
type="password"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
value={password}
@@ -198,7 +198,7 @@ export const RegisterPage: FC = () => {
required
type="password"
name="passwordConfirmation"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPasswordConfirmation(e.target.value)
}
value={passwordConfirmation}

View File

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

View File

@@ -30,14 +30,14 @@ export const useInteractiveRegistration = (): {
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser: boolean,
passwordlessUser: boolean
) => Promise<[MatrixClient, Session]>;
} => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
undefined,
undefined
);
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
undefined,
undefined
);
const authClient = useRef<MatrixClient>();
@@ -50,7 +50,7 @@ export const useInteractiveRegistration = (): {
useEffect(() => {
authClient.current!.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url,
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
);
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
});
@@ -62,7 +62,7 @@ export const useInteractiveRegistration = (): {
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser: boolean,
passwordlessUser: boolean
): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient.current!,
@@ -72,7 +72,7 @@ export const useInteractiveRegistration = (): {
password,
auth: auth || undefined,
}),
stateUpdated: (nextStage, status): void => {
stateUpdated: (nextStage, status) => {
if (status.error) {
throw new Error(status.error);
}
@@ -88,7 +88,7 @@ export const useInteractiveRegistration = (): {
});
}
},
requestEmailToken: (): Promise<{ sid: string }> => {
requestEmailToken: (...args) => {
return Promise.resolve({ sid: "dummy" });
},
});
@@ -106,7 +106,7 @@ export const useInteractiveRegistration = (): {
userId: user_id,
deviceId: device_id,
},
false,
false
);
await client.setDisplayName(displayName);
@@ -129,7 +129,7 @@ export const useInteractiveRegistration = (): {
return [client, session];
},
[],
[]
);
return { privacyPolicyUrl, recaptchaKey, register };

View File

@@ -35,11 +35,7 @@ interface RecaptchaPromiseRef {
reject: (error: Error) => void;
}
export function useRecaptcha(sitekey?: string): {
execute: () => Promise<string>;
reset: () => void;
recaptchaId: string;
} {
export const useRecaptcha = (sitekey?: string) => {
const { t } = useTranslation();
const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef<RecaptchaPromiseRef>();
@@ -47,7 +43,7 @@ export function useRecaptcha(sitekey?: string): {
useEffect(() => {
if (!sitekey) return;
const onRecaptchaLoaded = (): void => {
const onRecaptchaLoaded = () => {
if (!document.getElementById(recaptchaId)) return;
window.grecaptcha.render(recaptchaId, {
@@ -95,11 +91,11 @@ export function useRecaptcha(sitekey?: string): {
});
promiseRef.current = {
resolve: (value): void => {
resolve: (value) => {
resolve(value);
observer.disconnect();
},
reject: (error): void => {
reject: (error) => {
reject(error);
observer.disconnect();
},
@@ -108,7 +104,7 @@ export function useRecaptcha(sitekey?: string): {
window.grecaptcha.execute();
const iframe = document.querySelector<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]',
'iframe[src*="recaptcha/api2/bframe"]'
);
if (iframe?.parentNode?.parentNode) {
@@ -124,4 +120,4 @@ export function useRecaptcha(sitekey?: string): {
}, []);
return { execute, reset, recaptchaId };
}
};

View File

@@ -48,7 +48,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
randomString(16),
displayName,
recaptchaResponse,
true,
true
);
setClient({ client, session });
} catch (e) {
@@ -56,7 +56,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
throw e;
}
},
[execute, reset, register, setClient],
[execute, reset, register, setClient]
);
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };

View File

@@ -146,9 +146,7 @@ limitations under the License.
.copyButton {
width: 100%;
height: 40px;
transition:
border-color 250ms,
background-color 250ms;
transition: border-color 250ms, background-color 250ms;
}
.copyButton span {

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 { FC, forwardRef } from "react";
import { forwardRef } from "react";
import { PressEvent } from "@react-types/shared";
import classNames from "classnames";
import { useButton } from "@react-aria/button";
@@ -94,12 +94,12 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
onPressStart,
...rest
},
ref,
ref
) => {
const buttonRef = useObjectRef<HTMLButtonElement>(ref);
const { buttonProps } = useButton(
{ onPress, onPressStart, ...rest },
buttonRef,
buttonRef
);
// TODO: react-aria's useButton hook prevents form submission via keyboard
@@ -121,7 +121,7 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
{
[styles.on]: on,
[styles.off]: off,
},
}
)}
{...mergeProps(rest, filteredButtonProps)}
ref={buttonRef}
@@ -132,14 +132,17 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
</>
</button>
);
},
}
);
export const MicButton: FC<{
export function MicButton({
muted,
...rest
}: {
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");
@@ -151,13 +154,16 @@ export const MicButton: FC<{
</Button>
</Tooltip>
);
};
}
export const VideoButton: FC<{
export function VideoButton({
muted,
...rest
}: {
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");
@@ -169,14 +175,18 @@ export const VideoButton: FC<{
</Button>
</Tooltip>
);
};
}
export const ScreenshareButton: FC<{
export function ScreenshareButton({
enabled,
className,
...rest
}: {
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");
@@ -187,13 +197,16 @@ export const ScreenshareButton: FC<{
</Button>
</Tooltip>
);
};
}
export const HangupButton: FC<{
export function HangupButton({
className,
...rest
}: {
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}> = ({ className, ...rest }) => {
}) {
const { t } = useTranslation();
return (
@@ -207,13 +220,16 @@ export const HangupButton: FC<{
</Button>
</Tooltip>
);
};
}
export const SettingsButton: FC<{
export function SettingsButton({
className,
...rest
}: {
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}> = ({ className, ...rest }) => {
}) {
const { t } = useTranslation();
return (
@@ -223,7 +239,7 @@ export const SettingsButton: FC<{
</Button>
</Tooltip>
);
};
}
interface AudioButtonProps extends Omit<Props, "variant"> {
/**
@@ -232,7 +248,7 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
volume: number;
}
export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
const { t } = useTranslation();
return (
@@ -242,16 +258,16 @@ export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
</Button>
</Tooltip>
);
};
}
interface FullscreenButtonProps extends Omit<Props, "variant"> {
fullscreen?: boolean;
}
export const FullscreenButton: FC<FullscreenButtonProps> = ({
export function FullscreenButton({
fullscreen,
...rest
}) => {
}: FullscreenButtonProps) {
const { t } = useTranslation();
const Icon = fullscreen ? FullscreenExit : Fullscreen;
const label = fullscreen ? t("Exit full screen") : t("Full screen");
@@ -263,4 +279,4 @@ export const FullscreenButton: FC<FullscreenButtonProps> = ({
</Button>
</Tooltip>
);
};
}

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ export class Config {
}
async function downloadConfig(
configJsonFilename: string,
configJsonFilename: string
): Promise<ConfigOptions> {
const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString());

View File

@@ -16,7 +16,8 @@ limitations under the License.
import { useEffect, useMemo } from "react";
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
import { useEnableE2EE } from "../settings/useSetting";
import { useLocalStorage } from "../useLocalStorage";
import { useClient } from "../ClientContext";
import { useUrlParams } from "../UrlParams";
import { widget } from "../widget";
@@ -24,52 +25,39 @@ import { widget } from "../widget";
export const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`;
const useInternalRoomSharedKey = (roomId: string): string | null => {
const key = getRoomSharedKeyLocalStorageKey(roomId);
const roomSharedKey = useLocalStorage(key)[0];
const useInternalRoomSharedKey = (
roomId: string
): [string | null, (value: string) => void] => {
const key = useMemo(() => getRoomSharedKeyLocalStorageKey(roomId), [roomId]);
const [e2eeEnabled] = useEnableE2EE();
const [roomSharedKey, setRoomSharedKey] = useLocalStorage(key);
return roomSharedKey;
return [e2eeEnabled ? roomSharedKey : null, setRoomSharedKey];
};
/**
* Extracts the room password from the URL if one is present, saving it in localstorage
* and returning it in a tuple with the corresponding room ID from the URL.
* @returns A tuple of the roomId and password from the URL if the URL has both,
* otherwise [undefined, undefined]
*/
const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
const useKeyFromUrl = (roomId: string): string | null => {
const urlParams = useUrlParams();
const [e2eeSharedKey, setE2EESharedKey] = useInternalRoomSharedKey(roomId);
useEffect(() => {
if (!urlParams.password || !urlParams.roomId) return;
if (!urlParams.roomId) return;
if (!urlParams.password) return;
if (urlParams.password === "") return;
if (urlParams.password === e2eeSharedKey) return;
setLocalStorageItem(
// We set the Item by only using data from the url. This way we
// make sure, we always have matching pairs in the LocalStorage,
// as they occur in the call links.
getRoomSharedKeyLocalStorageKey(urlParams.roomId),
urlParams.password,
);
}, [urlParams]);
setE2EESharedKey(urlParams.password);
}, [urlParams, e2eeSharedKey, setE2EESharedKey]);
return urlParams.roomId && urlParams.password
? [urlParams.roomId, urlParams.password]
: [undefined, undefined];
return urlParams.password ?? null;
};
export const useRoomSharedKey = (roomId: string): string | undefined => {
export const useRoomSharedKey = (roomId: string): string | null => {
// make sure we've extracted the key from the URL first
// (and we still need to take the value it returns because
// the effect won't run in time for it to save to localstorage in
// time for us to read it out again).
const [urlRoomId, passwordFormUrl] = useKeyFromUrl();
const passwordFormUrl = useKeyFromUrl(roomId);
const storedPassword = useInternalRoomSharedKey(roomId);
if (storedPassword) return storedPassword;
if (urlRoomId === roomId) return passwordFormUrl;
return undefined;
return useInternalRoomSharedKey(roomId)[0] ?? passwordFormUrl;
};
export const useIsRoomE2EE = (roomId: string): boolean | null => {
@@ -80,6 +68,6 @@ export const useIsRoomE2EE = (roomId: string): boolean | null => {
// should inspect the e2eEnabled URL parameter here?
return useMemo(
() => widget === null && (room === null || !room.getCanonicalAlias()),
[room],
[room]
);
};

View File

@@ -36,5 +36,5 @@ export const Form = forwardRef<HTMLFormElement, FormProps>(
{children}
</form>
);
},
}
);

View File

@@ -18,7 +18,6 @@ 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";
@@ -32,8 +31,7 @@ interface CallListProps {
rooms: GroupCallRoom[];
client: MatrixClient;
}
export const CallList: FC<CallListProps> = ({ rooms, client }) => {
export function CallList({ rooms, client }: CallListProps) {
return (
<>
<div className={styles.callList}>
@@ -56,7 +54,7 @@ export const CallList: FC<CallListProps> = ({ rooms, client }) => {
</div>
</>
);
};
}
interface CallTileProps {
name: string;
avatarUrl: string;
@@ -64,8 +62,7 @@ interface CallTileProps {
participants: RoomMember[];
client: MatrixClient;
}
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
function CallTile({ name, avatarUrl, room }: CallTileProps) {
const roomSharedKey = useRoomSharedKey(room.roomId);
return (
@@ -74,7 +71,7 @@ const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
to={getRelativeRoomUrl(
room.roomId,
room.name,
roomSharedKey ?? undefined,
roomSharedKey ?? undefined
)}
className={styles.callTileLink}
>
@@ -92,9 +89,9 @@ const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
value={getAbsoluteRoomUrl(
room.roomId,
room.name,
roomSharedKey ?? undefined,
roomSharedKey ?? undefined
)}
/>
</div>
);
};
}

View File

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

View File

@@ -16,7 +16,6 @@ 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";
@@ -29,11 +28,7 @@ interface Props {
onJoin: (e: PressEvent) => void;
}
export const JoinExistingCallModal: FC<Props> = ({
onJoin,
open,
onDismiss,
}) => {
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
const { t } = useTranslation();
return (
@@ -47,4 +42,4 @@ export const JoinExistingCallModal: FC<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, FC } from "react";
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
@@ -38,14 +38,15 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
import { Caption } from "../typography/Typography";
import { Form } from "../form/Form";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { E2EEBanner } from "../E2EEBanner";
interface Props {
client: MatrixClient;
}
export const RegisteredView: FC<Props> = ({ client }) => {
export function RegisteredView({ client }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const [optInAnalytics] = useOptInAnalytics();
@@ -55,8 +56,9 @@ export const RegisteredView: FC<Props> = ({ client }) => {
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen],
[setJoinExistingCallModalOpen]
);
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {
@@ -68,18 +70,22 @@ export const RegisteredView: FC<Props> = ({ client }) => {
? sanitiseRoomNameInput(roomNameData)
: "";
async function submit(): Promise<void> {
async function submit() {
setError(undefined);
setLoading(true);
const createRoomResult = await createRoom(client, roomName, true);
const createRoomResult = await createRoom(
client,
roomName,
e2eeEnabled ?? false
);
history.push(
getRelativeRoomUrl(
createRoomResult.roomId,
roomName,
createRoomResult.password,
),
createRoomResult.password
)
);
}
@@ -96,7 +102,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
}
});
},
[client, history, setJoinExistingCallModalOpen],
[client, history, setJoinExistingCallModalOpen, e2eeEnabled]
);
const recentRooms = useGroupCallRooms(client);
@@ -150,6 +156,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
<AnalyticsNotice />
</Caption>
)}
<E2EEBanner />
{error && (
<FieldRow className={styles.fieldRow}>
<ErrorMessage error={error} />
@@ -168,4 +175,4 @@ export const RegisteredView: FC<Props> = ({ client }) => {
/>
</>
);
};
}

View File

@@ -41,8 +41,9 @@ import styles from "./UnauthenticatedView.module.css";
import commonStyles from "./common.module.css";
import { generateRandomName } from "../auth/generateRandomName";
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
import { useOptInAnalytics } from "../settings/useSetting";
import { useEnableE2EE, useOptInAnalytics } from "../settings/useSetting";
import { Config } from "../config/Config";
import { E2EEBanner } from "../E2EEBanner";
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
@@ -56,12 +57,14 @@ export const UnauthenticatedView: FC = () => {
useState(false);
const onDismissJoinExistingCallModal = useCallback(
() => setJoinExistingCallModalOpen(false),
[setJoinExistingCallModalOpen],
[setJoinExistingCallModalOpen]
);
const [onFinished, setOnFinished] = useState<() => void>();
const history = useHistory();
const { t } = useTranslation();
const [e2eeEnabled] = useEnableE2EE();
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e) => {
e.preventDefault();
@@ -69,7 +72,7 @@ export const UnauthenticatedView: FC = () => {
const roomName = sanitiseRoomNameInput(data.get("callName") as string);
const displayName = data.get("displayName") as string;
async function submit(): Promise<void> {
async function submit() {
setError(undefined);
setLoading(true);
const recaptchaResponse = await execute();
@@ -79,12 +82,16 @@ export const UnauthenticatedView: FC = () => {
randomString(16),
displayName,
recaptchaResponse,
true,
true
);
let createRoomResult;
try {
createRoomResult = await createRoom(client, roomName, true);
createRoomResult = await createRoom(
client,
roomName,
e2eeEnabled ?? false
);
} catch (error) {
if (!setClient) {
throw error;
@@ -117,8 +124,8 @@ export const UnauthenticatedView: FC = () => {
getRelativeRoomUrl(
createRoomResult.roomId,
roomName,
createRoomResult.password,
),
createRoomResult.password
)
);
}
@@ -136,7 +143,8 @@ export const UnauthenticatedView: FC = () => {
history,
setJoinExistingCallModalOpen,
setClient,
],
e2eeEnabled,
]
);
return (
@@ -193,6 +201,7 @@ export const UnauthenticatedView: FC = () => {
</Link>
</Trans>
</Caption>
<E2EEBanner />
{error && (
<FieldRow>
<ErrorMessage error={error} />

View File

@@ -31,7 +31,7 @@ export interface GroupCallRoom {
}
const tsCache: { [index: string]: number } = {};
function getLastTs(client: MatrixClient, r: Room): number {
function getLastTs(client: MatrixClient, r: Room) {
if (tsCache[r.roomId]) {
return tsCache[r.roomId];
}
@@ -47,7 +47,7 @@ function getLastTs(client: MatrixClient, r: Room): number {
if (r.getMyMembership() !== "join") {
const membershipEvent = r.currentState.getStateEvents(
"m.room.member",
myUserId,
myUserId
);
if (membershipEvent && !Array.isArray(membershipEvent)) {
@@ -82,7 +82,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
useEffect(() => {
function updateRooms(): void {
function updateRooms() {
if (!client.groupCallEventHandler) {
return;
}
@@ -115,7 +115,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
client.removeListener(
GroupCallEventHandlerEvent.Participants,
updateRooms,
updateRooms
);
};
}, [client]);

View File

@@ -68,8 +68,7 @@ limitations under the License.
font-weight: 400;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-Regular.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-Regular.woff2") format("woff2"),
url("/fonts/Inter/Inter-Regular.woff") format("woff");
}
@@ -79,8 +78,7 @@ limitations under the License.
font-weight: 400;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-Italic.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-Italic.woff2") format("woff2"),
url("/fonts/Inter/Inter-Italic.woff") format("woff");
}
@@ -90,8 +88,7 @@ limitations under the License.
font-weight: 500;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-Medium.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-Medium.woff2") format("woff2"),
url("/fonts/Inter/Inter-Medium.woff") format("woff");
}
@@ -101,8 +98,7 @@ limitations under the License.
font-weight: 500;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-MediumItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
}
@@ -112,8 +108,7 @@ limitations under the License.
font-weight: 600;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-SemiBold.woff2") format("woff2"),
url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
}
@@ -123,8 +118,7 @@ limitations under the License.
font-weight: 600;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-SemiBoldItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
}
@@ -134,8 +128,7 @@ limitations under the License.
font-weight: 700;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-Bold.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-Bold.woff2") format("woff2"),
url("/fonts/Inter/Inter-Bold.woff") format("woff");
}
@@ -145,8 +138,7 @@ limitations under the License.
font-weight: 700;
font-display: swap;
unicode-range: var(--inter-unicode-range);
src:
url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"),
src: url("/fonts/Inter/Inter-BoldItalic.woff2") format("woff2"),
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
}

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;
public config: LoadState = LoadState.None;
public sentry: LoadState = LoadState.None;
public openTelemetry: LoadState = LoadState.None;
config: LoadState = LoadState.None;
sentry: LoadState = LoadState.None;
openTelemetry: LoadState = LoadState.None;
public allDepsAreLoaded(): boolean {
allDepsAreLoaded() {
return !Object.values(this).some((s) => s !== LoadState.Loaded);
}
}
@@ -52,7 +52,7 @@ export class Initializer {
return Initializer.internalInstance?.isInitialized;
}
public static initBeforeReact(): void {
public static initBeforeReact() {
// this maybe also needs to return a promise in the future,
// if we have to do async inits before showing the loading screen
// but this should be avioded if possible
@@ -99,13 +99,13 @@ export class Initializer {
if (fontScale !== null) {
document.documentElement.style.setProperty(
"--font-scale",
fontScale.toString(),
fontScale.toString()
);
}
if (fonts.length > 0) {
document.documentElement.style.setProperty(
"--font-family",
fonts.map((f) => `"${f}"`).join(", "),
fonts.map((f) => `"${f}"`).join(", ")
);
}
@@ -126,9 +126,9 @@ export class Initializer {
return Initializer.internalInstance.initPromise;
}
private loadStates = new DependencyLoadStates();
loadStates = new DependencyLoadStates();
private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
initStep(resolve: (value: void | PromiseLike<void>) => void) {
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
// we need to decide if we want to init it here or keep it in initClient
// if (this.loadStates.olm === LoadState.None) {

View File

@@ -52,7 +52,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
onRemoveAvatar,
...rest
},
ref,
ref
) => {
const { t } = useTranslation();
@@ -64,7 +64,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
useEffect(() => {
const currentInput = fileInputRef.current;
const onChange = (e: Event): void => {
const onChange = (e: Event) => {
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 (): void => {
return () => {
currentInput?.removeEventListener("change", onChange);
};
});
@@ -120,5 +120,5 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
)}
</div>
);
},
}
);

View File

@@ -85,11 +85,8 @@ limitations under the License.
}
.inputField label {
transition:
font-size 0.25s ease-out 0.1s,
color 0.25s ease-out 0.1s,
top 0.25s ease-out 0.1s,
background-color 0.25s ease-out 0.1s;
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
color: var(--cpd-color-text-secondary);
background-color: transparent;
font-size: var(--font-size-body);
@@ -121,11 +118,8 @@ limitations under the License.
.inputField textarea:not(:placeholder-shown) + label,
.inputField.prefix textarea + label {
background-color: var(--cpd-color-bg-canvas-default);
transition:
font-size 0.25s ease-out 0s,
color 0.25s ease-out 0s,
top 0.25s ease-out 0s,
background-color 0.25s ease-out 0s;
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
font-size: var(--font-size-micro);
top: -13px;
padding: 0 2px;

View File

@@ -44,7 +44,7 @@ export function FieldRow({
className={classNames(
styles.fieldRow,
{ [styles.rightAlign]: rightAlign },
className,
className
)}
>
{children}
@@ -102,7 +102,7 @@ export const InputField = forwardRef<
disabled,
...rest
},
ref,
ref
) => {
const descriptionId = useId();
@@ -114,7 +114,7 @@ export const InputField = forwardRef<
[styles.prefix]: !!prefix,
[styles.disabled]: disabled,
},
className,
className
)}
>
{prefix && <span>{prefix}</span>}
@@ -163,7 +163,7 @@ export const InputField = forwardRef<
)}
</Field>
);
},
}
);
interface ErrorMessageProps {

View File

@@ -38,7 +38,7 @@ export function SelectInput(props: Props): JSX.Element {
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
props,
state,
ref,
ref
);
const { buttonProps } = useButton(triggerProps, ref);

View File

@@ -41,8 +41,8 @@ export function StarRatingInput({
return (
<div
className={styles.inputContainer}
onMouseEnter={(): void => setHover(index)}
onMouseLeave={(): void => setHover(rating)}
onMouseEnter={() => setHover(index)}
onMouseLeave={() => 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): void => {
onChange={(_ev) => {
setRating(index);
onChange(index);
}}

View File

@@ -51,8 +51,8 @@ export interface MediaDevices {
// Cargo-culted from @livekit/components-react
function useObservableState<T>(
observable: Observable<T> | undefined,
startWith: T,
): T {
startWith: T
) {
const [state, setState] = useState<T>(startWith);
useEffect(() => {
// observable state doesn't run in SSR
@@ -67,7 +67,7 @@ function useMediaDevice(
kind: MediaDeviceKind,
fallbackDevice: string | undefined,
usingNames: boolean,
alwaysDefault: boolean = false,
alwaysDefault: boolean = false
): MediaDevice {
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
@@ -83,7 +83,7 @@ function useMediaDevice(
// kind, which then results in multiple permissions requests.
const deviceObserver = useMemo(
() => createMediaDeviceObserver(kind, requestPermissions),
[kind, requestPermissions],
[kind, requestPermissions]
);
const available = useObservableState(deviceObserver, []);
const [selectedId, select] = useState(fallbackDevice);
@@ -143,18 +143,18 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
const audioInput = useMediaDevice(
"audioinput",
audioInputSetting,
usingNames,
usingNames
);
const audioOutput = useMediaDevice(
"audiooutput",
audioOutputSetting,
useOutputNames,
alwaysUseDefaultAudio,
alwaysUseDefaultAudio
);
const videoInput = useMediaDevice(
"videoinput",
videoInputSetting,
usingNames,
usingNames
);
useEffect(() => {
@@ -176,11 +176,11 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
const startUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n + 1),
[setNumCallersUsingNames],
[setNumCallersUsingNames]
);
const stopUsingDeviceNames = useCallback(
() => setNumCallersUsingNames((n) => n - 1),
[setNumCallersUsingNames],
[setNumCallersUsingNames]
);
const context: MediaDevices = useMemo(
@@ -197,7 +197,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
videoInput,
startUsingDeviceNames,
stopUsingDeviceNames,
],
]
);
return (
@@ -207,8 +207,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
);
};
export const useMediaDevices = (): MediaDevices =>
useContext(MediaDevicesContext);
export const useMediaDevices = () => useContext(MediaDevicesContext);
/**
* React hook that requests for the media devices context to be populated with
@@ -216,10 +215,7 @@ export const useMediaDevices = (): MediaDevices =>
* default because it may involve requesting additional permissions from the
* user.
*/
export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true,
): void =>
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
useEffect(() => {
if (enabled) {
context.startUsingDeviceNames();

View File

@@ -42,14 +42,14 @@ export type OpenIDClientParts = Pick<
export function useOpenIDSFU(
client: OpenIDClientParts,
rtcSession: MatrixRTCSession,
): SFUConfig | undefined {
rtcSession: MatrixRTCSession
) {
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
const activeFocus = useActiveFocus(rtcSession);
useEffect(() => {
(async (): Promise<void> => {
(async () => {
const sfuConfig = activeFocus
? await getSFUConfigWithOpenID(client, activeFocus)
: undefined;
@@ -62,20 +62,20 @@ export function useOpenIDSFU(
export async function getSFUConfigWithOpenID(
client: OpenIDClientParts,
activeFocus: LivekitFocus,
activeFocus: LivekitFocus
): Promise<SFUConfig | undefined> {
const openIdToken = await client.getOpenIdToken();
logger.debug("Got openID token", openIdToken);
try {
logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`,
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`
);
const sfuConfig = await getLiveKitJWT(
client,
activeFocus.livekit_service_url,
activeFocus.livekit_alias,
openIdToken,
openIdToken
);
logger.info(`Got JWT from call's active focus URL.`);
@@ -83,7 +83,7 @@ export async function getSFUConfigWithOpenID(
} catch (e) {
logger.warn(
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
e,
e
);
return undefined;
}
@@ -93,7 +93,7 @@ async function getLiveKitJWT(
client: OpenIDClientParts,
livekitServiceURL: string,
roomName: string,
openIDToken: IOpenIDToken,
openIDToken: IOpenIDToken
): Promise<SFUConfig> {
try {
const res = await fetch(livekitServiceURL + "/sfu/get", {

View File

@@ -1,19 +1,3 @@
/*
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

@@ -19,7 +19,6 @@ import {
ConnectionState,
Room,
RoomEvent,
Track,
} from "livekit-client";
import { useCallback, useEffect, useRef, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
@@ -52,7 +51,7 @@ async function doConnect(
livekitRoom: Room,
sfuConfig: SFUConfig,
audioEnabled: boolean,
audioOptions: AudioCaptureOptions,
audioOptions: AudioCaptureOptions
): Promise<void> {
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
@@ -61,14 +60,6 @@ async function doConnect(
// doesn't publish it until you unmute. We want to publish it from the start so we're
// always capturing audio: it helps keep bluetooth headsets in the right mode and
// mobile browsers to know we're doing a call.
if (livekitRoom!.localParticipant.getTrack(Track.Source.Microphone)) {
logger.warn(
"Pre-creating audio track but participant already appears to have an microphone track: this shouldn't happen!",
);
return;
}
logger.info("Pre-creating microphone track");
const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions,
});
@@ -78,14 +69,6 @@ async function doConnect(
}
if (!audioEnabled) await audioTracks[0].mute();
// check again having awaited for the track to create
if (livekitRoom!.localParticipant.getTrack(Track.Source.Microphone)) {
logger.warn(
"Publishing pre-created audio track but participant already appears to have an microphone track: this shouldn't happen!",
);
return;
}
logger.info("Publishing pre-created mic track");
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]);
}
@@ -93,12 +76,12 @@ export function useECConnectionState(
initialAudioOptions: AudioCaptureOptions,
initialAudioEnabled: boolean,
livekitRoom?: Room,
sfuConfig?: SFUConfig,
sfuConfig?: SFUConfig
): ECConnectionState {
const [connState, setConnState] = useState(
sfuConfig && livekitRoom
? livekitRoom.state
: ECAddonConnectionState.ECWaiting,
: ECAddonConnectionState.ECWaiting
);
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
@@ -133,10 +116,10 @@ export function useECConnectionState(
!sfuConfigEquals(currentSFUConfig.current, sfuConfig)
) {
logger.info(
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`,
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`
);
(async (): Promise<void> => {
(async () => {
setSwitchingFocus(true);
await livekitRoom?.disconnect();
setIsInDoConnect(true);
@@ -145,7 +128,7 @@ export function useECConnectionState(
livekitRoom!,
sfuConfig!,
initialAudioEnabled,
initialAudioOptions,
initialAudioOptions
);
} finally {
setIsInDoConnect(false);
@@ -166,7 +149,7 @@ export function useECConnectionState(
livekitRoom!,
sfuConfig!,
initialAudioEnabled,
initialAudioOptions,
initialAudioOptions
).finally(() => setIsInDoConnect(false));
}

View File

@@ -52,7 +52,7 @@ interface UseLivekitResult {
export function useLiveKit(
muteStates: MuteStates,
sfuConfig?: SFUConfig,
e2eeConfig?: E2EEConfig,
e2eeConfig?: E2EEConfig
): UseLivekitResult {
const e2eeOptions = useMemo(() => {
if (!e2eeConfig?.sharedKey) return undefined;
@@ -67,7 +67,7 @@ export function useLiveKit(
if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
e2eeConfig?.sharedKey,
e2eeConfig?.sharedKey
);
}, [e2eeOptions, e2eeConfig?.sharedKey]);
@@ -93,7 +93,7 @@ export function useLiveKit(
},
e2ee: e2eeOptions,
}),
[e2eeOptions],
[e2eeOptions]
);
// useECConnectionState creates and publishes an audio track by hand. To keep
@@ -127,11 +127,11 @@ export function useLiveKit(
const connectionState = useECConnectionState(
{
deviceId: initialDevices.current.audioInput.selectedId,
deviceId: initialDevices.current.audioOutput.selectedId,
},
initialMuteStates.current.audio.enabled,
room,
sfuConfig,
sfuConfig
);
// Unblock audio once the connection is finished
@@ -154,7 +154,7 @@ export function useLiveKit(
audio: muteStates.audio.enabled,
video: muteStates.video.enabled,
};
const syncMuteStateAudio = async (): Promise<void> => {
const syncMuteStateAudio = async () => {
if (
participant.isMicrophoneEnabled !== buttonEnabled.current.audio &&
!audioMuteUpdating.current
@@ -180,7 +180,7 @@ export function useLiveKit(
syncMuteStateAudio();
}
};
const syncMuteStateVideo = async (): Promise<void> => {
const syncMuteStateVideo = async () => {
if (
participant.isCameraEnabled !== buttonEnabled.current.video &&
!videoMuteUpdating.current
@@ -206,7 +206,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): void => {
const syncDevice = (kind: MediaDeviceKind, device: MediaDevice) => {
const id = device.selectedId;
// Detect if we're trying to use chrome's default device, in which case
@@ -223,11 +223,11 @@ export function useLiveKit(
room.options.audioCaptureDefaults?.deviceId === "default"
) {
const activeMicTrack = Array.from(
room.localParticipant.audioTracks.values(),
room.localParticipant.audioTracks.values()
).find((d) => d.source === Track.Source.Microphone)?.track;
const defaultDevice = device.available.find(
(d) => d.deviceId === "default",
(d) => d.deviceId === "default"
);
if (
defaultDevice &&
@@ -253,7 +253,7 @@ export function useLiveKit(
room
.switchActiveDevice(kind, id)
.catch((e) =>
logger.error(`Failed to sync ${kind} device with LiveKit`, e),
logger.error(`Failed to sync ${kind} device with LiveKit`, e)
);
}
}

View File

@@ -30,7 +30,7 @@ import {
setLogLevel,
} from "livekit-client";
import { App } from "./App";
import App from "./App";
import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer";
@@ -48,7 +48,7 @@ if (!window.isSecureContext) {
fatalError = new Error(
"This app cannot run in an insecure context. To fix this, access the app " +
"via a local loopback address, or serve it over HTTPS.\n" +
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
);
} else if (!navigator.mediaDevices) {
fatalError = new Error("Your browser does not support WebRTC.");
@@ -66,5 +66,5 @@ const history = createBrowserHistory();
root.render(
<StrictMode>
<App history={history} />
</StrictMode>,
</StrictMode>
);

View File

@@ -42,7 +42,7 @@ export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
export class CryptoStoreIntegrityError extends Error {
public constructor() {
constructor() {
super("Crypto store data was expected, but none was found");
}
}
@@ -54,13 +54,13 @@ const SYNC_STORE_NAME = "element-call-sync";
// (It's a good opportunity to make the database names consistent.)
const CRYPTO_STORE_NAME = "element-call-crypto";
function waitForSync(client: MatrixClient): Promise<void> {
function waitForSync(client: MatrixClient) {
return new Promise<void>((resolve, reject) => {
const onSync = (
state: SyncState,
_old: SyncState | null,
data?: ISyncStateData,
): void => {
data?: ISyncStateData
) => {
if (state === "PREPARED") {
client.removeListener(ClientEvent.Sync, onSync);
resolve();
@@ -83,7 +83,7 @@ function secureRandomString(entropyBytes: number): string {
// yet) so just use the built-in one and convert, replace the chars and strip the
// padding from the end (otherwise we'd need to pull in another dependency).
return btoa(
key.reduce((acc, current) => acc + String.fromCharCode(current), ""),
key.reduce((acc, current) => acc + String.fromCharCode(current), "")
)
.replace("+", "-")
.replace("/", "_")
@@ -101,7 +101,7 @@ function secureRandomString(entropyBytes: number): string {
*/
export async function initClient(
clientOptions: ICreateClientOpts,
restore: boolean,
restore: boolean
): Promise<MatrixClient> {
await loadOlm();
@@ -127,7 +127,7 @@ export async function initClient(
// Chrome supports it. (It bundles them fine in production mode.)
workerFactory: import.meta.env.DEV
? undefined
: (): Worker => new IndexedDBWorker(),
: () => new IndexedDBWorker(),
});
} else if (localStorage) {
baseOpts.store = new MemoryStore({ localStorage });
@@ -148,7 +148,7 @@ export async function initClient(
if (indexedDB) {
const cryptoStoreExists = await IndexedDBCryptoStore.exists(
indexedDB,
CRYPTO_STORE_NAME,
CRYPTO_STORE_NAME
);
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
} else if (localStorage) {
@@ -164,7 +164,7 @@ export async function initClient(
if (indexedDB) {
baseOpts.cryptoStore = new IndexedDBCryptoStore(
indexedDB,
CRYPTO_STORE_NAME,
CRYPTO_STORE_NAME
);
} else if (localStorage) {
baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
@@ -198,7 +198,7 @@ export async function initClient(
} catch (error) {
logger.error(
"Error starting matrix client store. Falling back to memory store.",
error,
error
);
client.store = new MemoryStore({ localStorage });
await client.store.startup();
@@ -268,7 +268,7 @@ export function roomNameFromRoomId(roomId: string): string {
.substring(1)
.split("-")
.map((part) =>
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part,
part.length > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part
)
.join(" ")
.toLowerCase();
@@ -297,7 +297,7 @@ interface CreateRoomResult {
export async function createRoom(
client: MatrixClient,
name: string,
e2ee: boolean,
e2ee: boolean
): Promise<CreateRoomResult> {
logger.log(`Creating room for group call`);
const createPromise = client.createRoom({
@@ -332,7 +332,7 @@ export async function createRoom(
// Wait for the room to arrive
await new Promise<void>((resolve, reject) => {
const onRoom = async (room: Room): Promise<void> => {
const onRoom = async (room: Room) => {
if (room.roomId === (await createPromise).room_id) {
resolve();
cleanUp();
@@ -343,7 +343,7 @@ export async function createRoom(
cleanUp();
});
const cleanUp = (): void => {
const cleanUp = () => {
client.off(ClientEvent.Room, onRoom);
};
client.on(ClientEvent.Room, onRoom);
@@ -358,7 +358,7 @@ export async function createRoom(
GroupCallType.Video,
false,
GroupCallIntent.Room,
true,
true
);
let password;
@@ -366,7 +366,7 @@ export async function createRoom(
password = secureRandomString(16);
setLocalStorageItem(
getRoomSharedKeyLocalStorageKey(result.room_id),
password,
password
);
}
@@ -386,7 +386,7 @@ export async function createRoom(
export function getAbsoluteRoomUrl(
roomId: string,
roomName?: string,
password?: string,
password?: string
): string {
return `${window.location.protocol}//${
window.location.host
@@ -402,7 +402,7 @@ export function getAbsoluteRoomUrl(
export function getRelativeRoomUrl(
roomId: string,
roomName?: string,
password?: string,
password?: string
): string {
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
// it in case it came from another client that generated a non url-safe one
@@ -419,7 +419,7 @@ export function getRelativeRoomUrl(
export function getAvatarUrl(
client: MatrixClient,
mxcUrl: string,
avatarSize = 96,
avatarSize = 96
): string {
const width = Math.floor(avatarSize * window.devicePixelRatio);
const height = Math.floor(avatarSize * window.devicePixelRatio);

View File

@@ -23,10 +23,10 @@ limitations under the License.
export async function findDeviceByName(
deviceName: string,
kind: MediaDeviceKind,
devices: MediaDeviceInfo[],
devices: MediaDeviceInfo[]
): Promise<string | undefined> {
const deviceInfo = devices.find(
(d) => d.kind === kind && d.label === deviceName,
(d) => d.kind === kind && d.label === deviceName
);
return deviceInfo?.deviceId;
}

View File

@@ -44,65 +44,65 @@ export class OTelCall {
OTelCallAbstractMediaStreamSpan
>();
public constructor(
constructor(
public userId: string,
public deviceId: string,
public call: MatrixCall,
public span: Span,
public span: Span
) {
if (call.peerConn) {
this.addCallPeerConnListeners();
} else {
this.call.once(
CallEvent.PeerConnectionCreated,
this.addCallPeerConnListeners,
this.addCallPeerConnListeners
);
}
}
public dispose(): void {
public dispose() {
this.call.peerConn?.removeEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged,
this.onCallConnectionStateChanged
);
this.call.peerConn?.removeEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged,
this.onCallSignalingStateChanged
);
this.call.peerConn?.removeEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged,
this.onIceConnectionStateChanged
);
this.call.peerConn?.removeEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged,
this.onIceGatheringStateChanged
);
this.call.peerConn?.removeEventListener(
"icecandidateerror",
this.onIceCandidateError,
this.onIceCandidateError
);
}
private addCallPeerConnListeners = (): void => {
this.call.peerConn?.addEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged,
this.onCallConnectionStateChanged
);
this.call.peerConn?.addEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged,
this.onCallSignalingStateChanged
);
this.call.peerConn?.addEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged,
this.onIceConnectionStateChanged
);
this.call.peerConn?.addEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged,
this.onIceGatheringStateChanged
);
this.call.peerConn?.addEventListener(
"icecandidateerror",
this.onIceCandidateError,
this.onIceCandidateError
);
};
@@ -147,8 +147,8 @@ export class OTelCall {
new OTelCallFeedMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
feed,
),
feed
)
);
}
this.trackFeedSpan.get(feed.stream)?.update(feed);
@@ -171,13 +171,13 @@ export class OTelCall {
new OTelCallTransceiverMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
transStats,
),
transStats
)
);
}
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
prvTransSpan = prvTransSpan.filter(
(prvStreamId) => prvStreamId !== transStats.mid,
(prvStreamId) => prvStreamId !== transStats.mid
);
});
@@ -190,7 +190,7 @@ export class OTelCall {
public end(): void {
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
this.trackTransceiverSpan.forEach((transceiverSpan) =>
transceiverSpan.end(),
transceiverSpan.end()
);
this.span.end();
}

View File

@@ -1,19 +1,3 @@
/*
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";
@@ -30,13 +14,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
public readonly span;
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
protected readonly type: string,
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
protected readonly type: string
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
callSpan,
callSpan
);
const options = {
links: [
@@ -48,13 +32,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
this.span = oTel.tracer.startSpan(this.type, options, ctx);
}
protected upsertTrackSpans(tracks: TrackStats[]): void {
protected upsertTrackSpans(tracks: TrackStats[]) {
let prvTracks: TrackId[] = [...this.trackSpans.keys()];
tracks.forEach((t) => {
if (!this.trackSpans.has(t.id)) {
this.trackSpans.set(
t.id,
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t),
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t)
);
}
this.trackSpans.get(t.id)?.update(t);

View File

@@ -1,19 +1,3 @@
/*
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,
@@ -26,10 +10,10 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
callFeed: CallFeedStats,
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
callFeed: CallFeedStats
) {
const postFix =
callFeed.type === "local" && callFeed.prefix === "from-call-feed"

View File

@@ -1,19 +1,3 @@
/*
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";
@@ -24,13 +8,13 @@ export class OTelCallMediaStreamTrackSpan {
private prev: TrackStats;
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly streamSpan: Span,
data: TrackStats,
readonly oTel: ElementCallOpenTelemetry,
readonly streamSpan: Span,
data: TrackStats
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
streamSpan,
streamSpan
);
const options = {
links: [

View File

@@ -1,19 +1,3 @@
/*
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,
@@ -29,10 +13,10 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr
currentDirection: string;
};
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
stats: TransceiverStats,
constructor(
readonly oTel: ElementCallOpenTelemetry,
readonly callSpan: Span,
stats: TransceiverStats
) {
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
this.span.setAttribute("transceiver.mid", stats.mid);

View File

@@ -62,10 +62,7 @@ export class OTelGroupCallMembership {
};
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
public constructor(
private groupCall: GroupCall,
client: MatrixClient,
) {
constructor(private groupCall: GroupCall, client: MatrixClient) {
const clientId = client.getUserId();
if (clientId) {
this.myUserId = clientId;
@@ -79,14 +76,14 @@ export class OTelGroupCallMembership {
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
}
public dispose(): void {
dispose() {
this.groupCall.removeListener(
GroupCallEvent.CallsChanged,
this.onCallsChanged,
this.onCallsChanged
);
}
public onJoinCall(): void {
public onJoinCall() {
if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started");
@@ -96,28 +93,28 @@ export class OTelGroupCallMembership {
// Create the main span that tracks the time we intend to be in the call
this.callMembershipSpan =
ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.groupCallMembership",
"matrix.groupCallMembership"
);
this.callMembershipSpan.setAttribute(
"matrix.confId",
this.groupCall.groupCallId,
this.groupCall.groupCallId
);
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
this.callMembershipSpan.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name",
this.myMember ? this.myMember.name : "unknown-name"
);
this.groupCallContext = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
this.callMembershipSpan,
this.callMembershipSpan
);
this.callMembershipSpan?.addEvent("matrix.joinCall");
}
public onLeaveCall(): void {
public onLeaveCall() {
if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended");
return;
@@ -130,7 +127,7 @@ export class OTelGroupCallMembership {
this.groupCallContext = undefined;
}
public onUpdateRoomState(event: MatrixEvent): void {
public onUpdateRoomState(event: MatrixEvent) {
if (
!event ||
(!event.getType().startsWith("m.call") &&
@@ -141,11 +138,11 @@ export class OTelGroupCallMembership {
this.callMembershipSpan?.addEvent(
`matrix.roomStateEvent_${event.getType()}`,
ObjectFlattener.flattenVoipEvent(event.getContent()),
ObjectFlattener.flattenVoipEvent(event.getContent())
);
}
public onCallsChanged(calls: CallsByUserAndDevice): void {
public onCallsChanged = (calls: CallsByUserAndDevice) => {
for (const [userId, userCalls] of calls.entries()) {
for (const [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) {
@@ -153,7 +150,7 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
`matrix.call`,
undefined,
this.groupCallContext,
this.groupCallContext
);
// XXX: anonymity
span.setAttribute("matrix.call.target.userId", userId);
@@ -163,7 +160,7 @@ export class OTelGroupCallMembership {
span.setAttribute("matrix.call.target.displayName", displayName);
this.callsByCallId.set(
call.callId,
new OTelCall(userId, deviceId, call, span),
new OTelCall(userId, deviceId, call, span)
);
}
}
@@ -182,9 +179,9 @@ export class OTelGroupCallMembership {
this.callsByCallId.delete(callTrackingInfo.call.callId);
}
}
}
};
public onCallStateChange(call: MatrixCall, newState: CallState): void {
public onCallStateChange(call: MatrixCall, newState: CallState) {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`);
@@ -196,7 +193,7 @@ export class OTelGroupCallMembership {
});
}
public onSendEvent(call: MatrixCall, event: VoipEvent): void {
public onSendEvent(call: MatrixCall, event: VoipEvent) {
const eventType = event.eventType as string;
if (
!eventType.startsWith("m.call") &&
@@ -213,17 +210,17 @@ export class OTelGroupCallMembership {
if (event.type === "toDevice") {
callTrackingInfo.span.addEvent(
`matrix.sendToDeviceEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event),
ObjectFlattener.flattenVoipEvent(event)
);
} else if (event.type === "sendEvent") {
callTrackingInfo.span.addEvent(
`matrix.sendToRoomEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event),
ObjectFlattener.flattenVoipEvent(event)
);
}
}
public onReceivedVoipEvent(event: MatrixEvent): void {
public onReceivedVoipEvent(event: MatrixEvent) {
// 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).
@@ -242,7 +239,7 @@ export class OTelGroupCallMembership {
"matrix.receive_voip_event_unknown_callid",
{
"sender.userId": event.getSender(),
},
}
);
logger.error("Received call event for unknown call ID " + callId);
return;
@@ -254,41 +251,37 @@ export class OTelGroupCallMembership {
});
}
public onToggleMicrophoneMuted(newValue: boolean): void {
public onToggleMicrophoneMuted(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
"matrix.microphone.muted": newValue,
});
}
public onSetMicrophoneMuted(setMuted: boolean): void {
public onSetMicrophoneMuted(setMuted: boolean) {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted,
});
}
public onToggleLocalVideoMuted(newValue: boolean): void {
public onToggleLocalVideoMuted(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue,
});
}
public onSetLocalVideoMuted(setMuted: boolean): void {
public onSetLocalVideoMuted(setMuted: boolean) {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted,
});
}
public onToggleScreensharing(newValue: boolean): void {
public onToggleScreensharing(newValue: boolean) {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue,
});
}
public onSpeaking(
member: RoomMember,
deviceId: string,
speaking: boolean,
): void {
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) {
if (speaking) {
// Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member);
@@ -301,7 +294,7 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.audioActivity",
undefined,
this.groupCallContext,
this.groupCallContext
);
span.setAttribute("matrix.userId", member.userId);
span.setAttribute("matrix.displayName", member.rawDisplayName);
@@ -318,7 +311,7 @@ export class OTelGroupCallMembership {
}
}
public onCallError(error: CallError, call: MatrixCall): void {
public onCallError(error: CallError, call: MatrixCall) {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`);
@@ -328,19 +321,17 @@ export class OTelGroupCallMembership {
callTrackingInfo.span.recordException(error);
}
public onGroupCallError(error: GroupCallError): void {
public onGroupCallError(error: GroupCallError) {
this.callMembershipSpan?.recordException(error);
}
public onUndecryptableToDevice(event: MatrixEvent): void {
public onUndecryptableToDevice(event: MatrixEvent) {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(),
});
}
public onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>,
): void {
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report.report?.callId;
@@ -357,10 +348,10 @@ export class OTelGroupCallMembership {
"call.opponentMemberId": report.report?.opponentMemberId
? report.report?.opponentMemberId
: "unknown",
},
}
);
logger.error(
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`,
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`
);
return;
} else {
@@ -370,26 +361,26 @@ export class OTelGroupCallMembership {
}
public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>,
): void {
statsReport: GroupCallStatsReport<ConnectionStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport,
statsReport.report,
statsReport.report
);
}
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
): void {
statsReport: GroupCallStatsReport<ByteSentStatsReport>
) {
this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport,
statsReport.report,
statsReport.report
);
}
public buildCallStatsSpan(
type: OTelStatsReportType,
report: ByteSentStatsReport | ConnectionStatsReport,
report: ByteSentStatsReport | ConnectionStatsReport
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
@@ -412,7 +403,7 @@ export class OTelGroupCallMembership {
const data = ObjectFlattener.flattenReportObject(type, report);
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
call.span,
call.span
);
const options = {
@@ -426,21 +417,21 @@ export class OTelGroupCallMembership {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
type,
options,
ctx,
ctx
);
span.setAttribute("matrix.callId", callId ?? "unknown");
span.setAttribute(
"matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown",
report.opponentMemberId ? report.opponentMemberId : "unknown"
);
span.addEvent("matrix.call.connection_stats_event", data);
span.end();
}
public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport>,
): void {
statsReport: GroupCallStatsReport<SummaryStatsReport>
) {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport;
@@ -448,12 +439,12 @@ export class OTelGroupCallMembership {
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = setSpan(
opentelemetry.context.active(),
this.callMembershipSpan,
this.callMembershipSpan
);
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.summaryReport",
undefined,
ctx,
ctx
);
if (span === undefined) {
return;
@@ -462,7 +453,7 @@ export class OTelGroupCallMembership {
span.setAttribute("matrix.userId", this.myUserId);
span.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name",
this.myMember ? this.myMember.name : "unknown-name"
);
span.addEvent(type, data);
span.end();

View File

@@ -25,7 +25,7 @@ import {
export class ObjectFlattener {
public static flattenReportObject(
prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport,
report: ConnectionStatsReport | ByteSentStatsReport
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
@@ -33,27 +33,27 @@ export class ObjectFlattener {
}
public static flattenByteSentStatsReportObject(
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
statsReport: GroupCallStatsReport<ByteSentStatsReport>
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.bytesSent.",
0,
0
);
return flatObject;
}
public static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport>,
): Attributes {
static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport>
) {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.summary.",
0,
0
);
return flatObject;
}
@@ -67,7 +67,7 @@ export class ObjectFlattener {
event as unknown as Record<string, unknown>, // XXX Types
flatObject,
"matrix.event.",
0,
0
);
return flatObject;
@@ -77,12 +77,12 @@ export class ObjectFlattener {
obj: Object,
flatObject: Attributes,
prefix: string,
depth: number,
depth: number
): void {
if (depth > 10)
throw new Error(
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
prefix,
prefix
);
let entries;
if (obj instanceof Map) {
@@ -101,7 +101,7 @@ export class ObjectFlattener {
v,
flatObject,
prefix + k + ".",
depth + 1,
depth + 1
);
}
}

View File

@@ -36,7 +36,7 @@ export class ElementCallOpenTelemetry {
private otlpExporter?: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
public static globalInit(): void {
static globalInit(): void {
const config = Config.get();
// we always enable opentelemetry in general. We only enable the OTLP
// collector if a URL is defined (and in future if another setting is defined)
@@ -50,18 +50,18 @@ export class ElementCallOpenTelemetry {
sharedInstance = new ElementCallOpenTelemetry(
config.opentelemetry?.collector_url,
config.rageshake?.submit_url,
config.rageshake?.submit_url
);
}
}
public static get instance(): ElementCallOpenTelemetry {
static get instance(): ElementCallOpenTelemetry {
return sharedInstance;
}
private constructor(
constructor(
collectorUrl: string | undefined,
rageshakeUrl: string | undefined,
rageshakeUrl: string | undefined
) {
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
const providerConfig = {
@@ -77,7 +77,7 @@ export class ElementCallOpenTelemetry {
url: collectorUrl,
});
this._provider.addSpanProcessor(
new SimpleSpanProcessor(this.otlpExporter),
new SimpleSpanProcessor(this.otlpExporter)
);
} else {
logger.info("OTLP collector disabled");
@@ -93,7 +93,7 @@ export class ElementCallOpenTelemetry {
this._tracer = opentelemetry.trace.getTracer(
// This is not the serviceName shown in jaeger
"my-element-call-otl-tracer",
"my-element-call-otl-tracer"
);
}

View File

@@ -40,7 +40,7 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
shouldCloseOnBlur: true,
isDismissable: true,
},
popoverRef,
popoverRef
);
return (
@@ -56,5 +56,5 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
</div>
</FocusScope>
);
},
}
);

View File

@@ -43,7 +43,7 @@ export const PopoverMenuTrigger = forwardRef<
const { menuTriggerProps, menuProps } = useMenuTrigger(
{},
popoverMenuState,
buttonRef,
buttonRef
);
const popoverRef = useRef(null);
@@ -62,7 +62,7 @@ export const PopoverMenuTrigger = forwardRef<
typeof children[1] !== "function"
) {
throw new Error(
"PopoverMenu must have two props. The first being a button and the second being a render prop.",
"PopoverMenu must have two props. The first being a button and the second being a render prop."
);
}

View File

@@ -39,11 +39,7 @@ type ProfileSaveCallback = ({
removeAvatar: boolean;
}) => Promise<void>;
interface UseProfile extends ProfileLoadState {
saveProfile: ProfileSaveCallback;
}
export function useProfile(client: MatrixClient | undefined): UseProfile {
export function useProfile(client: MatrixClient | undefined) {
const [{ success, loading, displayName, avatarUrl, error }, setState] =
useState<ProfileLoadState>(() => {
let user: User | undefined = undefined;
@@ -63,8 +59,8 @@ export function useProfile(client: MatrixClient | undefined): UseProfile {
useEffect(() => {
const onChangeUser = (
_event: MatrixEvent | undefined,
{ displayName, avatarUrl }: User,
): void => {
{ displayName, avatarUrl }: User
) => {
setState({
success: false,
loading: false,
@@ -108,8 +104,9 @@ export function useProfile(client: MatrixClient | undefined): UseProfile {
if (removeAvatar) {
await client.setAvatarUrl("");
} else if (avatar) {
({ content_uri: mxcAvatarUrl } =
await client.uploadContent(avatar));
({ content_uri: mxcAvatarUrl } = await client.uploadContent(
avatar
));
await client.setAvatarUrl(mxcAvatarUrl);
}
@@ -134,7 +131,7 @@ export function useProfile(client: MatrixClient | undefined): UseProfile {
logger.error("Client not initialized before calling saveProfile");
}
},
[client],
[client]
);
return {

View File

@@ -40,14 +40,14 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
e.stopPropagation();
setOpen(false);
},
[setOpen],
[setOpen]
);
const roomSharedKey = useRoomSharedKey(roomId ?? "");
const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
if (roomIsEncrypted && roomSharedKey === undefined) {
logger.error(
"Generating app redirect URL for encrypted room but don't have key available!",
"Generating app redirect URL for encrypted room but don't have key available!"
);
}
@@ -60,7 +60,7 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
const url = new URL(
roomId === null
? window.location.href
: getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined),
: getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined)
);
// Edit the URL to prevent the app selection prompt from appearing a second
// time within the app, and to keep the user confined to the current room

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react";
import { FC, FormEventHandler, useCallback, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
@@ -64,7 +64,7 @@ export const CallEndedView: FC<Props> = ({
PosthogAnalytics.instance.eventQualitySurvey.track(
endedCallId,
feedbackText,
starRating,
starRating
);
setSubmitting(true);
@@ -83,7 +83,7 @@ export const CallEndedView: FC<Props> = ({
}, 1000);
}, 1000);
},
[endedCallId, history, isPasswordlessUser, confineToRoom, starRating],
[endedCallId, history, isPasswordlessUser, confineToRoom, starRating]
);
const createAccountDialog = isPasswordlessUser && (
@@ -148,7 +148,7 @@ export const CallEndedView: FC<Props> = ({
</div>
);
const renderBody = (): ReactNode => {
const renderBody = () => {
if (leaveError) {
return (
<>

View File

@@ -47,7 +47,7 @@ export function GroupCallLoader({
ev.preventDefault();
history.push("/");
},
[history],
[history]
);
switch (groupCallState.kind) {
@@ -66,7 +66,7 @@ export function GroupCallLoader({
<Heading>{t("Call not found")}</Heading>
<Text>
{t(
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.",
"Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key."
)}
</Text>
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { 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";
@@ -40,6 +40,7 @@ import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMembership
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useJoinRule } from "./useJoinRule";
@@ -60,14 +61,14 @@ interface Props {
rtcSession: MatrixRTCSession;
}
export const GroupCallView: FC<Props> = ({
export function GroupCallView({
client,
isPasswordlessUser,
confineToRoom,
preload,
hideHeader,
rtcSession,
}) => {
}: Props) {
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
@@ -110,7 +111,7 @@ export const GroupCallView: FC<Props> = ({
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo(
() => new Set<string>(memberships.map((m) => m.sender!)).size,
[memberships],
[memberships]
);
const deviceContext = useMediaDevices();
@@ -124,9 +125,7 @@ export const GroupCallView: FC<Props> = ({
useEffect(() => {
if (widget && preload) {
// In preload mode, wait for a join action before entering
const onJoin = async (
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
// 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
@@ -142,14 +141,14 @@ export const GroupCallView: FC<Props> = ({
const deviceId = await findDeviceByName(
audioInput,
"audioinput",
devices,
devices
);
if (!deviceId) {
logger.warn("Unknown audio input: " + audioInput);
latestMuteStates.current!.audio.setEnabled?.(false);
} else {
logger.debug(
`Found audio input ID ${deviceId} for name ${audioInput}`,
`Found audio input ID ${deviceId} for name ${audioInput}`
);
latestDevices.current!.audioInput.select(deviceId);
latestMuteStates.current!.audio.setEnabled?.(true);
@@ -162,14 +161,14 @@ export const GroupCallView: FC<Props> = ({
const deviceId = await findDeviceByName(
videoInput,
"videoinput",
devices,
devices
);
if (!deviceId) {
logger.warn("Unknown video input: " + videoInput);
latestMuteStates.current!.video.setEnabled?.(false);
} else {
logger.debug(
`Found video input ID ${deviceId} for name ${videoInput}`,
`Found video input ID ${deviceId} for name ${videoInput}`
);
latestDevices.current!.videoInput.select(deviceId);
latestMuteStates.current!.video.setEnabled?.(true);
@@ -181,7 +180,7 @@ export const GroupCallView: FC<Props> = ({
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
// we only have room sessions right now, so call ID is the emprty string - we use the room ID
PosthogAnalytics.instance.eventCallStarted.track(
rtcSession.room.roomId,
rtcSession.room.roomId
);
await Promise.all([
@@ -212,7 +211,7 @@ export const GroupCallView: FC<Props> = ({
PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId,
rtcSession.memberships.length,
sendInstantly,
sendInstantly
);
await leaveRTCSession(rtcSession);
@@ -236,16 +235,14 @@ export const GroupCallView: FC<Props> = ({
history.push("/");
}
},
[rtcSession, isPasswordlessUser, confineToRoom, history],
[rtcSession, isPasswordlessUser, confineToRoom, history]
);
useEffect(() => {
if (widget && isJoined) {
const onHangup = async (
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
leaveRTCSession(rtcSession);
widget!.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {});
widget!.api.setAlwaysOnScreen(false);
};
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
@@ -255,9 +252,11 @@ export const GroupCallView: FC<Props> = ({
}
}, [isJoined, rtcSession]);
const [e2eeEnabled] = useEnableE2EE();
const e2eeConfig = useMemo(
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
[e2eeSharedKey],
[e2eeSharedKey]
);
const onReconnect = useCallback(() => {
@@ -271,12 +270,12 @@ export const GroupCallView: FC<Props> = ({
const [shareModalOpen, setInviteModalOpen] = useState(false);
const onDismissInviteModal = useCallback(
() => setInviteModalOpen(false),
[setInviteModalOpen],
[setInviteModalOpen]
);
const onShareClickFn = useCallback(
() => setInviteModalOpen(true),
[setInviteModalOpen],
[setInviteModalOpen]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -285,17 +284,17 @@ export const GroupCallView: FC<Props> = ({
ev.preventDefault();
history.push("/");
},
[history],
[history]
);
const { t } = useTranslation();
if (isRoomE2EE && !e2eeSharedKey) {
if (e2eeEnabled && isRoomE2EE && !e2eeSharedKey) {
return (
<ErrorView
error={
new Error(
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button.",
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button."
)
}
/>
@@ -306,7 +305,7 @@ export const GroupCallView: FC<Props> = ({
<Heading>Incompatible Browser</Heading>
<Text>
{t(
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117",
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117"
)}
</Text>
<Link href="/" onClick={onHomeClick}>
@@ -314,6 +313,8 @@ export const GroupCallView: FC<Props> = ({
</Link>
</FullScreenView>
);
} else if (!e2eeEnabled && isRoomE2EE) {
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
}
const shareModal = (
@@ -380,7 +381,7 @@ export const GroupCallView: FC<Props> = ({
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={(): void => enterRTCSession(rtcSession)}
onEnter={() => enterRTCSession(rtcSession)}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participantCount={participantCount}
@@ -389,4 +390,4 @@ export const GroupCallView: FC<Props> = ({
</>
);
}
};
}

View File

@@ -27,16 +27,7 @@ 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 {
FC,
ReactNode,
Ref,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { 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";
@@ -87,6 +78,9 @@ import {
import { useOpenIDSFU } from "../livekit/openIDSFU";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
// or with getUsermedia and getDisplaymedia being used within the same session.
// For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// How long we wait after a focus switch before showing the real participant list again
@@ -97,12 +91,12 @@ export interface ActiveCallProps
e2eeConfig?: E2EEConfig;
}
export const ActiveCall: FC<ActiveCallProps> = (props) => {
export function ActiveCall(props: ActiveCallProps) {
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
const { livekitRoom, connState } = useLiveKit(
props.muteStates,
sfuConfig,
props.e2eeConfig,
props.e2eeConfig
);
if (!livekitRoom) {
@@ -118,7 +112,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
</RoomContext.Provider>
);
};
}
export interface InCallViewProps {
client: MatrixClient;
@@ -134,7 +128,7 @@ export interface InCallViewProps {
onShareClick: (() => void) | null;
}
export const InCallView: FC<InCallViewProps> = ({
export function InCallView({
client,
matrixInfo,
rtcSession,
@@ -146,7 +140,7 @@ export const InCallView: FC<InCallViewProps> = ({
otelGroupCallMembership,
connState,
onShareClick,
}) => {
}: InCallViewProps) {
const { t } = useTranslation();
usePreventScroll();
useWakeLock();
@@ -169,10 +163,10 @@ export const InCallView: FC<InCallViewProps> = ({
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
{
room: livekitRoom,
},
}
);
const { layout, setLayout } = useVideoGridLayout(
screenSharingTracks.length > 0,
screenSharingTracks.length > 0
);
const [showConnectionStats] = useShowConnectionStats();
@@ -185,11 +179,11 @@ export const InCallView: FC<InCallViewProps> = ({
const toggleMicrophone = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e),
[muteStates],
[muteStates]
);
const toggleCamera = useCallback(
() => muteStates.video.setEnabled?.((e) => !e),
[muteStates],
[muteStates]
);
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
@@ -198,7 +192,7 @@ export const InCallView: FC<InCallViewProps> = ({
containerRef1,
toggleMicrophone,
toggleCamera,
(muted) => muteStates.audio.setEnabled?.(!muted),
(muted) => muteStates.audio.setEnabled?.(!muted)
);
const onLeavePress = useCallback(() => {
@@ -210,32 +204,32 @@ export const InCallView: FC<InCallViewProps> = ({
layout === "grid"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{},
{}
);
}, [layout]);
useEffect(() => {
if (widget) {
const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("grid");
widget!.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
setLayout("spotlight");
widget!.api.transport.reply(ev.detail, {});
await widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.on(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
onSpotlightLayout
);
return () => {
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
onSpotlightLayout
);
};
}
@@ -258,7 +252,7 @@ export const InCallView: FC<InCallViewProps> = ({
(noControls
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
: null),
[fullscreenItem, noControls, items],
[fullscreenItem, noControls, items]
);
const Grid =
@@ -301,7 +295,7 @@ export const InCallView: FC<InCallViewProps> = ({
disableAnimations={prefersReducedMotion || isSafari}
layoutStates={layoutStates}
>
{(props): ReactNode => (
{(props) => (
<VideoTile
maximised={false}
fullscreen={false}
@@ -317,18 +311,18 @@ export const InCallView: FC<InCallViewProps> = ({
};
const rageshakeRequestModalProps = useRageshakeRequestModal(
rtcSession.room.roomId,
rtcSession.room.roomId
);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen],
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen],
[setSettingsModalOpen]
);
const toggleScreensharing = useCallback(async () => {
@@ -362,29 +356,25 @@ export const InCallView: FC<InCallViewProps> = ({
onPress={toggleCamera}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>,
/>
);
if (!reducedControls) {
if (canScreenshare && !hideScreensharing) {
if (canScreenshare && !hideScreensharing && !isSafari) {
buttons.push(
<ScreenshareButton
key="3"
enabled={isScreenShareEnabled}
onPress={toggleScreensharing}
data-testid="incall_screenshare"
/>,
/>
);
}
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
}
buttons.push(
<HangupButton
key="6"
onPress={onLeavePress}
data-testid="incall_leave"
/>,
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
);
footer = (
<div className={styles.footer}>
@@ -444,11 +434,11 @@ export const InCallView: FC<InCallViewProps> = ({
/>
</div>
);
};
}
function findMatrixMember(
room: MatrixRoom,
id: string,
id: string
): RoomMember | undefined {
if (!id) return undefined;
@@ -456,7 +446,7 @@ function findMatrixMember(
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
if (parts.length < 3) {
logger.warn(
"Livekit participants ID doesn't look like a userId:deviceId combination",
"Livekit participants ID doesn't look like a userId:deviceId combination"
);
return undefined;
}
@@ -470,7 +460,7 @@ function findMatrixMember(
function useParticipantTiles(
livekitRoom: Room,
matrixRoom: MatrixRoom,
connState: ECConnectionState,
connState: ECConnectionState
): TileDescriptor<ItemData>[] {
const previousTiles = useRef<TileDescriptor<ItemData>[]>([]);
@@ -499,7 +489,7 @@ function useParticipantTiles(
// connected, this is fine and we'll be in "all ghosts" mode.
if (id !== "" && member === undefined) {
logger.warn(
`Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`,
`Ruh, roh! No matrix member found for SFU participant '${id}': creating g-g-g-ghost!`
);
}
allGhosts &&= member === undefined;
@@ -543,11 +533,11 @@ function useParticipantTiles(
return screenShareTile
? [userMediaTile, screenShareTile]
: [userMediaTile];
},
}
);
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
tiles.length,
tiles.length
);
// If every item is a ghost, that probably means we're still connecting and

View File

@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
const url = useMemo(
() =>
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
[room, roomSharedKey],
[room, roomSharedKey]
);
const [, setCopied] = useClipboard(url);
const [toastOpen, setToastOpen] = useState(false);
@@ -53,7 +53,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
onDismiss();
setToastOpen(true);
},
[setCopied, onDismiss],
[setCopied, onDismiss]
);
return (

View File

@@ -36,7 +36,7 @@ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
[setLayout],
[setLayout]
);
const spotlightId = useId();

View File

@@ -63,22 +63,22 @@ export const LobbyView: FC<Props> = ({
const onAudioPress = useCallback(
() => muteStates.audio.setEnabled?.((e) => !e),
[muteStates],
[muteStates]
);
const onVideoPress = useCallback(
() => muteStates.video.setEnabled?.((e) => !e),
[muteStates],
[muteStates]
);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen],
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen],
[setSettingsModalOpen]
);
const history = useHistory();

View File

@@ -49,18 +49,18 @@ export interface MuteStates {
function useMuteState(
device: MediaDevice,
enabledByDefault: () => boolean,
enabledByDefault: () => boolean
): MuteState {
const [enabled, setEnabled] = useReactiveState<boolean>(
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
[device],
[device]
);
return useMemo(
() =>
device.available.length === 0
? deviceUnavailable
: { enabled, setEnabled },
[device, enabled, setEnabled],
[device, enabled, setEnabled]
);
}
@@ -69,7 +69,7 @@ export function useMuteStates(participantCount: number): MuteStates {
const audio = useMuteState(
devices.audioInput,
() => participantCount <= MUTE_PARTICIPANT_COUNT,
() => participantCount <= MUTE_PARTICIPANT_COUNT
);
const video = useMuteState(devices.videoInput, () => true);

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Props as ModalProps } from "../Modal";
import { Modal, ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
@@ -47,13 +47,13 @@ export const RageshakeRequestModal: FC<Props> = ({
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
<Body>
{t(
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log.",
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
)}
</Body>
<FieldRow>
<Button
onPress={(): void =>
void submitRageshake({
onPress={() =>
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 { FC, useCallback, useState } from "react";
import { useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
@@ -29,7 +29,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config";
export const RoomAuthView: FC = () => {
export function RoomAuthView() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
@@ -52,7 +52,7 @@ export const RoomAuthView: FC = () => {
setError(error);
});
},
[registerPasswordlessUser],
[registerPasswordlessUser]
);
const { t } = useTranslation();
@@ -122,4 +122,4 @@ export const RoomAuthView: FC = () => {
</div>
</>
);
};
}

View File

@@ -81,7 +81,7 @@ export const RoomPage: FC = () => {
hideHeader={hideHeader}
/>
),
[client, passwordlessUser, confineToRoom, preload, hideHeader],
[client, passwordlessUser, confineToRoom, preload, hideHeader]
);
let content: ReactNode;

View File

@@ -82,14 +82,14 @@ export const VideoPreview: FC<Props> = ({
},
(error) => {
logger.error("Error while creating preview Tracks:", error);
},
}
);
const videoTrack = useMemo(
() =>
tracks?.find((t) => t.kind === Track.Kind.Video) as
| LocalVideoTrack
| undefined,
[tracks],
[tracks]
);
const videoEl = useRef<HTMLVideoElement | null>(null);

View File

@@ -20,24 +20,14 @@ import {
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useCallback, useEffect, useState } from "react";
import { deepCompare } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { LivekitFocus } from "../livekit/LivekitFocus";
function getActiveFocus(
rtcSession: MatrixRTCSession,
rtcSession: MatrixRTCSession
): LivekitFocus | undefined {
const oldestMembership = rtcSession.getOldestMembership();
const focus = oldestMembership?.getActiveFoci()[0] as LivekitFocus;
if (focus) {
logger.info(
`Got active focus for call from ${oldestMembership?.sender}/${oldestMembership?.deviceId}`,
focus,
);
}
return focus;
return oldestMembership?.getActiveFoci()[0] as LivekitFocus;
}
/**
@@ -46,10 +36,10 @@ function getActiveFocus(
* and the same focus.
*/
export function useActiveFocus(
rtcSession: MatrixRTCSession,
rtcSession: MatrixRTCSession
): LivekitFocus | undefined {
const [activeFocus, setActiveFocus] = useState(() =>
getActiveFocus(rtcSession),
getActiveFocus(rtcSession)
);
const onMembershipsChanged = useCallback(() => {
@@ -63,13 +53,13 @@ export function useActiveFocus(
useEffect(() => {
rtcSession.on(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
onMembershipsChanged
);
return () => {
rtcSession.off(
MatrixRTCSessionEvent.MembershipsChanged,
onMembershipsChanged,
onMembershipsChanged
);
};
});

View File

@@ -22,11 +22,11 @@ import { TileDescriptor } from "../video-grid/VideoGrid";
import { useReactiveState } from "../useReactiveState";
import { useEventTarget } from "../useEvents";
const isFullscreen = (): boolean =>
const isFullscreen = () =>
Boolean(document.fullscreenElement) ||
Boolean(document.webkitFullscreenElement);
function enterFullscreen(): void {
function enterFullscreen() {
if (document.body.requestFullscreen) {
document.body.requestFullscreen();
} else if (document.body.webkitRequestFullscreen) {
@@ -36,7 +36,7 @@ function enterFullscreen(): void {
}
}
function exitFullscreen(): void {
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
@@ -46,7 +46,7 @@ function exitFullscreen(): void {
}
}
function useFullscreenChange(onFullscreenChange: () => void): void {
function useFullscreenChange(onFullscreenChange: () => void) {
useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
}
@@ -66,7 +66,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
prevItem == null
? null
: items.find((i) => i.id === prevItem.id) ?? null,
[items],
[items]
);
const latestItems = useRef<TileDescriptor<T>[]>(items);
@@ -80,15 +80,15 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
setFullscreenItem(
latestFullscreenItem.current === null
? latestItems.current.find((i) => i.id === itemId) ?? null
: null,
: null
);
},
[setFullscreenItem],
[setFullscreenItem]
);
const exitFullscreenCallback = useCallback(
() => setFullscreenItem(null),
[setFullscreenItem],
[setFullscreenItem]
);
useLayoutEffect(() => {
@@ -103,7 +103,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
useFullscreenChange(
useCallback(() => {
if (!isFullscreen()) setFullscreenItem(null);
}, [setFullscreenItem]),
}, [setFullscreenItem])
);
return {

View File

@@ -15,14 +15,12 @@ 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 function useJoinRule(room: Room): JoinRule {
return useRoomState(
export const useJoinRule = (room: Room) =>
useRoomState(
room,
useCallback((state) => state.getJoinRule(), []),
useCallback((state) => state.getJoinRule(), [])
);
}

View File

@@ -23,6 +23,7 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { useEnableE2EE } from "../settings/useSetting";
export type GroupCallLoaded = {
kind: "loaded";
@@ -51,11 +52,13 @@ export interface GroupCallLoadState {
export const useLoadGroupCall = (
client: MatrixClient,
roomIdOrAlias: string,
viaServers: string[],
viaServers: string[]
): GroupCallStatus => {
const { t } = useTranslation();
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
const [e2eeEnabled] = useEnableE2EE();
useEffect(() => {
const fetchOrCreateRoom = async (): Promise<Room> => {
let room: Room | null = null;
@@ -67,7 +70,7 @@ export const useLoadGroupCall = (
// join anyway but the js-sdk recreates the room if you pass the alias for a
// room you're already joined to (which it probably ought not to).
const lookupResult = await client.getRoomIdForAlias(
roomIdOrAlias.toLowerCase(),
roomIdOrAlias.toLowerCase()
);
logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`);
room = client.getRoom(lookupResult.room_id);
@@ -78,7 +81,7 @@ export const useLoadGroupCall = (
});
} else {
logger.info(
`Already in room ${lookupResult.room_id}, not rejoining.`,
`Already in room ${lookupResult.room_id}, not rejoining.`
);
}
} else {
@@ -89,7 +92,7 @@ export const useLoadGroupCall = (
}
logger.info(
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`,
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`
);
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
logger.info(`${roomIdOrAlias}, is ready for group calls`);
@@ -104,13 +107,13 @@ export const useLoadGroupCall = (
return rtcSession;
};
const waitForClientSyncing = async (): Promise<void> => {
const waitForClientSyncing = async () => {
if (client.getSyncState() !== SyncState.Syncing) {
logger.debug(
"useLoadGroupCall: waiting for client to start syncing...",
"useLoadGroupCall: waiting for client to start syncing..."
);
await new Promise<void>((resolve) => {
const onSync = (): void => {
const onSync = () => {
if (client.getSyncState() === SyncState.Syncing) {
client.off(ClientEvent.Sync, onSync);
return resolve();
@@ -126,7 +129,7 @@ export const useLoadGroupCall = (
.then(fetchOrCreateGroupCall)
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
.catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, t]);
}, [client, roomIdOrAlias, viaServers, t, e2eeEnabled]);
return state;
};

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
useTypedEventEmitter(
room,
RoomStateEvent.Update,
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates]),
useCallback(() => setNumUpdates((n) => n + 1), [setNumUpdates])
);
// We want any change to the update counter to trigger an update here
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -33,7 +33,7 @@ function makeFocus(livekitAlias: string): LivekitFocus {
};
}
export function enterRTCSession(rtcSession: MatrixRTCSession): void {
export function enterRTCSession(rtcSession: MatrixRTCSession) {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
@@ -48,7 +48,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession): void {
}
export async function leaveRTCSession(
rtcSession: MatrixRTCSession,
rtcSession: MatrixRTCSession
): Promise<void> {
//groupCallOTelMembership?.onLeaveCall();
await rtcSession.leaveRoomSession();

Some files were not shown because too many files have changed in this diff Show More