Merge remote-tracking branch 'origin/livekit' into dbkr/write_key_with_right_roomid
This commit is contained in:
@@ -1,13 +1,31 @@
|
|||||||
|
const COPYRIGHT_HEADER = `/*
|
||||||
|
Copyright %%CURRENT_YEAR%% New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org"],
|
plugins: ["matrix-org"],
|
||||||
extends: [
|
extends: [
|
||||||
"prettier",
|
|
||||||
"plugin:matrix-org/react",
|
"plugin:matrix-org/react",
|
||||||
"plugin:matrix-org/a11y",
|
"plugin:matrix-org/a11y",
|
||||||
"plugin:matrix-org/typescript",
|
"plugin:matrix-org/typescript",
|
||||||
|
"prettier",
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
project: ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
},
|
},
|
||||||
@@ -15,29 +33,13 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
"jsx-a11y/media-has-caption": ["off"],
|
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
|
||||||
|
"jsx-a11y/media-has-caption": "off",
|
||||||
|
"deprecate/import": "off", // Disabled because it crashes the linter
|
||||||
|
// We should use the js-sdk logger, never console directly.
|
||||||
|
"no-console": ["error"],
|
||||||
},
|
},
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
|
||||||
extends: [
|
|
||||||
"plugin:matrix-org/typescript",
|
|
||||||
"plugin:matrix-org/react",
|
|
||||||
"prettier",
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
// We're aiming to convert this code to strict mode
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
// We should use the js-sdk logger, never console directly.
|
|
||||||
"no-console": ["error"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: "detect",
|
version: "detect",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module.exports = {
|
|||||||
Array.isArray(item) &&
|
Array.isArray(item) &&
|
||||||
item.length > 0 &&
|
item.length > 0 &&
|
||||||
item[0].name === "vite-plugin-mdx"
|
item[0].name === "vite-plugin-mdx"
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
config.plugins.push(svgrPlugin());
|
config.plugins.push(svgrPlugin());
|
||||||
config.resolve = config.resolve || {};
|
config.resolve = config.resolve || {};
|
||||||
|
|||||||
@@ -105,19 +105,22 @@
|
|||||||
"eslint": "^8.14.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-deprecate": "^0.8.2",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-matrix-org": "^0.4.0",
|
"eslint-plugin-matrix-org": "^1.2.1",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.5.0",
|
"eslint-plugin-react-hooks": "^4.5.0",
|
||||||
|
"eslint-plugin-unicorn": "^48.0.1",
|
||||||
"i18next-parser": "^8.0.0",
|
"i18next-parser": "^8.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.2.2",
|
"jest": "^29.2.2",
|
||||||
"jest-environment-jsdom": "^29.3.1",
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
"jest-mock": "^29.5.0",
|
"jest-mock": "^29.5.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^3.0.0",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
|
"typescript-eslint-language-service": "^5.0.5",
|
||||||
"vite": "^4.2.0",
|
"vite": "^4.2.0",
|
||||||
"vite-plugin-html-template": "^1.1.0",
|
"vite-plugin-html-template": "^1.1.0",
|
||||||
"vite-plugin-svgr": "^4.0.0"
|
"vite-plugin-svgr": "^4.0.0"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -114,5 +114,10 @@
|
|||||||
"Call not found": "Nie znaleziono połączenia",
|
"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.",
|
"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",
|
"You": "Ty",
|
||||||
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117"
|
"Your web browser does not support media end-to-end encryption. Supported Browsers are Chrome, Safari, Firefox >=117": "Twoja przeglądarka nie wspiera szyfrowania end-to-end. Wspierane przeglądarki to Chrome, Safari, Firefox >=117",
|
||||||
|
"Invite": "Zaproś",
|
||||||
|
"Link copied to clipboard": "Skopiowano link do schowka",
|
||||||
|
"Participants": "Uczestnicy",
|
||||||
|
"Copy link": "Kopiuj link",
|
||||||
|
"Invite to this call": "Zaproś do połączenia"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { FC, Suspense, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter as Router,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -41,7 +41,7 @@ interface BackgroundProviderProps {
|
|||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BackgroundProvider = ({ children }: BackgroundProviderProps) => {
|
const BackgroundProvider: FC<BackgroundProviderProps> = ({ children }) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,7 +61,7 @@ interface AppProps {
|
|||||||
history: History;
|
history: History;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App({ history }: AppProps) {
|
export const App: FC<AppProps> = ({ history }) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -109,4 +109,4 @@ export default function App({ history }: AppProps) {
|
|||||||
</BackgroundProvider>
|
</BackgroundProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const Avatar: FC<Props> = ({
|
|||||||
Object.values(Size).includes(size as Size)
|
Object.values(Size).includes(size as Size)
|
||||||
? sizes.get(size as Size)
|
? sizes.get(size as Size)
|
||||||
: (size as number),
|
: (size as number),
|
||||||
[size]
|
[size],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedSrc = useMemo(() => {
|
const resolvedSrc = useMemo(() => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
import styles from "./Banner.module.css";
|
import styles from "./Banner.module.css";
|
||||||
|
|
||||||
@@ -22,6 +22,6 @@ interface Props {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Banner = ({ children }: Props) => {
|
export const Banner: FC<Props> = ({ children }) => {
|
||||||
return <div className={styles.banner}>{children}</div>;
|
return <div className={styles.banner}>{children}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export type SetClientParams = {
|
|||||||
|
|
||||||
const ClientContext = createContext<ClientState | undefined>(undefined);
|
const ClientContext = createContext<ClientState | undefined>(undefined);
|
||||||
|
|
||||||
export const useClientState = () => useContext(ClientContext);
|
export const useClientState = (): ClientState | undefined =>
|
||||||
|
useContext(ClientContext);
|
||||||
|
|
||||||
export function useClient(): {
|
export function useClient(): {
|
||||||
client?: MatrixClient;
|
client?: MatrixClient;
|
||||||
@@ -189,7 +190,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
user: session.user_id,
|
user: session.user_id,
|
||||||
password: session.tempPassword,
|
password: session.tempPassword,
|
||||||
},
|
},
|
||||||
password
|
password,
|
||||||
);
|
);
|
||||||
|
|
||||||
saveSession({ ...session, passwordlessUser: false });
|
saveSession({ ...session, passwordlessUser: false });
|
||||||
@@ -199,7 +200,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
passwordlessUser: false,
|
passwordlessUser: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[initClientState?.client]
|
[initClientState?.client],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setClient = useCallback(
|
const setClient = useCallback(
|
||||||
@@ -221,7 +222,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
setInitClientState(null);
|
setInitClientState(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[initClientState?.client]
|
[initClientState?.client],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logout = useCallback(async () => {
|
const logout = useCallback(async () => {
|
||||||
@@ -249,7 +250,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
|
const [alreadyOpenedErr, setAlreadyOpenedErr] = useState<Error | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
useEventTarget(
|
useEventTarget(
|
||||||
loadChannel,
|
loadChannel,
|
||||||
@@ -257,9 +258,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
initClientState?.client.stopClient();
|
initClientState?.client.stopClient();
|
||||||
setAlreadyOpenedErr(
|
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);
|
const [isDisconnected, setIsDisconnected] = useState(false);
|
||||||
@@ -300,7 +301,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
|
(state: SyncState, _old: SyncState | null, data?: ISyncStateData) => {
|
||||||
setIsDisconnected(clientIsDisconnected(state, data));
|
setIsDisconnected(clientIsDisconnected(state, data));
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -386,7 +387,7 @@ async function loadClient(): Promise<InitResult | null> {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
"The previous session was lost, and we couldn't log it out, " +
|
"The previous session was lost, and we couldn't log it out, " +
|
||||||
err +
|
err +
|
||||||
"either"
|
"either",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,8 +409,8 @@ export interface Session {
|
|||||||
tempPassword?: string;
|
tempPassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
const clearSession = (): void => localStorage.removeItem("matrix-auth-store");
|
||||||
const saveSession = (s: Session) =>
|
const saveSession = (s: Session): void =>
|
||||||
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
|
localStorage.setItem("matrix-auth-store", JSON.stringify(s));
|
||||||
const loadSession = (): Session | undefined => {
|
const loadSession = (): Session | undefined => {
|
||||||
const data = localStorage.getItem("matrix-auth-store");
|
const data = localStorage.getItem("matrix-auth-store");
|
||||||
@@ -422,5 +423,6 @@ const loadSession = (): Session | undefined => {
|
|||||||
|
|
||||||
const clientIsDisconnected = (
|
const clientIsDisconnected = (
|
||||||
syncState: SyncState,
|
syncState: SyncState,
|
||||||
syncData?: ISyncStateData
|
syncData?: ISyncStateData,
|
||||||
) => syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
|
): boolean =>
|
||||||
|
syncState === "ERROR" && syncData?.error?.name === "ConnectionError";
|
||||||
|
|||||||
@@ -15,22 +15,22 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { HTMLAttributes, ReactNode } from "react";
|
import { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import styles from "./DisconnectedBanner.module.css";
|
import styles from "./DisconnectedBanner.module.css";
|
||||||
import { ValidClientState, useClientState } from "./ClientContext";
|
import { ValidClientState, useClientState } from "./ClientContext";
|
||||||
|
|
||||||
interface DisconnectedBannerProps extends HTMLAttributes<HTMLElement> {
|
interface Props extends HTMLAttributes<HTMLElement> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DisconnectedBanner({
|
export const DisconnectedBanner: FC<Props> = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...rest
|
...rest
|
||||||
}: DisconnectedBannerProps) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const clientState = useClientState();
|
const clientState = useClientState();
|
||||||
let shouldShowBanner = false;
|
let shouldShowBanner = false;
|
||||||
@@ -50,4 +50,4 @@ export function DisconnectedBanner({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
import { Banner } from "./Banner";
|
import { Banner } from "./Banner";
|
||||||
import styles from "./E2EEBanner.module.css";
|
import styles from "./E2EEBanner.module.css";
|
||||||
import LockOffIcon from "./icons/LockOff.svg?react";
|
import LockOffIcon from "./icons/LockOff.svg?react";
|
||||||
import { useEnableE2EE } from "./settings/useSetting";
|
import { useEnableE2EE } from "./settings/useSetting";
|
||||||
|
|
||||||
export const E2EEBanner = () => {
|
export const E2EEBanner: FC = () => {
|
||||||
const [e2eeEnabled] = useEnableE2EE();
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
if (e2eeEnabled) return null;
|
if (e2eeEnabled) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode, useCallback, useEffect } from "react";
|
import { FC, ReactNode, useCallback, useEffect } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
@@ -33,7 +33,10 @@ interface FullScreenViewProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FullScreenView({ className, children }: FullScreenViewProps) {
|
export const FullScreenView: FC<FullScreenViewProps> = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.page, className)}>
|
<div className={classNames(styles.page, className)}>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -47,13 +50,13 @@ export function FullScreenView({ className, children }: FullScreenViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ErrorViewProps {
|
interface ErrorViewProps {
|
||||||
error: Error;
|
error: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorView({ error }: ErrorViewProps) {
|
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -96,9 +99,9 @@ export function ErrorView({ error }: ErrorViewProps) {
|
|||||||
)}
|
)}
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function CrashView() {
|
export const CrashView: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onReload = useCallback(() => {
|
const onReload = useCallback(() => {
|
||||||
@@ -127,9 +130,9 @@ export function CrashView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function LoadingView() {
|
export const LoadingView: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -137,4 +140,4 @@ export function LoadingView() {
|
|||||||
<h1>{t("Loading…")}</h1>
|
<h1>{t("Loading…")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -48,5 +48,5 @@ export const Glass = forwardRef<HTMLDivElement, Props>(
|
|||||||
>
|
>
|
||||||
{Children.only(children)}
|
{Children.only(children)}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }: HeaderProps) {
|
export const Header: FC<HeaderProps> = ({ children, className, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<header className={classNames(styles.header, className)} {...rest}>
|
<header className={classNames(styles.header, className)} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -46,26 +46,26 @@ interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
|||||||
hideMobile?: boolean;
|
hideMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeftNav({
|
export const LeftNav: FC<LeftNavProps> = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
hideMobile,
|
hideMobile,
|
||||||
...rest
|
...rest
|
||||||
}: LeftNavProps) {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.nav,
|
styles.nav,
|
||||||
styles.leftNav,
|
styles.leftNav,
|
||||||
{ [styles.hideMobile]: hideMobile },
|
{ [styles.hideMobile]: hideMobile },
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -73,32 +73,32 @@ interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
|||||||
hideMobile?: boolean;
|
hideMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RightNav({
|
export const RightNav: FC<RightNavProps> = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
hideMobile,
|
hideMobile,
|
||||||
...rest
|
...rest
|
||||||
}: RightNavProps) {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.nav,
|
styles.nav,
|
||||||
styles.rightNav,
|
styles.rightNav,
|
||||||
{ [styles.hideMobile]: hideMobile },
|
{ [styles.hideMobile]: hideMobile },
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface HeaderLogoProps {
|
interface HeaderLogoProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderLogo({ className }: HeaderLogoProps) {
|
export const HeaderLogo: FC<HeaderLogoProps> = ({ className }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -110,7 +110,7 @@ export function HeaderLogo({ className }: HeaderLogoProps) {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RoomHeaderInfoProps {
|
interface RoomHeaderInfoProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class LazyEventEmitter extends EventEmitter {
|
|||||||
public addListener(
|
public addListener(
|
||||||
type: string | symbol,
|
type: string | symbol,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
listener: (...args: any[]) => void
|
listener: (...args: any[]) => void,
|
||||||
): this {
|
): this {
|
||||||
return this.on(type, listener);
|
return this.on(type, listener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MutableRefObject, PointerEvent, useCallback, useRef } from "react";
|
import {
|
||||||
|
MutableRefObject,
|
||||||
|
PointerEvent,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
|
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
|
||||||
import { ListState } from "@react-stately/list";
|
import { ListState } from "@react-stately/list";
|
||||||
import { Node } from "@react-types/shared";
|
import { Node } from "@react-types/shared";
|
||||||
@@ -35,7 +41,7 @@ export function ListBox<T>({
|
|||||||
className,
|
className,
|
||||||
listBoxRef,
|
listBoxRef,
|
||||||
...rest
|
...rest
|
||||||
}: ListBoxProps<T>) {
|
}: ListBoxProps<T>): ReactNode {
|
||||||
const ref = useRef<HTMLUListElement>(null);
|
const ref = useRef<HTMLUListElement>(null);
|
||||||
|
|
||||||
const listRef = listBoxRef ?? ref;
|
const listRef = listBoxRef ?? ref;
|
||||||
@@ -66,12 +72,12 @@ interface OptionProps<T> {
|
|||||||
item: Node<T>;
|
item: Node<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Option<T>({ item, state, className }: OptionProps<T>) {
|
function Option<T>({ item, state, className }: OptionProps<T>): ReactNode {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||||
{ key: item.key },
|
{ key: item.key },
|
||||||
state,
|
state,
|
||||||
ref
|
ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hack: remove the onPointerUp event handler and re-wire it to
|
// Hack: remove the onPointerUp event handler and re-wire it to
|
||||||
@@ -91,7 +97,7 @@ function Option<T>({ item, state, className }: OptionProps<T>) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
origPointerUp(e as unknown as PointerEvent<HTMLElement>);
|
origPointerUp(e as unknown as PointerEvent<HTMLElement>);
|
||||||
},
|
},
|
||||||
[origPointerUp]
|
[origPointerUp],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
13
src/Menu.tsx
13
src/Menu.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Key, useRef, useState } from "react";
|
import { Key, ReactNode, useRef, useState } from "react";
|
||||||
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
||||||
import { TreeState, useTreeState } from "@react-stately/tree";
|
import { TreeState, useTreeState } from "@react-stately/tree";
|
||||||
import { mergeProps } from "@react-aria/utils";
|
import { mergeProps } from "@react-aria/utils";
|
||||||
@@ -37,7 +37,7 @@ export function Menu<T extends object>({
|
|||||||
onClose,
|
onClose,
|
||||||
label,
|
label,
|
||||||
...rest
|
...rest
|
||||||
}: MenuProps<T>) {
|
}: MenuProps<T>): ReactNode {
|
||||||
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
|
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||||
@@ -68,7 +68,12 @@ interface MenuItemProps<T> {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
function MenuItem<T>({
|
||||||
|
item,
|
||||||
|
state,
|
||||||
|
onAction,
|
||||||
|
onClose,
|
||||||
|
}: MenuItemProps<T>): ReactNode {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const { menuItemProps } = useMenuItem(
|
const { menuItemProps } = useMenuItem(
|
||||||
{
|
{
|
||||||
@@ -77,7 +82,7 @@ function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
|||||||
onClose,
|
onClose,
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
ref
|
ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isFocused, setFocused] = useState(false);
|
const [isFocused, setFocused] = useState(false);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode, useCallback } from "react";
|
import { FC, ReactNode, useCallback } from "react";
|
||||||
import { AriaDialogProps } from "@react-types/dialog";
|
import { AriaDialogProps } from "@react-types/dialog";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +37,7 @@ import { useMediaQuery } from "./useMediaQuery";
|
|||||||
import { Glass } from "./Glass";
|
import { Glass } from "./Glass";
|
||||||
|
|
||||||
// TODO: Support tabs
|
// TODO: Support tabs
|
||||||
export interface ModalProps extends AriaDialogProps {
|
export interface Props extends AriaDialogProps {
|
||||||
title: string;
|
title: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -59,14 +59,14 @@ export interface ModalProps extends AriaDialogProps {
|
|||||||
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
|
* A modal, taking the form of a drawer / bottom sheet on touchscreen devices,
|
||||||
* and a dialog box on desktop.
|
* and a dialog box on desktop.
|
||||||
*/
|
*/
|
||||||
export function Modal({
|
export const Modal: FC<Props> = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
open,
|
open,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
...rest
|
...rest
|
||||||
}: ModalProps) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Empirically, Chrome on Android can end up not matching (hover: none), but
|
// Empirically, Chrome on Android can end up not matching (hover: none), but
|
||||||
// still matching (pointer: coarse) :/
|
// still matching (pointer: coarse) :/
|
||||||
@@ -75,7 +75,7 @@ export function Modal({
|
|||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (!open) onDismiss?.();
|
if (!open) onDismiss?.();
|
||||||
},
|
},
|
||||||
[onDismiss]
|
[onDismiss],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (touchscreen) {
|
if (touchscreen) {
|
||||||
@@ -92,7 +92,7 @@ export function Modal({
|
|||||||
className,
|
className,
|
||||||
overlayStyles.overlay,
|
overlayStyles.overlay,
|
||||||
styles.modal,
|
styles.modal,
|
||||||
styles.drawer
|
styles.drawer,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
@@ -124,7 +124,7 @@ export function Modal({
|
|||||||
overlayStyles.overlay,
|
overlayStyles.overlay,
|
||||||
overlayStyles.animate,
|
overlayStyles.animate,
|
||||||
styles.modal,
|
styles.modal,
|
||||||
styles.dialog
|
styles.dialog,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@@ -152,4 +152,4 @@ export function Modal({
|
|||||||
</DialogRoot>
|
</DialogRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const Toast: FC<Props> = ({
|
|||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (!open) onDismiss();
|
if (!open) onDismiss();
|
||||||
},
|
},
|
||||||
[onDismiss]
|
[onDismiss],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,7 +91,7 @@ export const Toast: FC<Props> = ({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
overlayStyles.overlay,
|
overlayStyles.overlay,
|
||||||
overlayStyles.animate,
|
overlayStyles.animate,
|
||||||
styles.toast
|
styles.toast,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogTitle asChild>
|
<DialogTitle asChild>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ interface TooltipProps {
|
|||||||
const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
(
|
(
|
||||||
{ state, className, children, ...rest }: TooltipProps,
|
{ state, className, children, ...rest }: TooltipProps,
|
||||||
ref: ForwardedRef<HTMLDivElement>
|
ref: ForwardedRef<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { tooltipProps } = useTooltip(rest, state);
|
const { tooltipProps } = useTooltip(rest, state);
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface TooltipTriggerProps {
|
interface TooltipTriggerProps {
|
||||||
@@ -69,7 +69,7 @@ interface TooltipTriggerProps {
|
|||||||
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
||||||
(
|
(
|
||||||
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
|
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
const tooltipTriggerProps = { delay: 250, ...rest };
|
const tooltipTriggerProps = { delay: 250, ...rest };
|
||||||
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
||||||
@@ -78,7 +78,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
|||||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||||
tooltipTriggerProps,
|
tooltipTriggerProps,
|
||||||
tooltipState,
|
tooltipState,
|
||||||
triggerRef
|
triggerRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { overlayProps } = useOverlayPosition({
|
const { overlayProps } = useOverlayPosition({
|
||||||
@@ -94,7 +94,7 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
|||||||
<children.type
|
<children.type
|
||||||
{...mergeProps<typeof children.props | typeof rest>(
|
{...mergeProps<typeof children.props | typeof rest>(
|
||||||
children.props,
|
children.props,
|
||||||
rest
|
rest,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{tooltipState.isOpen && (
|
{tooltipState.isOpen && (
|
||||||
@@ -110,5 +110,5 @@ export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
|||||||
)}
|
)}
|
||||||
</FocusableProvider>
|
</FocusableProvider>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,5 +37,7 @@ class TranslatedErrorImpl extends TranslatedError {}
|
|||||||
|
|
||||||
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
// i18next-parser can't detect calls to a constructor, so we expose a bare
|
||||||
// function instead
|
// function instead
|
||||||
export const translatedError = (messageKey: string, t: typeof i18n.t) =>
|
export const translatedError = (
|
||||||
new TranslatedErrorImpl(messageKey, t);
|
messageKey: string,
|
||||||
|
t: typeof i18n.t,
|
||||||
|
): TranslatedError => new TranslatedErrorImpl(messageKey, t);
|
||||||
|
|||||||
@@ -119,17 +119,17 @@ interface UrlParams {
|
|||||||
// file.
|
// file.
|
||||||
export function editFragmentQuery(
|
export function editFragmentQuery(
|
||||||
hash: string,
|
hash: string,
|
||||||
edit: (params: URLSearchParams) => URLSearchParams
|
edit: (params: URLSearchParams) => URLSearchParams,
|
||||||
): string {
|
): string {
|
||||||
const fragmentQueryStart = hash.indexOf("?");
|
const fragmentQueryStart = hash.indexOf("?");
|
||||||
const fragmentParams = edit(
|
const fragmentParams = edit(
|
||||||
new URLSearchParams(
|
new URLSearchParams(
|
||||||
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
|
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
return `${hash.substring(
|
return `${hash.substring(
|
||||||
0,
|
0,
|
||||||
fragmentQueryStart
|
fragmentQueryStart,
|
||||||
)}?${fragmentParams.toString()}`;
|
)}?${fragmentParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,30 +137,30 @@ class ParamParser {
|
|||||||
private fragmentParams: URLSearchParams;
|
private fragmentParams: URLSearchParams;
|
||||||
private queryParams: URLSearchParams;
|
private queryParams: URLSearchParams;
|
||||||
|
|
||||||
constructor(search: string, hash: string) {
|
public constructor(search: string, hash: string) {
|
||||||
this.queryParams = new URLSearchParams(search);
|
this.queryParams = new URLSearchParams(search);
|
||||||
|
|
||||||
const fragmentQueryStart = hash.indexOf("?");
|
const fragmentQueryStart = hash.indexOf("?");
|
||||||
this.fragmentParams = new URLSearchParams(
|
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
|
// Normally, URL params should be encoded in the fragment so as to avoid
|
||||||
// leaking them to the server. However, we also check the normal query
|
// leaking them to the server. However, we also check the normal query
|
||||||
// string for backwards compatibility with versions that only used that.
|
// string for backwards compatibility with versions that only used that.
|
||||||
getParam(name: string): string | null {
|
public getParam(name: string): string | null {
|
||||||
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllParams(name: string): string[] {
|
public getAllParams(name: string): string[] {
|
||||||
return [
|
return [
|
||||||
...this.fragmentParams.getAll(name),
|
...this.fragmentParams.getAll(name),
|
||||||
...this.queryParams.getAll(name),
|
...this.queryParams.getAll(name),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getFlagParam(name: string, defaultValue = false): boolean {
|
public getFlagParam(name: string, defaultValue = false): boolean {
|
||||||
const param = this.getParam(name);
|
const param = this.getParam(name);
|
||||||
return param === null ? defaultValue : param !== "false";
|
return param === null ? defaultValue : param !== "false";
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ class ParamParser {
|
|||||||
*/
|
*/
|
||||||
export const getUrlParams = (
|
export const getUrlParams = (
|
||||||
search = window.location.search,
|
search = window.location.search,
|
||||||
hash = window.location.hash
|
hash = window.location.hash,
|
||||||
): UrlParams => {
|
): UrlParams => {
|
||||||
const parser = new ParamParser(search, hash);
|
const parser = new ParamParser(search, hash);
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ export const useUrlParams = (): UrlParams => {
|
|||||||
export function getRoomIdentifierFromUrl(
|
export function getRoomIdentifierFromUrl(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
search: string,
|
search: string,
|
||||||
hash: string
|
hash: string,
|
||||||
): RoomIdentifier {
|
): RoomIdentifier {
|
||||||
let roomAlias: string | null = null;
|
let roomAlias: string | null = null;
|
||||||
pathname = pathname.substring(1); // Strip the "/"
|
pathname = pathname.substring(1); // Strip the "/"
|
||||||
@@ -281,6 +281,6 @@ export const useRoomIdentifier = (): RoomIdentifier => {
|
|||||||
const { pathname, search, hash } = useLocation();
|
const { pathname, search, hash } = useLocation();
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => getRoomIdentifierFromUrl(pathname, search, hash),
|
() => getRoomIdentifierFromUrl(pathname, search, hash),
|
||||||
[pathname, search, hash]
|
[pathname, search, hash],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { FC, ReactNode, useCallback, useMemo } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -31,7 +31,7 @@ import LogoutIcon from "./icons/Logout.svg?react";
|
|||||||
import { Body } from "./typography/Typography";
|
import { Body } from "./typography/Typography";
|
||||||
import styles from "./UserMenu.module.css";
|
import styles from "./UserMenu.module.css";
|
||||||
|
|
||||||
interface UserMenuProps {
|
interface Props {
|
||||||
preventNavigation: boolean;
|
preventNavigation: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
@@ -41,7 +41,7 @@ interface UserMenuProps {
|
|||||||
onAction: (value: string) => void;
|
onAction: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMenu({
|
export const UserMenu: FC<Props> = ({
|
||||||
preventNavigation,
|
preventNavigation,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
@@ -49,7 +49,7 @@ export function UserMenu({
|
|||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
onAction,
|
onAction,
|
||||||
}: UserMenuProps) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ export function UserMenu({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(props: any) => (
|
(props: any): ReactNode => (
|
||||||
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
<Menu {...props} label={t("User menu")} onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label, dataTestid }) => (
|
{items.map(({ key, icon: Icon, label, dataTestid }) => (
|
||||||
<Item key={key} textValue={label}>
|
<Item key={key} textValue={label}>
|
||||||
@@ -141,4 +141,4 @@ export function UserMenu({
|
|||||||
}
|
}
|
||||||
</PopoverMenuTrigger>
|
</PopoverMenuTrigger>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { FC, useCallback, useState } from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { useClientLegacy } from "./ClientContext";
|
import { useClientLegacy } from "./ClientContext";
|
||||||
@@ -26,7 +26,7 @@ interface Props {
|
|||||||
preventNavigation?: boolean;
|
preventNavigation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMenuContainer({ preventNavigation = false }: Props) {
|
export const UserMenuContainer: FC<Props> = ({ preventNavigation = false }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
const { client, logout, authenticated, passwordlessUser } = useClientLegacy();
|
||||||
@@ -34,7 +34,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
|||||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||||
const onDismissSettingsModal = useCallback(
|
const onDismissSettingsModal = useCallback(
|
||||||
() => setSettingsModalOpen(false),
|
() => setSettingsModalOpen(false),
|
||||||
[setSettingsModalOpen]
|
[setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
|
const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>();
|
||||||
@@ -58,7 +58,7 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[history, location, logout, setSettingsModalOpen]
|
[history, location, logout, setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const userName = client?.getUserIdLocalpart() ?? "";
|
const userName = client?.getUserIdLocalpart() ?? "";
|
||||||
@@ -83,4 +83,4 @@ export function UserMenuContainer({ preventNavigation = false }: Props) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class PosthogAnalytics {
|
|||||||
return this.internalInstance;
|
return this.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly posthog: PostHog) {
|
private constructor(private readonly posthog: PostHog) {
|
||||||
const posthogConfig: PosthogSettings = {
|
const posthogConfig: PosthogSettings = {
|
||||||
project_api_key: Config.get().posthog?.api_key,
|
project_api_key: Config.get().posthog?.api_key,
|
||||||
api_host: Config.get().posthog?.api_host,
|
api_host: Config.get().posthog?.api_host,
|
||||||
@@ -146,7 +146,7 @@ export class PosthogAnalytics {
|
|||||||
this.enabled = true;
|
this.enabled = true;
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
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;
|
this.enabled = false;
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ export class PosthogAnalytics {
|
|||||||
|
|
||||||
private sanitizeProperties = (
|
private sanitizeProperties = (
|
||||||
properties: Properties,
|
properties: Properties,
|
||||||
_eventName: string
|
_eventName: string,
|
||||||
): Properties => {
|
): Properties => {
|
||||||
// Callback from posthog to sanitize properties before sending them to the server.
|
// 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.
|
// Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
|
||||||
@@ -183,7 +183,7 @@ export class PosthogAnalytics {
|
|||||||
return properties;
|
return properties;
|
||||||
};
|
};
|
||||||
|
|
||||||
private registerSuperProperties(properties: Properties) {
|
private registerSuperProperties(properties: Properties): void {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
this.posthog.register(properties);
|
this.posthog.register(properties);
|
||||||
}
|
}
|
||||||
@@ -201,8 +201,8 @@ export class PosthogAnalytics {
|
|||||||
private capture(
|
private capture(
|
||||||
eventName: string,
|
eventName: string,
|
||||||
properties: Properties,
|
properties: Properties,
|
||||||
options?: CaptureOptions
|
options?: CaptureOptions,
|
||||||
) {
|
): void {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ export class PosthogAnalytics {
|
|||||||
return this.enabled;
|
return this.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnonymity(anonymity: Anonymity): void {
|
private setAnonymity(anonymity: Anonymity): void {
|
||||||
// Update this.anonymity.
|
// Update this.anonymity.
|
||||||
// To update the anonymity typically you want to call updateAnonymityFromSettings
|
// To update the anonymity typically you want to call updateAnonymityFromSettings
|
||||||
// to ensure this value is in step with the user's settings.
|
// to ensure this value is in step with the user's settings.
|
||||||
@@ -236,7 +236,9 @@ export class PosthogAnalytics {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async identifyUser(analyticsIdGenerator: () => string) {
|
private async identifyUser(
|
||||||
|
analyticsIdGenerator: () => string,
|
||||||
|
): Promise<void> {
|
||||||
if (this.anonymity == Anonymity.Pseudonymous && this.enabled) {
|
if (this.anonymity == Anonymity.Pseudonymous && this.enabled) {
|
||||||
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
|
||||||
// different devices to send the same ID.
|
// different devices to send the same ID.
|
||||||
@@ -258,27 +260,27 @@ export class PosthogAnalytics {
|
|||||||
// The above could fail due to network requests, but not essential to starting the application,
|
// The above could fail due to network requests, but not essential to starting the application,
|
||||||
// so swallow it.
|
// so swallow it.
|
||||||
logger.log(
|
logger.log(
|
||||||
"Unable to identify user for tracking" + (e as Error)?.toString()
|
"Unable to identify user for tracking" + (e as Error)?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (analyticsID) {
|
if (analyticsID) {
|
||||||
this.posthog.identify(analyticsID);
|
this.posthog.identify(analyticsID);
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
"No analyticsID is availble. Should not try to setup posthog"
|
"No analyticsID is availble. Should not try to setup posthog",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAnalyticsId() {
|
private async getAnalyticsId(): Promise<string | null> {
|
||||||
const client: MatrixClient = window.matrixclient;
|
const client: MatrixClient = window.matrixclient;
|
||||||
let accountAnalyticsId;
|
let accountAnalyticsId;
|
||||||
if (widget) {
|
if (widget) {
|
||||||
accountAnalyticsId = getUrlParams().analyticsID;
|
accountAnalyticsId = getUrlParams().analyticsID;
|
||||||
} else {
|
} else {
|
||||||
const accountData = await client.getAccountDataFromServer(
|
const accountData = await client.getAccountDataFromServer(
|
||||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE
|
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||||
);
|
);
|
||||||
accountAnalyticsId = accountData?.id;
|
accountAnalyticsId = accountData?.id;
|
||||||
}
|
}
|
||||||
@@ -291,12 +293,14 @@ export class PosthogAnalytics {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hashedEcAnalyticsId(accountAnalyticsId: string): Promise<string> {
|
private async hashedEcAnalyticsId(
|
||||||
|
accountAnalyticsId: string,
|
||||||
|
): Promise<string> {
|
||||||
const client: MatrixClient = window.matrixclient;
|
const client: MatrixClient = window.matrixclient;
|
||||||
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
|
const posthogIdMaterial = "ec" + accountAnalyticsId + client.getUserId();
|
||||||
const bufferForPosthogId = await crypto.subtle.digest(
|
const bufferForPosthogId = await crypto.subtle.digest(
|
||||||
"sha-256",
|
"sha-256",
|
||||||
Buffer.from(posthogIdMaterial, "utf-8")
|
Buffer.from(posthogIdMaterial, "utf-8"),
|
||||||
);
|
);
|
||||||
const view = new Int32Array(bufferForPosthogId);
|
const view = new Int32Array(bufferForPosthogId);
|
||||||
return Array.from(view)
|
return Array.from(view)
|
||||||
@@ -304,17 +308,17 @@ export class PosthogAnalytics {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAccountAnalyticsId(analyticsID: string) {
|
private async setAccountAnalyticsId(analyticsID: string): Promise<void> {
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
const client = window.matrixclient;
|
const client = window.matrixclient;
|
||||||
|
|
||||||
// the analytics ID only needs to be set in the standalone version.
|
// the analytics ID only needs to be set in the standalone version.
|
||||||
const accountData = await client.getAccountDataFromServer(
|
const accountData = await client.getAccountDataFromServer(
|
||||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE
|
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||||
);
|
);
|
||||||
await client.setAccountData(
|
await client.setAccountData(
|
||||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||||
Object.assign({ id: analyticsID }, accountData)
|
Object.assign({ id: analyticsID }, accountData),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +339,7 @@ export class PosthogAnalytics {
|
|||||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSuperProperties() {
|
private updateSuperProperties(): void {
|
||||||
// Update super properties in posthog with our platform (app version, platform).
|
// Update super properties in posthog with our platform (app version, platform).
|
||||||
// These properties will be subsequently passed in every event.
|
// These properties will be subsequently passed in every event.
|
||||||
//
|
//
|
||||||
@@ -356,7 +360,7 @@ export class PosthogAnalytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateAnonymityAndIdentifyUser(
|
private async updateAnonymityAndIdentifyUser(
|
||||||
pseudonymousOptIn: boolean
|
pseudonymousOptIn: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Update this.anonymity based on the user's analytics opt-in settings
|
// Update this.anonymity based on the user's analytics opt-in settings
|
||||||
const anonymity = pseudonymousOptIn
|
const anonymity = pseudonymousOptIn
|
||||||
@@ -372,11 +376,11 @@ export class PosthogAnalytics {
|
|||||||
this.setRegistrationType(
|
this.setRegistrationType(
|
||||||
window.matrixclient.isGuest() || window.passwordlessUser
|
window.matrixclient.isGuest() || window.passwordlessUser
|
||||||
? RegistrationType.Guest
|
? RegistrationType.Guest
|
||||||
: RegistrationType.Registered
|
: RegistrationType.Registered,
|
||||||
);
|
);
|
||||||
// store the promise to await posthog-tracking-events until the identification is done.
|
// store the promise to await posthog-tracking-events until the identification is done.
|
||||||
this.identificationPromise = this.identifyUser(
|
this.identificationPromise = this.identifyUser(
|
||||||
PosthogAnalytics.getRandomAnalyticsId
|
PosthogAnalytics.getRandomAnalyticsId,
|
||||||
);
|
);
|
||||||
await this.identificationPromise;
|
await this.identificationPromise;
|
||||||
if (this.userRegisteredInThisSession()) {
|
if (this.userRegisteredInThisSession()) {
|
||||||
@@ -391,7 +395,7 @@ export class PosthogAnalytics {
|
|||||||
|
|
||||||
public async trackEvent<E extends IPosthogEvent>(
|
public async trackEvent<E extends IPosthogEvent>(
|
||||||
{ eventName, ...properties }: E,
|
{ eventName, ...properties }: E,
|
||||||
options?: CaptureOptions
|
options?: CaptureOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.identificationPromise) {
|
if (this.identificationPromise) {
|
||||||
// only make calls to posthog after the identificaion is done
|
// only make calls to posthog after the identificaion is done
|
||||||
|
|||||||
@@ -36,18 +36,22 @@ export class CallEndedTracker {
|
|||||||
maxParticipantsCount: 0,
|
maxParticipantsCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
cacheStartCall(time: Date) {
|
public cacheStartCall(time: Date): void {
|
||||||
this.cache.startTime = time;
|
this.cache.startTime = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheParticipantCountChanged(count: number) {
|
public cacheParticipantCountChanged(count: number): void {
|
||||||
this.cache.maxParticipantsCount = Math.max(
|
this.cache.maxParticipantsCount = Math.max(
|
||||||
count,
|
count,
|
||||||
this.cache.maxParticipantsCount
|
this.cache.maxParticipantsCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
track(callId: string, callParticipantsNow: number, sendInstantly: boolean) {
|
public track(
|
||||||
|
callId: string,
|
||||||
|
callParticipantsNow: number,
|
||||||
|
sendInstantly: boolean,
|
||||||
|
): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
||||||
{
|
{
|
||||||
eventName: "CallEnded",
|
eventName: "CallEnded",
|
||||||
@@ -56,7 +60,7 @@ export class CallEndedTracker {
|
|||||||
callParticipantsOnLeave: callParticipantsNow,
|
callParticipantsOnLeave: callParticipantsNow,
|
||||||
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
||||||
},
|
},
|
||||||
{ send_instantly: sendInstantly }
|
{ send_instantly: sendInstantly },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +71,7 @@ interface CallStarted extends IPosthogEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CallStartedTracker {
|
export class CallStartedTracker {
|
||||||
track(callId: string) {
|
public track(callId: string): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
||||||
eventName: "CallStarted",
|
eventName: "CallStarted",
|
||||||
callId: callId,
|
callId: callId,
|
||||||
@@ -86,19 +90,19 @@ export class SignupTracker {
|
|||||||
signupEnd: new Date(0),
|
signupEnd: new Date(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
cacheSignupStart(time: Date) {
|
public cacheSignupStart(time: Date): void {
|
||||||
this.cache.signupStart = time;
|
this.cache.signupStart = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSignupEndTime() {
|
public getSignupEndTime(): Date {
|
||||||
return this.cache.signupEnd;
|
return this.cache.signupEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheSignupEnd(time: Date) {
|
public cacheSignupEnd(time: Date): void {
|
||||||
this.cache.signupEnd = time;
|
this.cache.signupEnd = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
track() {
|
public track(): void {
|
||||||
PosthogAnalytics.instance.trackEvent<Signup>({
|
PosthogAnalytics.instance.trackEvent<Signup>({
|
||||||
eventName: "Signup",
|
eventName: "Signup",
|
||||||
signupDuration: Date.now() - this.cache.signupStart.getTime(),
|
signupDuration: Date.now() - this.cache.signupStart.getTime(),
|
||||||
@@ -112,7 +116,7 @@ interface Login extends IPosthogEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LoginTracker {
|
export class LoginTracker {
|
||||||
track() {
|
public track(): void {
|
||||||
PosthogAnalytics.instance.trackEvent<Login>({
|
PosthogAnalytics.instance.trackEvent<Login>({
|
||||||
eventName: "Login",
|
eventName: "Login",
|
||||||
});
|
});
|
||||||
@@ -127,7 +131,7 @@ interface MuteMicrophone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MuteMicrophoneTracker {
|
export class MuteMicrophoneTracker {
|
||||||
track(targetIsMute: boolean, callId: string) {
|
public track(targetIsMute: boolean, callId: string): void {
|
||||||
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
|
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
|
||||||
eventName: "MuteMicrophone",
|
eventName: "MuteMicrophone",
|
||||||
targetMuteState: targetIsMute ? "mute" : "unmute",
|
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||||
@@ -143,7 +147,7 @@ interface MuteCamera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MuteCameraTracker {
|
export class MuteCameraTracker {
|
||||||
track(targetIsMute: boolean, callId: string) {
|
public track(targetIsMute: boolean, callId: string): void {
|
||||||
PosthogAnalytics.instance.trackEvent<MuteCamera>({
|
PosthogAnalytics.instance.trackEvent<MuteCamera>({
|
||||||
eventName: "MuteCamera",
|
eventName: "MuteCamera",
|
||||||
targetMuteState: targetIsMute ? "mute" : "unmute",
|
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||||
@@ -158,7 +162,7 @@ interface UndecryptableToDeviceEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UndecryptableToDeviceEventTracker {
|
export class UndecryptableToDeviceEventTracker {
|
||||||
track(callId: string) {
|
public track(callId: string): void {
|
||||||
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
|
PosthogAnalytics.instance.trackEvent<UndecryptableToDeviceEvent>({
|
||||||
eventName: "UndecryptableToDeviceEvent",
|
eventName: "UndecryptableToDeviceEvent",
|
||||||
callId,
|
callId,
|
||||||
@@ -174,7 +178,7 @@ interface QualitySurveyEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class QualitySurveyEventTracker {
|
export class QualitySurveyEventTracker {
|
||||||
track(callId: string, feedbackText: string, stars: number) {
|
public track(callId: string, feedbackText: string, stars: number): void {
|
||||||
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
|
PosthogAnalytics.instance.trackEvent<QualitySurveyEvent>({
|
||||||
eventName: "QualitySurvey",
|
eventName: "QualitySurvey",
|
||||||
callId,
|
callId,
|
||||||
@@ -190,7 +194,7 @@ interface CallDisconnectedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CallDisconnectedEventTracker {
|
export class CallDisconnectedEventTracker {
|
||||||
track(reason?: DisconnectReason) {
|
public track(reason?: DisconnectReason): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
|
PosthogAnalytics.instance.trackEvent<CallDisconnectedEvent>({
|
||||||
eventName: "CallDisconnected",
|
eventName: "CallDisconnected",
|
||||||
reason,
|
reason,
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
|
|||||||
* Span processor that extracts certain metrics from spans to send to PostHog
|
* Span processor that extracts certain metrics from spans to send to PostHog
|
||||||
*/
|
*/
|
||||||
export class PosthogSpanProcessor implements SpanProcessor {
|
export class PosthogSpanProcessor implements SpanProcessor {
|
||||||
async forceFlush(): Promise<void> {}
|
public async forceFlush(): Promise<void> {}
|
||||||
|
|
||||||
onStart(span: Span): void {
|
public onStart(span: Span): void {
|
||||||
// Hack: Yield to allow attributes to be set before processing
|
// Hack: Yield to allow attributes to be set before processing
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
switch (span.name) {
|
switch (span.name) {
|
||||||
@@ -55,7 +55,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnd(span: ReadableSpan): void {
|
public onEnd(span: ReadableSpan): void {
|
||||||
switch (span.name) {
|
switch (span.name) {
|
||||||
case "matrix.groupCallMembership":
|
case "matrix.groupCallMembership":
|
||||||
this.onGroupCallMembershipEnd(span);
|
this.onGroupCallMembershipEnd(span);
|
||||||
@@ -148,7 +148,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
|
|||||||
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
|
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
|
||||||
},
|
},
|
||||||
// Send instantly because the window might be closing
|
// Send instantly because the window might be closing
|
||||||
{ send_instantly: true }
|
{ send_instantly: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ export class PosthogSpanProcessor implements SpanProcessor {
|
|||||||
/**
|
/**
|
||||||
* Shutdown the processor.
|
* Shutdown the processor.
|
||||||
*/
|
*/
|
||||||
shutdown(): Promise<void> {
|
public shutdown(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
import { Attributes } from "@opentelemetry/api";
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AttributeValue, Attributes } from "@opentelemetry/api";
|
||||||
import { hrTimeToMicroseconds } from "@opentelemetry/core";
|
import { hrTimeToMicroseconds } from "@opentelemetry/core";
|
||||||
import {
|
import {
|
||||||
SpanProcessor,
|
SpanProcessor,
|
||||||
@@ -6,7 +22,21 @@ import {
|
|||||||
Span,
|
Span,
|
||||||
} from "@opentelemetry/sdk-trace-base";
|
} from "@opentelemetry/sdk-trace-base";
|
||||||
|
|
||||||
const dumpAttributes = (attr: Attributes) =>
|
const dumpAttributes = (
|
||||||
|
attr: Attributes,
|
||||||
|
): {
|
||||||
|
key: string;
|
||||||
|
type:
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "bigint"
|
||||||
|
| "boolean"
|
||||||
|
| "symbol"
|
||||||
|
| "undefined"
|
||||||
|
| "object"
|
||||||
|
| "function";
|
||||||
|
value: AttributeValue | undefined;
|
||||||
|
}[] =>
|
||||||
Object.entries(attr).map(([key, value]) => ({
|
Object.entries(attr).map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
type: typeof value,
|
type: typeof value,
|
||||||
@@ -20,13 +50,13 @@ const dumpAttributes = (attr: Attributes) =>
|
|||||||
export class RageshakeSpanProcessor implements SpanProcessor {
|
export class RageshakeSpanProcessor implements SpanProcessor {
|
||||||
private readonly spans: ReadableSpan[] = [];
|
private readonly spans: ReadableSpan[] = [];
|
||||||
|
|
||||||
async forceFlush(): Promise<void> {}
|
public async forceFlush(): Promise<void> {}
|
||||||
|
|
||||||
onStart(span: Span): void {
|
public onStart(span: Span): void {
|
||||||
this.spans.push(span);
|
this.spans.push(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnd(): void {}
|
public onEnd(): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dumps the spans collected so far as Jaeger-compatible JSON.
|
* Dumps the spans collected so far as Jaeger-compatible JSON.
|
||||||
@@ -110,5 +140,5 @@ export class RageshakeSpanProcessor implements SpanProcessor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {}
|
public async shutdown(): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ limitations under the License.
|
|||||||
// Array.prototype.findLastIndex
|
// Array.prototype.findLastIndex
|
||||||
export function findLastIndex<T>(
|
export function findLastIndex<T>(
|
||||||
array: T[],
|
array: T[],
|
||||||
predicate: (item: T, index: number) => boolean
|
predicate: (item: T, index: number) => boolean,
|
||||||
): number | null {
|
): number | null {
|
||||||
for (let i = array.length - 1; i >= 0; i--) {
|
for (let i = array.length - 1; i >= 0; i--) {
|
||||||
if (predicate(array[i], i)) return i;
|
if (predicate(array[i], i)) return i;
|
||||||
@@ -36,9 +36,9 @@ export function findLastIndex<T>(
|
|||||||
*/
|
*/
|
||||||
export const count = <T>(
|
export const count = <T>(
|
||||||
array: T[],
|
array: T[],
|
||||||
predicate: (item: T, index: number) => boolean
|
predicate: (item: T, index: number) => boolean,
|
||||||
): number =>
|
): number =>
|
||||||
array.reduce(
|
array.reduce(
|
||||||
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
|
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const LoginPage: FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[login, location, history, homeserver, setClient]
|
[login, location, history, homeserver, setClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const RegisterPage: FC = () => {
|
|||||||
|
|
||||||
if (password !== passwordConfirmation) return;
|
if (password !== passwordConfirmation) return;
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async (): Promise<void> => {
|
||||||
setRegistering(true);
|
setRegistering(true);
|
||||||
|
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
@@ -78,7 +78,7 @@ export const RegisterPage: FC = () => {
|
|||||||
password,
|
password,
|
||||||
userName,
|
userName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
passwordlessUser
|
passwordlessUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (client && client?.groupCallEventHandler && passwordlessUser) {
|
if (client && client?.groupCallEventHandler && passwordlessUser) {
|
||||||
@@ -135,7 +135,7 @@ export const RegisterPage: FC = () => {
|
|||||||
execute,
|
execute,
|
||||||
client,
|
client,
|
||||||
setClient,
|
setClient,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -184,7 +184,7 @@ export const RegisterPage: FC = () => {
|
|||||||
required
|
required
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||||
setPassword(e.target.value)
|
setPassword(e.target.value)
|
||||||
}
|
}
|
||||||
value={password}
|
value={password}
|
||||||
@@ -198,7 +198,7 @@ export const RegisterPage: FC = () => {
|
|||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
name="passwordConfirmation"
|
name="passwordConfirmation"
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||||
setPasswordConfirmation(e.target.value)
|
setPasswordConfirmation(e.target.value)
|
||||||
}
|
}
|
||||||
value={passwordConfirmation}
|
value={passwordConfirmation}
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
|||||||
import { initClient } from "../matrix-utils";
|
import { initClient } from "../matrix-utils";
|
||||||
import { Session } from "../ClientContext";
|
import { Session } from "../ClientContext";
|
||||||
|
|
||||||
export const useInteractiveLogin = () =>
|
export function useInteractiveLogin(): (
|
||||||
useCallback<
|
homeserver: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
) => Promise<[MatrixClient, Session]> {
|
||||||
|
return useCallback<
|
||||||
(
|
(
|
||||||
homeserver: string,
|
homeserver: string,
|
||||||
username: string,
|
username: string,
|
||||||
password: string
|
password: string,
|
||||||
) => Promise<[MatrixClient, Session]>
|
) => Promise<[MatrixClient, Session]>
|
||||||
>(async (homeserver: string, username: string, password: string) => {
|
>(async (homeserver: string, username: string, password: string) => {
|
||||||
const authClient = createClient({ baseUrl: homeserver });
|
const authClient = createClient({ baseUrl: homeserver });
|
||||||
@@ -41,8 +45,8 @@ export const useInteractiveLogin = () =>
|
|||||||
},
|
},
|
||||||
password,
|
password,
|
||||||
}),
|
}),
|
||||||
stateUpdated: (...args) => {},
|
stateUpdated: (): void => {},
|
||||||
requestEmailToken: (...args): Promise<{ sid: string }> => {
|
requestEmailToken: (): Promise<{ sid: string }> => {
|
||||||
return Promise.resolve({ sid: "" });
|
return Promise.resolve({ sid: "" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -66,9 +70,9 @@ export const useInteractiveLogin = () =>
|
|||||||
userId: user_id,
|
userId: user_id,
|
||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
},
|
},
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
return [client, session];
|
return [client, session];
|
||||||
}, []);
|
}, []);
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export const useInteractiveRegistration = (): {
|
|||||||
password: string,
|
password: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
recaptchaResponse: string,
|
recaptchaResponse: string,
|
||||||
passwordlessUser: boolean
|
passwordlessUser: boolean,
|
||||||
) => Promise<[MatrixClient, Session]>;
|
) => Promise<[MatrixClient, Session]>;
|
||||||
} => {
|
} => {
|
||||||
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
|
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
|
const [recaptchaKey, setRecaptchaKey] = useState<string | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const authClient = useRef<MatrixClient>();
|
const authClient = useRef<MatrixClient>();
|
||||||
@@ -50,7 +50,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
authClient.current!.registerRequest({}).catch((error) => {
|
authClient.current!.registerRequest({}).catch((error) => {
|
||||||
setPrivacyPolicyUrl(
|
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);
|
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
password: string,
|
password: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
recaptchaResponse: string,
|
recaptchaResponse: string,
|
||||||
passwordlessUser: boolean
|
passwordlessUser: boolean,
|
||||||
): Promise<[MatrixClient, Session]> => {
|
): Promise<[MatrixClient, Session]> => {
|
||||||
const interactiveAuth = new InteractiveAuth({
|
const interactiveAuth = new InteractiveAuth({
|
||||||
matrixClient: authClient.current!,
|
matrixClient: authClient.current!,
|
||||||
@@ -72,7 +72,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
password,
|
password,
|
||||||
auth: auth || undefined,
|
auth: auth || undefined,
|
||||||
}),
|
}),
|
||||||
stateUpdated: (nextStage, status) => {
|
stateUpdated: (nextStage, status): void => {
|
||||||
if (status.error) {
|
if (status.error) {
|
||||||
throw new Error(status.error);
|
throw new Error(status.error);
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestEmailToken: (...args) => {
|
requestEmailToken: (): Promise<{ sid: string }> => {
|
||||||
return Promise.resolve({ sid: "dummy" });
|
return Promise.resolve({ sid: "dummy" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -106,7 +106,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
userId: user_id,
|
userId: user_id,
|
||||||
deviceId: device_id,
|
deviceId: device_id,
|
||||||
},
|
},
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setDisplayName(displayName);
|
await client.setDisplayName(displayName);
|
||||||
@@ -129,7 +129,7 @@ export const useInteractiveRegistration = (): {
|
|||||||
|
|
||||||
return [client, session];
|
return [client, session];
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { privacyPolicyUrl, recaptchaKey, register };
|
return { privacyPolicyUrl, recaptchaKey, register };
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ interface RecaptchaPromiseRef {
|
|||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRecaptcha = (sitekey?: string) => {
|
export function useRecaptcha(sitekey?: string): {
|
||||||
|
execute: () => Promise<string>;
|
||||||
|
reset: () => void;
|
||||||
|
recaptchaId: string;
|
||||||
|
} {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [recaptchaId] = useState(() => randomString(16));
|
const [recaptchaId] = useState(() => randomString(16));
|
||||||
const promiseRef = useRef<RecaptchaPromiseRef>();
|
const promiseRef = useRef<RecaptchaPromiseRef>();
|
||||||
@@ -43,7 +47,7 @@ export const useRecaptcha = (sitekey?: string) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sitekey) return;
|
if (!sitekey) return;
|
||||||
|
|
||||||
const onRecaptchaLoaded = () => {
|
const onRecaptchaLoaded = (): void => {
|
||||||
if (!document.getElementById(recaptchaId)) return;
|
if (!document.getElementById(recaptchaId)) return;
|
||||||
|
|
||||||
window.grecaptcha.render(recaptchaId, {
|
window.grecaptcha.render(recaptchaId, {
|
||||||
@@ -91,11 +95,11 @@ export const useRecaptcha = (sitekey?: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
promiseRef.current = {
|
promiseRef.current = {
|
||||||
resolve: (value) => {
|
resolve: (value): void => {
|
||||||
resolve(value);
|
resolve(value);
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
},
|
},
|
||||||
reject: (error) => {
|
reject: (error): void => {
|
||||||
reject(error);
|
reject(error);
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
},
|
},
|
||||||
@@ -104,7 +108,7 @@ export const useRecaptcha = (sitekey?: string) => {
|
|||||||
window.grecaptcha.execute();
|
window.grecaptcha.execute();
|
||||||
|
|
||||||
const iframe = document.querySelector<HTMLIFrameElement>(
|
const iframe = document.querySelector<HTMLIFrameElement>(
|
||||||
'iframe[src*="recaptcha/api2/bframe"]'
|
'iframe[src*="recaptcha/api2/bframe"]',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (iframe?.parentNode?.parentNode) {
|
if (iframe?.parentNode?.parentNode) {
|
||||||
@@ -120,4 +124,4 @@ export const useRecaptcha = (sitekey?: string) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { execute, reset, recaptchaId };
|
return { execute, reset, recaptchaId };
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
|||||||
randomString(16),
|
randomString(16),
|
||||||
displayName,
|
displayName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
setClient({ client, session });
|
setClient({ client, session });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -56,7 +56,7 @@ export function useRegisterPasswordlessUser(): UseRegisterPasswordlessUserType {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[execute, reset, register, setClient]
|
[execute, reset, register, setClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };
|
return { privacyPolicyUrl, registerPasswordlessUser, recaptchaId };
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ limitations under the License.
|
|||||||
.copyButton {
|
.copyButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
transition: border-color 250ms, background-color 250ms;
|
transition:
|
||||||
|
border-color 250ms,
|
||||||
|
background-color 250ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyButton span {
|
.copyButton span {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { forwardRef } from "react";
|
import { FC, forwardRef } from "react";
|
||||||
import { PressEvent } from "@react-types/shared";
|
import { PressEvent } from "@react-types/shared";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
@@ -94,12 +94,12 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
|
|||||||
onPressStart,
|
onPressStart,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const buttonRef = useObjectRef<HTMLButtonElement>(ref);
|
const buttonRef = useObjectRef<HTMLButtonElement>(ref);
|
||||||
const { buttonProps } = useButton(
|
const { buttonProps } = useButton(
|
||||||
{ onPress, onPressStart, ...rest },
|
{ onPress, onPressStart, ...rest },
|
||||||
buttonRef
|
buttonRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: react-aria's useButton hook prevents form submission via keyboard
|
// 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.on]: on,
|
||||||
[styles.off]: off,
|
[styles.off]: off,
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
{...mergeProps(rest, filteredButtonProps)}
|
{...mergeProps(rest, filteredButtonProps)}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
@@ -132,17 +132,14 @@ export const Button = forwardRef<HTMLButtonElement, Props>(
|
|||||||
</>
|
</>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export function MicButton({
|
export const MicButton: FC<{
|
||||||
muted,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}> = ({ muted, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
|
const Icon = muted ? MicOffSolidIcon : MicOnSolidIcon;
|
||||||
const label = muted ? t("Unmute microphone") : t("Mute microphone");
|
const label = muted ? t("Unmute microphone") : t("Mute microphone");
|
||||||
@@ -154,16 +151,13 @@ export function MicButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function VideoButton({
|
export const VideoButton: FC<{
|
||||||
muted,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}> = ({ muted, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
|
const Icon = muted ? VideoCallOffIcon : VideoCallIcon;
|
||||||
const label = muted ? t("Start video") : t("Stop video");
|
const label = muted ? t("Start video") : t("Stop video");
|
||||||
@@ -175,18 +169,14 @@ export function VideoButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function ScreenshareButton({
|
export const ScreenshareButton: FC<{
|
||||||
enabled,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}> = ({ enabled, className, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const label = enabled ? t("Sharing screen") : t("Share screen");
|
const label = enabled ? t("Sharing screen") : t("Share screen");
|
||||||
|
|
||||||
@@ -197,16 +187,13 @@ export function ScreenshareButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function HangupButton({
|
export const HangupButton: FC<{
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}> = ({ className, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -220,16 +207,13 @@ export function HangupButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function SettingsButton({
|
export const SettingsButton: FC<{
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
// TODO: add all props for <Button>
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}> = ({ className, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -239,7 +223,7 @@ export function SettingsButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AudioButtonProps extends Omit<Props, "variant"> {
|
interface AudioButtonProps extends Omit<Props, "variant"> {
|
||||||
/**
|
/**
|
||||||
@@ -248,7 +232,7 @@ interface AudioButtonProps extends Omit<Props, "variant"> {
|
|||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
export const AudioButton: FC<AudioButtonProps> = ({ volume, ...rest }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -258,16 +242,16 @@ export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface FullscreenButtonProps extends Omit<Props, "variant"> {
|
interface FullscreenButtonProps extends Omit<Props, "variant"> {
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FullscreenButton({
|
export const FullscreenButton: FC<FullscreenButtonProps> = ({
|
||||||
fullscreen,
|
fullscreen,
|
||||||
...rest
|
...rest
|
||||||
}: FullscreenButtonProps) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = fullscreen ? FullscreenExit : Fullscreen;
|
const Icon = fullscreen ? FullscreenExit : Fullscreen;
|
||||||
const label = fullscreen ? t("Exit full screen") : t("Full screen");
|
const label = fullscreen ? t("Exit full screen") : t("Full screen");
|
||||||
@@ -279,4 +263,4 @@ export function FullscreenButton({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useClipboard from "react-use-clipboard";
|
import useClipboard from "react-use-clipboard";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
import CheckIcon from "../icons/Check.svg?react";
|
import CheckIcon from "../icons/Check.svg?react";
|
||||||
import CopyIcon from "../icons/Copy.svg?react";
|
import CopyIcon from "../icons/Copy.svg?react";
|
||||||
@@ -28,14 +29,15 @@ interface Props {
|
|||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
copiedMessage?: string;
|
copiedMessage?: string;
|
||||||
}
|
}
|
||||||
export function CopyButton({
|
|
||||||
|
export const CopyButton: FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
copiedMessage,
|
copiedMessage,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
const [isCopied, setCopied] = useClipboard(value, { successDuration: 3000 });
|
||||||
|
|
||||||
@@ -62,4 +64,4 @@ export function CopyButton({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HTMLAttributes } from "react";
|
import { FC, HTMLAttributes } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import * as H from "history";
|
import * as H from "history";
|
||||||
@@ -34,20 +34,20 @@ interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinkButton({
|
export const LinkButton: FC<Props> = ({
|
||||||
children,
|
children,
|
||||||
to,
|
to,
|
||||||
size,
|
size,
|
||||||
variant,
|
variant,
|
||||||
className,
|
className,
|
||||||
...rest
|
...rest
|
||||||
}: Props) {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={classNames(
|
className={classNames(
|
||||||
variantToClassName[variant || "secondary"],
|
variantToClassName[variant || "secondary"],
|
||||||
size ? sizeToClassName[size] : [],
|
size ? sizeToClassName[size] : [],
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
to={to}
|
to={to}
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -55,4 +55,4 @@ export function LinkButton({
|
|||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadConfig(
|
async function downloadConfig(
|
||||||
configJsonFilename: string
|
configJsonFilename: string,
|
||||||
): Promise<ConfigOptions> {
|
): Promise<ConfigOptions> {
|
||||||
const url = new URL(configJsonFilename, window.location.href);
|
const url = new URL(configJsonFilename, window.location.href);
|
||||||
url.searchParams.set("cachebuster", Date.now().toString());
|
url.searchParams.set("cachebuster", Date.now().toString());
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ export const Form = forwardRef<HTMLFormElement, FormProps>(
|
|||||||
{children}
|
{children}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { Avatar, Size } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
@@ -31,7 +32,8 @@ interface CallListProps {
|
|||||||
rooms: GroupCallRoom[];
|
rooms: GroupCallRoom[];
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
export function CallList({ rooms, client }: CallListProps) {
|
|
||||||
|
export const CallList: FC<CallListProps> = ({ rooms, client }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
@@ -54,7 +56,7 @@ export function CallList({ rooms, client }: CallListProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
interface CallTileProps {
|
interface CallTileProps {
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@@ -62,7 +64,8 @@ interface CallTileProps {
|
|||||||
participants: RoomMember[];
|
participants: RoomMember[];
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
function CallTile({ name, avatarUrl, room }: CallTileProps) {
|
|
||||||
|
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
|
||||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
const roomSharedKey = useRoomSharedKey(room.roomId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +74,7 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) {
|
|||||||
to={getRelativeRoomUrl(
|
to={getRelativeRoomUrl(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
room.name,
|
room.name,
|
||||||
roomSharedKey ?? undefined
|
roomSharedKey ?? undefined,
|
||||||
)}
|
)}
|
||||||
className={styles.callTileLink}
|
className={styles.callTileLink}
|
||||||
>
|
>
|
||||||
@@ -89,9 +92,9 @@ function CallTile({ name, avatarUrl, room }: CallTileProps) {
|
|||||||
value={getAbsoluteRoomUrl(
|
value={getAbsoluteRoomUrl(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
room.name,
|
room.name,
|
||||||
roomSharedKey ?? undefined
|
roomSharedKey ?? undefined,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
import { useClientState } from "../ClientContext";
|
import { useClientState } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
@@ -22,7 +23,7 @@ import { UnauthenticatedView } from "./UnauthenticatedView";
|
|||||||
import { RegisteredView } from "./RegisteredView";
|
import { RegisteredView } from "./RegisteredView";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
|
||||||
export function HomePage() {
|
export const HomePage: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePageTitle(t("Home"));
|
usePageTitle(t("Home"));
|
||||||
|
|
||||||
@@ -39,4 +40,4 @@ export function HomePage() {
|
|||||||
<UnauthenticatedView />
|
<UnauthenticatedView />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import { PressEvent } from "@react-types/shared";
|
import { PressEvent } from "@react-types/shared";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
@@ -28,7 +29,11 @@ interface Props {
|
|||||||
onJoin: (e: PressEvent) => void;
|
onJoin: (e: PressEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
|
export const JoinExistingCallModal: FC<Props> = ({
|
||||||
|
onJoin,
|
||||||
|
open,
|
||||||
|
onDismiss,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,4 +47,4 @@ export function JoinExistingCallModal({ onJoin, open, onDismiss }: Props) {
|
|||||||
</FieldRow>
|
</FieldRow>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, FormEvent, FormEventHandler } from "react";
|
import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -46,7 +46,7 @@ interface Props {
|
|||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegisteredView({ client }: Props) {
|
export const RegisteredView: FC<Props> = ({ client }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
const [optInAnalytics] = useOptInAnalytics();
|
const [optInAnalytics] = useOptInAnalytics();
|
||||||
@@ -56,7 +56,7 @@ export function RegisteredView({ client }: Props) {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const onDismissJoinExistingCallModal = useCallback(
|
const onDismissJoinExistingCallModal = useCallback(
|
||||||
() => setJoinExistingCallModalOpen(false),
|
() => setJoinExistingCallModalOpen(false),
|
||||||
[setJoinExistingCallModalOpen]
|
[setJoinExistingCallModalOpen],
|
||||||
);
|
);
|
||||||
const [e2eeEnabled] = useEnableE2EE();
|
const [e2eeEnabled] = useEnableE2EE();
|
||||||
|
|
||||||
@@ -70,22 +70,22 @@ export function RegisteredView({ client }: Props) {
|
|||||||
? sanitiseRoomNameInput(roomNameData)
|
? sanitiseRoomNameInput(roomNameData)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
async function submit() {
|
async function submit(): Promise<void> {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const createRoomResult = await createRoom(
|
const createRoomResult = await createRoom(
|
||||||
client,
|
client,
|
||||||
roomName,
|
roomName,
|
||||||
e2eeEnabled ?? false
|
e2eeEnabled ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
getRelativeRoomUrl(
|
getRelativeRoomUrl(
|
||||||
createRoomResult.roomId,
|
createRoomResult.roomId,
|
||||||
roomName,
|
roomName,
|
||||||
createRoomResult.password
|
createRoomResult.password,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export function RegisteredView({ client }: Props) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client, history, setJoinExistingCallModalOpen, e2eeEnabled]
|
[client, history, setJoinExistingCallModalOpen, e2eeEnabled],
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
const recentRooms = useGroupCallRooms(client);
|
||||||
@@ -175,4 +175,4 @@ export function RegisteredView({ client }: Props) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const onDismissJoinExistingCallModal = useCallback(
|
const onDismissJoinExistingCallModal = useCallback(
|
||||||
() => setJoinExistingCallModalOpen(false),
|
() => setJoinExistingCallModalOpen(false),
|
||||||
[setJoinExistingCallModalOpen]
|
[setJoinExistingCallModalOpen],
|
||||||
);
|
);
|
||||||
const [onFinished, setOnFinished] = useState<() => void>();
|
const [onFinished, setOnFinished] = useState<() => void>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -72,7 +72,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
const roomName = sanitiseRoomNameInput(data.get("callName") as string);
|
const roomName = sanitiseRoomNameInput(data.get("callName") as string);
|
||||||
const displayName = data.get("displayName") as string;
|
const displayName = data.get("displayName") as string;
|
||||||
|
|
||||||
async function submit() {
|
async function submit(): Promise<void> {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recaptchaResponse = await execute();
|
const recaptchaResponse = await execute();
|
||||||
@@ -82,7 +82,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
randomString(16),
|
randomString(16),
|
||||||
displayName,
|
displayName,
|
||||||
recaptchaResponse,
|
recaptchaResponse,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let createRoomResult;
|
let createRoomResult;
|
||||||
@@ -90,7 +90,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
createRoomResult = await createRoom(
|
createRoomResult = await createRoom(
|
||||||
client,
|
client,
|
||||||
roomName,
|
roomName,
|
||||||
e2eeEnabled ?? false
|
e2eeEnabled ?? false,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!setClient) {
|
if (!setClient) {
|
||||||
@@ -124,8 +124,8 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
getRelativeRoomUrl(
|
getRelativeRoomUrl(
|
||||||
createRoomResult.roomId,
|
createRoomResult.roomId,
|
||||||
roomName,
|
roomName,
|
||||||
createRoomResult.password
|
createRoomResult.password,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
setJoinExistingCallModalOpen,
|
setJoinExistingCallModalOpen,
|
||||||
setClient,
|
setClient,
|
||||||
e2eeEnabled,
|
e2eeEnabled,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface GroupCallRoom {
|
|||||||
}
|
}
|
||||||
const tsCache: { [index: string]: number } = {};
|
const tsCache: { [index: string]: number } = {};
|
||||||
|
|
||||||
function getLastTs(client: MatrixClient, r: Room) {
|
function getLastTs(client: MatrixClient, r: Room): number {
|
||||||
if (tsCache[r.roomId]) {
|
if (tsCache[r.roomId]) {
|
||||||
return tsCache[r.roomId];
|
return tsCache[r.roomId];
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ function getLastTs(client: MatrixClient, r: Room) {
|
|||||||
if (r.getMyMembership() !== "join") {
|
if (r.getMyMembership() !== "join") {
|
||||||
const membershipEvent = r.currentState.getStateEvents(
|
const membershipEvent = r.currentState.getStateEvents(
|
||||||
"m.room.member",
|
"m.room.member",
|
||||||
myUserId
|
myUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
if (membershipEvent && !Array.isArray(membershipEvent)) {
|
||||||
@@ -82,7 +82,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
|||||||
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
|
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function updateRooms() {
|
function updateRooms(): void {
|
||||||
if (!client.groupCallEventHandler) {
|
if (!client.groupCallEventHandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
|||||||
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||||
client.removeListener(
|
client.removeListener(
|
||||||
GroupCallEventHandlerEvent.Participants,
|
GroupCallEventHandlerEvent.Participants,
|
||||||
updateRooms
|
updateRooms,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ limitations under the License.
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-Regular.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,8 @@ limitations under the License.
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-Italic.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,8 @@ limitations under the License.
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-Medium.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +101,8 @@ limitations under the License.
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-MediumItalic.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +112,8 @@ limitations under the License.
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-SemiBold.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +123,8 @@ limitations under the License.
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-SemiBoldItalic.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +134,8 @@ limitations under the License.
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-Bold.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +145,8 @@ limitations under the License.
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: var(--inter-unicode-range);
|
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");
|
url("/fonts/Inter/Inter-BoldItalic.woff") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ enum LoadState {
|
|||||||
class DependencyLoadStates {
|
class DependencyLoadStates {
|
||||||
// TODO: decide where olm should be initialized (see TODO comment below)
|
// TODO: decide where olm should be initialized (see TODO comment below)
|
||||||
// olm: LoadState = LoadState.None;
|
// olm: LoadState = LoadState.None;
|
||||||
config: LoadState = LoadState.None;
|
public config: LoadState = LoadState.None;
|
||||||
sentry: LoadState = LoadState.None;
|
public sentry: LoadState = LoadState.None;
|
||||||
openTelemetry: LoadState = LoadState.None;
|
public openTelemetry: LoadState = LoadState.None;
|
||||||
|
|
||||||
allDepsAreLoaded() {
|
public allDepsAreLoaded(): boolean {
|
||||||
return !Object.values(this).some((s) => s !== LoadState.Loaded);
|
return !Object.values(this).some((s) => s !== LoadState.Loaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export class Initializer {
|
|||||||
return Initializer.internalInstance?.isInitialized;
|
return Initializer.internalInstance?.isInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static initBeforeReact() {
|
public static initBeforeReact(): void {
|
||||||
// this maybe also needs to return a promise in the future,
|
// this maybe also needs to return a promise in the future,
|
||||||
// if we have to do async inits before showing the loading screen
|
// if we have to do async inits before showing the loading screen
|
||||||
// but this should be avioded if possible
|
// but this should be avioded if possible
|
||||||
@@ -99,13 +99,13 @@ export class Initializer {
|
|||||||
if (fontScale !== null) {
|
if (fontScale !== null) {
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--font-scale",
|
"--font-scale",
|
||||||
fontScale.toString()
|
fontScale.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (fonts.length > 0) {
|
if (fonts.length > 0) {
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--font-family",
|
"--font-family",
|
||||||
fonts.map((f) => `"${f}"`).join(", ")
|
fonts.map((f) => `"${f}"`).join(", "),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +126,9 @@ export class Initializer {
|
|||||||
return Initializer.internalInstance.initPromise;
|
return Initializer.internalInstance.initPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStates = new DependencyLoadStates();
|
private loadStates = new DependencyLoadStates();
|
||||||
|
|
||||||
initStep(resolve: (value: void | PromiseLike<void>) => void) {
|
private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
|
||||||
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
|
// TODO: Olm is initialized with the client currently (see `initClient()` and `olm.ts`)
|
||||||
// we need to decide if we want to init it here or keep it in initClient
|
// we need to decide if we want to init it here or keep it in initClient
|
||||||
// if (this.loadStates.olm === LoadState.None) {
|
// if (this.loadStates.olm === LoadState.None) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
onRemoveAvatar,
|
onRemoveAvatar,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentInput = fileInputRef.current;
|
const currentInput = fileInputRef.current;
|
||||||
|
|
||||||
const onChange = (e: Event) => {
|
const onChange = (e: Event): void => {
|
||||||
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
|
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
|
||||||
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
|
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
|
||||||
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
||||||
@@ -76,7 +76,7 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
|
|
||||||
currentInput.addEventListener("change", onChange);
|
currentInput.addEventListener("change", onChange);
|
||||||
|
|
||||||
return () => {
|
return (): void => {
|
||||||
currentInput?.removeEventListener("change", onChange);
|
currentInput?.removeEventListener("change", onChange);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -120,5 +120,5 @@ export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,8 +85,11 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inputField label {
|
.inputField label {
|
||||||
transition: font-size 0.25s ease-out 0.1s, color 0.25s ease-out 0.1s,
|
transition:
|
||||||
top 0.25s ease-out 0.1s, background-color 0.25s ease-out 0.1s;
|
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);
|
color: var(--cpd-color-text-secondary);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: var(--font-size-body);
|
font-size: var(--font-size-body);
|
||||||
@@ -118,8 +121,11 @@ limitations under the License.
|
|||||||
.inputField textarea:not(:placeholder-shown) + label,
|
.inputField textarea:not(:placeholder-shown) + label,
|
||||||
.inputField.prefix textarea + label {
|
.inputField.prefix textarea + label {
|
||||||
background-color: var(--cpd-color-bg-canvas-default);
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
transition: font-size 0.25s ease-out 0s, color 0.25s ease-out 0s,
|
transition:
|
||||||
top 0.25s ease-out 0s, background-color 0.25s ease-out 0s;
|
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);
|
font-size: var(--font-size-micro);
|
||||||
top: -13px;
|
top: -13px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function FieldRow({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
styles.fieldRow,
|
styles.fieldRow,
|
||||||
{ [styles.rightAlign]: rightAlign },
|
{ [styles.rightAlign]: rightAlign },
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -102,7 +102,7 @@ export const InputField = forwardRef<
|
|||||||
disabled,
|
disabled,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const descriptionId = useId();
|
const descriptionId = useId();
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export const InputField = forwardRef<
|
|||||||
[styles.prefix]: !!prefix,
|
[styles.prefix]: !!prefix,
|
||||||
[styles.disabled]: disabled,
|
[styles.disabled]: disabled,
|
||||||
},
|
},
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{prefix && <span>{prefix}</span>}
|
{prefix && <span>{prefix}</span>}
|
||||||
@@ -163,7 +163,7 @@ export const InputField = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ErrorMessageProps {
|
interface ErrorMessageProps {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function SelectInput(props: Props): JSX.Element {
|
|||||||
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
||||||
props,
|
props,
|
||||||
state,
|
state,
|
||||||
ref
|
ref,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { buttonProps } = useButton(triggerProps, ref);
|
const { buttonProps } = useButton(triggerProps, ref);
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export function StarRatingInput({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.inputContainer}
|
className={styles.inputContainer}
|
||||||
onMouseEnter={() => setHover(index)}
|
onMouseEnter={(): void => setHover(index)}
|
||||||
onMouseLeave={() => setHover(rating)}
|
onMouseLeave={(): void => setHover(rating)}
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -51,7 +51,7 @@ export function StarRatingInput({
|
|||||||
id={"starInput" + String(index)}
|
id={"starInput" + String(index)}
|
||||||
value={String(index) + "Star"}
|
value={String(index) + "Star"}
|
||||||
name="star rating"
|
name="star rating"
|
||||||
onChange={(_ev) => {
|
onChange={(_ev): void => {
|
||||||
setRating(index);
|
setRating(index);
|
||||||
onChange(index);
|
onChange(index);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export interface MediaDevices {
|
|||||||
// Cargo-culted from @livekit/components-react
|
// Cargo-culted from @livekit/components-react
|
||||||
function useObservableState<T>(
|
function useObservableState<T>(
|
||||||
observable: Observable<T> | undefined,
|
observable: Observable<T> | undefined,
|
||||||
startWith: T
|
startWith: T,
|
||||||
) {
|
): T {
|
||||||
const [state, setState] = useState<T>(startWith);
|
const [state, setState] = useState<T>(startWith);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// observable state doesn't run in SSR
|
// observable state doesn't run in SSR
|
||||||
@@ -67,7 +67,7 @@ function useMediaDevice(
|
|||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
fallbackDevice: string | undefined,
|
fallbackDevice: string | undefined,
|
||||||
usingNames: boolean,
|
usingNames: boolean,
|
||||||
alwaysDefault: boolean = false
|
alwaysDefault: boolean = false,
|
||||||
): MediaDevice {
|
): MediaDevice {
|
||||||
// Make sure we don't needlessly reset to a device observer without names,
|
// Make sure we don't needlessly reset to a device observer without names,
|
||||||
// once permissions are already given
|
// once permissions are already given
|
||||||
@@ -83,7 +83,7 @@ function useMediaDevice(
|
|||||||
// kind, which then results in multiple permissions requests.
|
// kind, which then results in multiple permissions requests.
|
||||||
const deviceObserver = useMemo(
|
const deviceObserver = useMemo(
|
||||||
() => createMediaDeviceObserver(kind, requestPermissions),
|
() => createMediaDeviceObserver(kind, requestPermissions),
|
||||||
[kind, requestPermissions]
|
[kind, requestPermissions],
|
||||||
);
|
);
|
||||||
const available = useObservableState(deviceObserver, []);
|
const available = useObservableState(deviceObserver, []);
|
||||||
const [selectedId, select] = useState(fallbackDevice);
|
const [selectedId, select] = useState(fallbackDevice);
|
||||||
@@ -143,18 +143,18 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
const audioInput = useMediaDevice(
|
const audioInput = useMediaDevice(
|
||||||
"audioinput",
|
"audioinput",
|
||||||
audioInputSetting,
|
audioInputSetting,
|
||||||
usingNames
|
usingNames,
|
||||||
);
|
);
|
||||||
const audioOutput = useMediaDevice(
|
const audioOutput = useMediaDevice(
|
||||||
"audiooutput",
|
"audiooutput",
|
||||||
audioOutputSetting,
|
audioOutputSetting,
|
||||||
useOutputNames,
|
useOutputNames,
|
||||||
alwaysUseDefaultAudio
|
alwaysUseDefaultAudio,
|
||||||
);
|
);
|
||||||
const videoInput = useMediaDevice(
|
const videoInput = useMediaDevice(
|
||||||
"videoinput",
|
"videoinput",
|
||||||
videoInputSetting,
|
videoInputSetting,
|
||||||
usingNames
|
usingNames,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -176,11 +176,11 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
|
|
||||||
const startUsingDeviceNames = useCallback(
|
const startUsingDeviceNames = useCallback(
|
||||||
() => setNumCallersUsingNames((n) => n + 1),
|
() => setNumCallersUsingNames((n) => n + 1),
|
||||||
[setNumCallersUsingNames]
|
[setNumCallersUsingNames],
|
||||||
);
|
);
|
||||||
const stopUsingDeviceNames = useCallback(
|
const stopUsingDeviceNames = useCallback(
|
||||||
() => setNumCallersUsingNames((n) => n - 1),
|
() => setNumCallersUsingNames((n) => n - 1),
|
||||||
[setNumCallersUsingNames]
|
[setNumCallersUsingNames],
|
||||||
);
|
);
|
||||||
|
|
||||||
const context: MediaDevices = useMemo(
|
const context: MediaDevices = useMemo(
|
||||||
@@ -197,7 +197,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
videoInput,
|
videoInput,
|
||||||
startUsingDeviceNames,
|
startUsingDeviceNames,
|
||||||
stopUsingDeviceNames,
|
stopUsingDeviceNames,
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -207,7 +207,8 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMediaDevices = () => useContext(MediaDevicesContext);
|
export const useMediaDevices = (): MediaDevices =>
|
||||||
|
useContext(MediaDevicesContext);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React hook that requests for the media devices context to be populated with
|
* React hook that requests for the media devices context to be populated with
|
||||||
@@ -215,7 +216,10 @@ export const useMediaDevices = () => useContext(MediaDevicesContext);
|
|||||||
* default because it may involve requesting additional permissions from the
|
* default because it may involve requesting additional permissions from the
|
||||||
* user.
|
* user.
|
||||||
*/
|
*/
|
||||||
export const useMediaDeviceNames = (context: MediaDevices, enabled = true) =>
|
export const useMediaDeviceNames = (
|
||||||
|
context: MediaDevices,
|
||||||
|
enabled = true,
|
||||||
|
): void =>
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
context.startUsingDeviceNames();
|
context.startUsingDeviceNames();
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ export type OpenIDClientParts = Pick<
|
|||||||
|
|
||||||
export function useOpenIDSFU(
|
export function useOpenIDSFU(
|
||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
rtcSession: MatrixRTCSession
|
rtcSession: MatrixRTCSession,
|
||||||
) {
|
): SFUConfig | undefined {
|
||||||
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
|
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
|
||||||
|
|
||||||
const activeFocus = useActiveFocus(rtcSession);
|
const activeFocus = useActiveFocus(rtcSession);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async (): Promise<void> => {
|
||||||
const sfuConfig = activeFocus
|
const sfuConfig = activeFocus
|
||||||
? await getSFUConfigWithOpenID(client, activeFocus)
|
? await getSFUConfigWithOpenID(client, activeFocus)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -62,20 +62,20 @@ export function useOpenIDSFU(
|
|||||||
|
|
||||||
export async function getSFUConfigWithOpenID(
|
export async function getSFUConfigWithOpenID(
|
||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
activeFocus: LivekitFocus
|
activeFocus: LivekitFocus,
|
||||||
): Promise<SFUConfig | undefined> {
|
): Promise<SFUConfig | undefined> {
|
||||||
const openIdToken = await client.getOpenIdToken();
|
const openIdToken = await client.getOpenIdToken();
|
||||||
logger.debug("Got openID token", openIdToken);
|
logger.debug("Got openID token", openIdToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(
|
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(
|
const sfuConfig = await getLiveKitJWT(
|
||||||
client,
|
client,
|
||||||
activeFocus.livekit_service_url,
|
activeFocus.livekit_service_url,
|
||||||
activeFocus.livekit_alias,
|
activeFocus.livekit_alias,
|
||||||
openIdToken
|
openIdToken,
|
||||||
);
|
);
|
||||||
logger.info(`Got JWT from call's active focus URL.`);
|
logger.info(`Got JWT from call's active focus URL.`);
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export async function getSFUConfigWithOpenID(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
|
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
|
||||||
e
|
e,
|
||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ async function getLiveKitJWT(
|
|||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
roomName: string,
|
roomName: string,
|
||||||
openIDToken: IOpenIDToken
|
openIDToken: IOpenIDToken,
|
||||||
): Promise<SFUConfig> {
|
): Promise<SFUConfig> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(livekitServiceURL + "/sfu/get", {
|
const res = await fetch(livekitServiceURL + "/sfu/get", {
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AudioPresets,
|
AudioPresets,
|
||||||
DefaultReconnectPolicy,
|
DefaultReconnectPolicy,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function doConnect(
|
|||||||
livekitRoom: Room,
|
livekitRoom: Room,
|
||||||
sfuConfig: SFUConfig,
|
sfuConfig: SFUConfig,
|
||||||
audioEnabled: boolean,
|
audioEnabled: boolean,
|
||||||
audioOptions: AudioCaptureOptions
|
audioOptions: AudioCaptureOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
|
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
|
||||||
|
|
||||||
@@ -76,12 +76,12 @@ export function useECConnectionState(
|
|||||||
initialAudioOptions: AudioCaptureOptions,
|
initialAudioOptions: AudioCaptureOptions,
|
||||||
initialAudioEnabled: boolean,
|
initialAudioEnabled: boolean,
|
||||||
livekitRoom?: Room,
|
livekitRoom?: Room,
|
||||||
sfuConfig?: SFUConfig
|
sfuConfig?: SFUConfig,
|
||||||
): ECConnectionState {
|
): ECConnectionState {
|
||||||
const [connState, setConnState] = useState(
|
const [connState, setConnState] = useState(
|
||||||
sfuConfig && livekitRoom
|
sfuConfig && livekitRoom
|
||||||
? livekitRoom.state
|
? livekitRoom.state
|
||||||
: ECAddonConnectionState.ECWaiting
|
: ECAddonConnectionState.ECWaiting,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
|
const [isSwitchingFocus, setSwitchingFocus] = useState(false);
|
||||||
@@ -116,10 +116,10 @@ export function useECConnectionState(
|
|||||||
!sfuConfigEquals(currentSFUConfig.current, sfuConfig)
|
!sfuConfigEquals(currentSFUConfig.current, sfuConfig)
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`
|
`SFU config changed! URL was ${currentSFUConfig.current?.url} now ${sfuConfig?.url}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
(async () => {
|
(async (): Promise<void> => {
|
||||||
setSwitchingFocus(true);
|
setSwitchingFocus(true);
|
||||||
await livekitRoom?.disconnect();
|
await livekitRoom?.disconnect();
|
||||||
setIsInDoConnect(true);
|
setIsInDoConnect(true);
|
||||||
@@ -128,7 +128,7 @@ export function useECConnectionState(
|
|||||||
livekitRoom!,
|
livekitRoom!,
|
||||||
sfuConfig!,
|
sfuConfig!,
|
||||||
initialAudioEnabled,
|
initialAudioEnabled,
|
||||||
initialAudioOptions
|
initialAudioOptions,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsInDoConnect(false);
|
setIsInDoConnect(false);
|
||||||
@@ -149,7 +149,7 @@ export function useECConnectionState(
|
|||||||
livekitRoom!,
|
livekitRoom!,
|
||||||
sfuConfig!,
|
sfuConfig!,
|
||||||
initialAudioEnabled,
|
initialAudioEnabled,
|
||||||
initialAudioOptions
|
initialAudioOptions,
|
||||||
).finally(() => setIsInDoConnect(false));
|
).finally(() => setIsInDoConnect(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ interface UseLivekitResult {
|
|||||||
export function useLiveKit(
|
export function useLiveKit(
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
e2eeConfig?: E2EEConfig
|
e2eeConfig?: E2EEConfig,
|
||||||
): UseLivekitResult {
|
): UseLivekitResult {
|
||||||
const e2eeOptions = useMemo(() => {
|
const e2eeOptions = useMemo(() => {
|
||||||
if (!e2eeConfig?.sharedKey) return undefined;
|
if (!e2eeConfig?.sharedKey) return undefined;
|
||||||
@@ -67,7 +67,7 @@ export function useLiveKit(
|
|||||||
if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
|
if (!e2eeConfig?.sharedKey || !e2eeOptions) return;
|
||||||
|
|
||||||
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
|
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
|
||||||
e2eeConfig?.sharedKey
|
e2eeConfig?.sharedKey,
|
||||||
);
|
);
|
||||||
}, [e2eeOptions, e2eeConfig?.sharedKey]);
|
}, [e2eeOptions, e2eeConfig?.sharedKey]);
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export function useLiveKit(
|
|||||||
},
|
},
|
||||||
e2ee: e2eeOptions,
|
e2ee: e2eeOptions,
|
||||||
}),
|
}),
|
||||||
[e2eeOptions]
|
[e2eeOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
// useECConnectionState creates and publishes an audio track by hand. To keep
|
// useECConnectionState creates and publishes an audio track by hand. To keep
|
||||||
@@ -131,7 +131,7 @@ export function useLiveKit(
|
|||||||
},
|
},
|
||||||
initialMuteStates.current.audio.enabled,
|
initialMuteStates.current.audio.enabled,
|
||||||
room,
|
room,
|
||||||
sfuConfig
|
sfuConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unblock audio once the connection is finished
|
// Unblock audio once the connection is finished
|
||||||
@@ -154,7 +154,7 @@ export function useLiveKit(
|
|||||||
audio: muteStates.audio.enabled,
|
audio: muteStates.audio.enabled,
|
||||||
video: muteStates.video.enabled,
|
video: muteStates.video.enabled,
|
||||||
};
|
};
|
||||||
const syncMuteStateAudio = async () => {
|
const syncMuteStateAudio = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
participant.isMicrophoneEnabled !== buttonEnabled.current.audio &&
|
participant.isMicrophoneEnabled !== buttonEnabled.current.audio &&
|
||||||
!audioMuteUpdating.current
|
!audioMuteUpdating.current
|
||||||
@@ -174,7 +174,7 @@ export function useLiveKit(
|
|||||||
syncMuteStateAudio();
|
syncMuteStateAudio();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const syncMuteStateVideo = async () => {
|
const syncMuteStateVideo = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
participant.isCameraEnabled !== buttonEnabled.current.video &&
|
participant.isCameraEnabled !== buttonEnabled.current.video &&
|
||||||
!videoMuteUpdating.current
|
!videoMuteUpdating.current
|
||||||
@@ -198,7 +198,7 @@ export function useLiveKit(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Sync the requested devices with LiveKit's devices
|
// Sync the requested devices with LiveKit's devices
|
||||||
if (room !== undefined && connectionState === ConnectionState.Connected) {
|
if (room !== undefined && connectionState === ConnectionState.Connected) {
|
||||||
const syncDevice = (kind: MediaDeviceKind, device: MediaDevice) => {
|
const syncDevice = (kind: MediaDeviceKind, device: MediaDevice): void => {
|
||||||
const id = device.selectedId;
|
const id = device.selectedId;
|
||||||
|
|
||||||
// Detect if we're trying to use chrome's default device, in which case
|
// Detect if we're trying to use chrome's default device, in which case
|
||||||
@@ -215,11 +215,11 @@ export function useLiveKit(
|
|||||||
room.options.audioCaptureDefaults?.deviceId === "default"
|
room.options.audioCaptureDefaults?.deviceId === "default"
|
||||||
) {
|
) {
|
||||||
const activeMicTrack = Array.from(
|
const activeMicTrack = Array.from(
|
||||||
room.localParticipant.audioTracks.values()
|
room.localParticipant.audioTracks.values(),
|
||||||
).find((d) => d.source === Track.Source.Microphone)?.track;
|
).find((d) => d.source === Track.Source.Microphone)?.track;
|
||||||
|
|
||||||
const defaultDevice = device.available.find(
|
const defaultDevice = device.available.find(
|
||||||
(d) => d.deviceId === "default"
|
(d) => d.deviceId === "default",
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
defaultDevice &&
|
defaultDevice &&
|
||||||
@@ -245,7 +245,7 @@ export function useLiveKit(
|
|||||||
room
|
room
|
||||||
.switchActiveDevice(kind, id)
|
.switchActiveDevice(kind, id)
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
logger.error(`Failed to sync ${kind} device with LiveKit`, e)
|
logger.error(`Failed to sync ${kind} device with LiveKit`, e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
setLogLevel,
|
setLogLevel,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
|
|
||||||
import App from "./App";
|
import { App } from "./App";
|
||||||
import { init as initRageshake } from "./settings/rageshake";
|
import { init as initRageshake } from "./settings/rageshake";
|
||||||
import { Initializer } from "./initializer";
|
import { Initializer } from "./initializer";
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ if (!window.isSecureContext) {
|
|||||||
fatalError = new Error(
|
fatalError = new Error(
|
||||||
"This app cannot run in an insecure context. To fix this, access the app " +
|
"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" +
|
"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) {
|
} else if (!navigator.mediaDevices) {
|
||||||
fatalError = new Error("Your browser does not support WebRTC.");
|
fatalError = new Error("Your browser does not support WebRTC.");
|
||||||
@@ -66,5 +66,5 @@ const history = createBrowserHistory();
|
|||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App history={history} />
|
<App history={history} />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const fallbackICEServerAllowed =
|
|||||||
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
|
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
|
||||||
|
|
||||||
export class CryptoStoreIntegrityError extends Error {
|
export class CryptoStoreIntegrityError extends Error {
|
||||||
constructor() {
|
public constructor() {
|
||||||
super("Crypto store data was expected, but none was found");
|
super("Crypto store data was expected, but none was found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,13 +54,13 @@ const SYNC_STORE_NAME = "element-call-sync";
|
|||||||
// (It's a good opportunity to make the database names consistent.)
|
// (It's a good opportunity to make the database names consistent.)
|
||||||
const CRYPTO_STORE_NAME = "element-call-crypto";
|
const CRYPTO_STORE_NAME = "element-call-crypto";
|
||||||
|
|
||||||
function waitForSync(client: MatrixClient) {
|
function waitForSync(client: MatrixClient): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const onSync = (
|
const onSync = (
|
||||||
state: SyncState,
|
state: SyncState,
|
||||||
_old: SyncState | null,
|
_old: SyncState | null,
|
||||||
data?: ISyncStateData
|
data?: ISyncStateData,
|
||||||
) => {
|
): void => {
|
||||||
if (state === "PREPARED") {
|
if (state === "PREPARED") {
|
||||||
client.removeListener(ClientEvent.Sync, onSync);
|
client.removeListener(ClientEvent.Sync, onSync);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -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
|
// 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).
|
// padding from the end (otherwise we'd need to pull in another dependency).
|
||||||
return btoa(
|
return btoa(
|
||||||
key.reduce((acc, current) => acc + String.fromCharCode(current), "")
|
key.reduce((acc, current) => acc + String.fromCharCode(current), ""),
|
||||||
)
|
)
|
||||||
.replace("+", "-")
|
.replace("+", "-")
|
||||||
.replace("/", "_")
|
.replace("/", "_")
|
||||||
@@ -101,7 +101,7 @@ function secureRandomString(entropyBytes: number): string {
|
|||||||
*/
|
*/
|
||||||
export async function initClient(
|
export async function initClient(
|
||||||
clientOptions: ICreateClientOpts,
|
clientOptions: ICreateClientOpts,
|
||||||
restore: boolean
|
restore: boolean,
|
||||||
): Promise<MatrixClient> {
|
): Promise<MatrixClient> {
|
||||||
await loadOlm();
|
await loadOlm();
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ export async function initClient(
|
|||||||
// Chrome supports it. (It bundles them fine in production mode.)
|
// Chrome supports it. (It bundles them fine in production mode.)
|
||||||
workerFactory: import.meta.env.DEV
|
workerFactory: import.meta.env.DEV
|
||||||
? undefined
|
? undefined
|
||||||
: () => new IndexedDBWorker(),
|
: (): Worker => new IndexedDBWorker(),
|
||||||
});
|
});
|
||||||
} else if (localStorage) {
|
} else if (localStorage) {
|
||||||
baseOpts.store = new MemoryStore({ localStorage });
|
baseOpts.store = new MemoryStore({ localStorage });
|
||||||
@@ -148,7 +148,7 @@ export async function initClient(
|
|||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
const cryptoStoreExists = await IndexedDBCryptoStore.exists(
|
const cryptoStoreExists = await IndexedDBCryptoStore.exists(
|
||||||
indexedDB,
|
indexedDB,
|
||||||
CRYPTO_STORE_NAME
|
CRYPTO_STORE_NAME,
|
||||||
);
|
);
|
||||||
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
|
if (!cryptoStoreExists) throw new CryptoStoreIntegrityError();
|
||||||
} else if (localStorage) {
|
} else if (localStorage) {
|
||||||
@@ -164,7 +164,7 @@ export async function initClient(
|
|||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
baseOpts.cryptoStore = new IndexedDBCryptoStore(
|
baseOpts.cryptoStore = new IndexedDBCryptoStore(
|
||||||
indexedDB,
|
indexedDB,
|
||||||
CRYPTO_STORE_NAME
|
CRYPTO_STORE_NAME,
|
||||||
);
|
);
|
||||||
} else if (localStorage) {
|
} else if (localStorage) {
|
||||||
baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
|
baseOpts.cryptoStore = new LocalStorageCryptoStore(localStorage);
|
||||||
@@ -198,7 +198,7 @@ export async function initClient(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Error starting matrix client store. Falling back to memory store.",
|
"Error starting matrix client store. Falling back to memory store.",
|
||||||
error
|
error,
|
||||||
);
|
);
|
||||||
client.store = new MemoryStore({ localStorage });
|
client.store = new MemoryStore({ localStorage });
|
||||||
await client.store.startup();
|
await client.store.startup();
|
||||||
@@ -268,7 +268,7 @@ export function roomNameFromRoomId(roomId: string): string {
|
|||||||
.substring(1)
|
.substring(1)
|
||||||
.split("-")
|
.split("-")
|
||||||
.map((part) =>
|
.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(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
@@ -297,7 +297,7 @@ interface CreateRoomResult {
|
|||||||
export async function createRoom(
|
export async function createRoom(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
name: string,
|
name: string,
|
||||||
e2ee: boolean
|
e2ee: boolean,
|
||||||
): Promise<CreateRoomResult> {
|
): Promise<CreateRoomResult> {
|
||||||
logger.log(`Creating room for group call`);
|
logger.log(`Creating room for group call`);
|
||||||
const createPromise = client.createRoom({
|
const createPromise = client.createRoom({
|
||||||
@@ -332,7 +332,7 @@ export async function createRoom(
|
|||||||
|
|
||||||
// Wait for the room to arrive
|
// Wait for the room to arrive
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const onRoom = async (room: Room) => {
|
const onRoom = async (room: Room): Promise<void> => {
|
||||||
if (room.roomId === (await createPromise).room_id) {
|
if (room.roomId === (await createPromise).room_id) {
|
||||||
resolve();
|
resolve();
|
||||||
cleanUp();
|
cleanUp();
|
||||||
@@ -343,7 +343,7 @@ export async function createRoom(
|
|||||||
cleanUp();
|
cleanUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanUp = () => {
|
const cleanUp = (): void => {
|
||||||
client.off(ClientEvent.Room, onRoom);
|
client.off(ClientEvent.Room, onRoom);
|
||||||
};
|
};
|
||||||
client.on(ClientEvent.Room, onRoom);
|
client.on(ClientEvent.Room, onRoom);
|
||||||
@@ -358,7 +358,7 @@ export async function createRoom(
|
|||||||
GroupCallType.Video,
|
GroupCallType.Video,
|
||||||
false,
|
false,
|
||||||
GroupCallIntent.Room,
|
GroupCallIntent.Room,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let password;
|
let password;
|
||||||
@@ -366,7 +366,7 @@ export async function createRoom(
|
|||||||
password = secureRandomString(16);
|
password = secureRandomString(16);
|
||||||
setLocalStorageItem(
|
setLocalStorageItem(
|
||||||
getRoomSharedKeyLocalStorageKey(result.room_id),
|
getRoomSharedKeyLocalStorageKey(result.room_id),
|
||||||
password
|
password,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ export async function createRoom(
|
|||||||
export function getAbsoluteRoomUrl(
|
export function getAbsoluteRoomUrl(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
roomName?: string,
|
roomName?: string,
|
||||||
password?: string
|
password?: string,
|
||||||
): string {
|
): string {
|
||||||
return `${window.location.protocol}//${
|
return `${window.location.protocol}//${
|
||||||
window.location.host
|
window.location.host
|
||||||
@@ -402,7 +402,7 @@ export function getAbsoluteRoomUrl(
|
|||||||
export function getRelativeRoomUrl(
|
export function getRelativeRoomUrl(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
roomName?: string,
|
roomName?: string,
|
||||||
password?: string
|
password?: string,
|
||||||
): string {
|
): string {
|
||||||
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
|
// 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
|
// 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(
|
export function getAvatarUrl(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
mxcUrl: string,
|
mxcUrl: string,
|
||||||
avatarSize = 96
|
avatarSize = 96,
|
||||||
): string {
|
): string {
|
||||||
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
const width = Math.floor(avatarSize * window.devicePixelRatio);
|
||||||
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
const height = Math.floor(avatarSize * window.devicePixelRatio);
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ limitations under the License.
|
|||||||
export async function findDeviceByName(
|
export async function findDeviceByName(
|
||||||
deviceName: string,
|
deviceName: string,
|
||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
devices: MediaDeviceInfo[]
|
devices: MediaDeviceInfo[],
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const deviceInfo = devices.find(
|
const deviceInfo = devices.find(
|
||||||
(d) => d.kind === kind && d.label === deviceName
|
(d) => d.kind === kind && d.label === deviceName,
|
||||||
);
|
);
|
||||||
return deviceInfo?.deviceId;
|
return deviceInfo?.deviceId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,65 +44,65 @@ export class OTelCall {
|
|||||||
OTelCallAbstractMediaStreamSpan
|
OTelCallAbstractMediaStreamSpan
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(
|
public constructor(
|
||||||
public userId: string,
|
public userId: string,
|
||||||
public deviceId: string,
|
public deviceId: string,
|
||||||
public call: MatrixCall,
|
public call: MatrixCall,
|
||||||
public span: Span
|
public span: Span,
|
||||||
) {
|
) {
|
||||||
if (call.peerConn) {
|
if (call.peerConn) {
|
||||||
this.addCallPeerConnListeners();
|
this.addCallPeerConnListeners();
|
||||||
} else {
|
} else {
|
||||||
this.call.once(
|
this.call.once(
|
||||||
CallEvent.PeerConnectionCreated,
|
CallEvent.PeerConnectionCreated,
|
||||||
this.addCallPeerConnListeners
|
this.addCallPeerConnListeners,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose(): void {
|
||||||
this.call.peerConn?.removeEventListener(
|
this.call.peerConn?.removeEventListener(
|
||||||
"connectionstatechange",
|
"connectionstatechange",
|
||||||
this.onCallConnectionStateChanged
|
this.onCallConnectionStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.removeEventListener(
|
this.call.peerConn?.removeEventListener(
|
||||||
"signalingstatechange",
|
"signalingstatechange",
|
||||||
this.onCallSignalingStateChanged
|
this.onCallSignalingStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.removeEventListener(
|
this.call.peerConn?.removeEventListener(
|
||||||
"iceconnectionstatechange",
|
"iceconnectionstatechange",
|
||||||
this.onIceConnectionStateChanged
|
this.onIceConnectionStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.removeEventListener(
|
this.call.peerConn?.removeEventListener(
|
||||||
"icegatheringstatechange",
|
"icegatheringstatechange",
|
||||||
this.onIceGatheringStateChanged
|
this.onIceGatheringStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.removeEventListener(
|
this.call.peerConn?.removeEventListener(
|
||||||
"icecandidateerror",
|
"icecandidateerror",
|
||||||
this.onIceCandidateError
|
this.onIceCandidateError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCallPeerConnListeners = (): void => {
|
private addCallPeerConnListeners = (): void => {
|
||||||
this.call.peerConn?.addEventListener(
|
this.call.peerConn?.addEventListener(
|
||||||
"connectionstatechange",
|
"connectionstatechange",
|
||||||
this.onCallConnectionStateChanged
|
this.onCallConnectionStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.addEventListener(
|
this.call.peerConn?.addEventListener(
|
||||||
"signalingstatechange",
|
"signalingstatechange",
|
||||||
this.onCallSignalingStateChanged
|
this.onCallSignalingStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.addEventListener(
|
this.call.peerConn?.addEventListener(
|
||||||
"iceconnectionstatechange",
|
"iceconnectionstatechange",
|
||||||
this.onIceConnectionStateChanged
|
this.onIceConnectionStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.addEventListener(
|
this.call.peerConn?.addEventListener(
|
||||||
"icegatheringstatechange",
|
"icegatheringstatechange",
|
||||||
this.onIceGatheringStateChanged
|
this.onIceGatheringStateChanged,
|
||||||
);
|
);
|
||||||
this.call.peerConn?.addEventListener(
|
this.call.peerConn?.addEventListener(
|
||||||
"icecandidateerror",
|
"icecandidateerror",
|
||||||
this.onIceCandidateError
|
this.onIceCandidateError,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,8 +147,8 @@ export class OTelCall {
|
|||||||
new OTelCallFeedMediaStreamSpan(
|
new OTelCallFeedMediaStreamSpan(
|
||||||
ElementCallOpenTelemetry.instance,
|
ElementCallOpenTelemetry.instance,
|
||||||
this.span,
|
this.span,
|
||||||
feed
|
feed,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.trackFeedSpan.get(feed.stream)?.update(feed);
|
this.trackFeedSpan.get(feed.stream)?.update(feed);
|
||||||
@@ -171,13 +171,13 @@ export class OTelCall {
|
|||||||
new OTelCallTransceiverMediaStreamSpan(
|
new OTelCallTransceiverMediaStreamSpan(
|
||||||
ElementCallOpenTelemetry.instance,
|
ElementCallOpenTelemetry.instance,
|
||||||
this.span,
|
this.span,
|
||||||
transStats
|
transStats,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
|
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
|
||||||
prvTransSpan = prvTransSpan.filter(
|
prvTransSpan = prvTransSpan.filter(
|
||||||
(prvStreamId) => prvStreamId !== transStats.mid
|
(prvStreamId) => prvStreamId !== transStats.mid,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ export class OTelCall {
|
|||||||
public end(): void {
|
public end(): void {
|
||||||
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
|
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
|
||||||
this.trackTransceiverSpan.forEach((transceiverSpan) =>
|
this.trackTransceiverSpan.forEach((transceiverSpan) =>
|
||||||
transceiverSpan.end()
|
transceiverSpan.end(),
|
||||||
);
|
);
|
||||||
this.span.end();
|
this.span.end();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import opentelemetry, { Span } from "@opentelemetry/api";
|
import opentelemetry, { Span } from "@opentelemetry/api";
|
||||||
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||||
|
|
||||||
@@ -14,13 +30,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
|
|||||||
public readonly span;
|
public readonly span;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
readonly oTel: ElementCallOpenTelemetry,
|
protected readonly oTel: ElementCallOpenTelemetry,
|
||||||
readonly callSpan: Span,
|
protected readonly callSpan: Span,
|
||||||
protected readonly type: string
|
protected readonly type: string,
|
||||||
) {
|
) {
|
||||||
const ctx = opentelemetry.trace.setSpan(
|
const ctx = opentelemetry.trace.setSpan(
|
||||||
opentelemetry.context.active(),
|
opentelemetry.context.active(),
|
||||||
callSpan
|
callSpan,
|
||||||
);
|
);
|
||||||
const options = {
|
const options = {
|
||||||
links: [
|
links: [
|
||||||
@@ -32,13 +48,13 @@ export abstract class OTelCallAbstractMediaStreamSpan {
|
|||||||
this.span = oTel.tracer.startSpan(this.type, options, ctx);
|
this.span = oTel.tracer.startSpan(this.type, options, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected upsertTrackSpans(tracks: TrackStats[]) {
|
protected upsertTrackSpans(tracks: TrackStats[]): void {
|
||||||
let prvTracks: TrackId[] = [...this.trackSpans.keys()];
|
let prvTracks: TrackId[] = [...this.trackSpans.keys()];
|
||||||
tracks.forEach((t) => {
|
tracks.forEach((t) => {
|
||||||
if (!this.trackSpans.has(t.id)) {
|
if (!this.trackSpans.has(t.id)) {
|
||||||
this.trackSpans.set(
|
this.trackSpans.set(
|
||||||
t.id,
|
t.id,
|
||||||
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t)
|
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.trackSpans.get(t.id)?.update(t);
|
this.trackSpans.get(t.id)?.update(t);
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Span } from "@opentelemetry/api";
|
import { Span } from "@opentelemetry/api";
|
||||||
import {
|
import {
|
||||||
CallFeedStats,
|
CallFeedStats,
|
||||||
@@ -10,10 +26,10 @@ import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSp
|
|||||||
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
|
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
|
||||||
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
|
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
|
||||||
|
|
||||||
constructor(
|
public constructor(
|
||||||
readonly oTel: ElementCallOpenTelemetry,
|
protected readonly oTel: ElementCallOpenTelemetry,
|
||||||
readonly callSpan: Span,
|
protected readonly callSpan: Span,
|
||||||
callFeed: CallFeedStats
|
callFeed: CallFeedStats,
|
||||||
) {
|
) {
|
||||||
const postFix =
|
const postFix =
|
||||||
callFeed.type === "local" && callFeed.prefix === "from-call-feed"
|
callFeed.type === "local" && callFeed.prefix === "from-call-feed"
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
import { TrackStats } from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||||
import opentelemetry, { Span } from "@opentelemetry/api";
|
import opentelemetry, { Span } from "@opentelemetry/api";
|
||||||
|
|
||||||
@@ -8,13 +24,13 @@ export class OTelCallMediaStreamTrackSpan {
|
|||||||
private prev: TrackStats;
|
private prev: TrackStats;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
readonly oTel: ElementCallOpenTelemetry,
|
protected readonly oTel: ElementCallOpenTelemetry,
|
||||||
readonly streamSpan: Span,
|
protected readonly streamSpan: Span,
|
||||||
data: TrackStats
|
data: TrackStats,
|
||||||
) {
|
) {
|
||||||
const ctx = opentelemetry.trace.setSpan(
|
const ctx = opentelemetry.trace.setSpan(
|
||||||
opentelemetry.context.active(),
|
opentelemetry.context.active(),
|
||||||
streamSpan
|
streamSpan,
|
||||||
);
|
);
|
||||||
const options = {
|
const options = {
|
||||||
links: [
|
links: [
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Span } from "@opentelemetry/api";
|
import { Span } from "@opentelemetry/api";
|
||||||
import {
|
import {
|
||||||
TrackStats,
|
TrackStats,
|
||||||
@@ -13,10 +29,10 @@ export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStr
|
|||||||
currentDirection: string;
|
currentDirection: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
public constructor(
|
||||||
readonly oTel: ElementCallOpenTelemetry,
|
protected readonly oTel: ElementCallOpenTelemetry,
|
||||||
readonly callSpan: Span,
|
protected readonly callSpan: Span,
|
||||||
stats: TransceiverStats
|
stats: TransceiverStats,
|
||||||
) {
|
) {
|
||||||
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
|
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
|
||||||
this.span.setAttribute("transceiver.mid", stats.mid);
|
this.span.setAttribute("transceiver.mid", stats.mid);
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ export class OTelGroupCallMembership {
|
|||||||
};
|
};
|
||||||
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
|
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
|
||||||
|
|
||||||
constructor(private groupCall: GroupCall, client: MatrixClient) {
|
public constructor(
|
||||||
|
private groupCall: GroupCall,
|
||||||
|
client: MatrixClient,
|
||||||
|
) {
|
||||||
const clientId = client.getUserId();
|
const clientId = client.getUserId();
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
this.myUserId = clientId;
|
this.myUserId = clientId;
|
||||||
@@ -76,14 +79,14 @@ export class OTelGroupCallMembership {
|
|||||||
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
|
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
public dispose(): void {
|
||||||
this.groupCall.removeListener(
|
this.groupCall.removeListener(
|
||||||
GroupCallEvent.CallsChanged,
|
GroupCallEvent.CallsChanged,
|
||||||
this.onCallsChanged
|
this.onCallsChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onJoinCall() {
|
public onJoinCall(): void {
|
||||||
if (!ElementCallOpenTelemetry.instance) return;
|
if (!ElementCallOpenTelemetry.instance) return;
|
||||||
if (this.callMembershipSpan !== undefined) {
|
if (this.callMembershipSpan !== undefined) {
|
||||||
logger.warn("Call membership span is already started");
|
logger.warn("Call membership span is already started");
|
||||||
@@ -93,28 +96,28 @@ export class OTelGroupCallMembership {
|
|||||||
// Create the main span that tracks the time we intend to be in the call
|
// Create the main span that tracks the time we intend to be in the call
|
||||||
this.callMembershipSpan =
|
this.callMembershipSpan =
|
||||||
ElementCallOpenTelemetry.instance.tracer.startSpan(
|
ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||||
"matrix.groupCallMembership"
|
"matrix.groupCallMembership",
|
||||||
);
|
);
|
||||||
this.callMembershipSpan.setAttribute(
|
this.callMembershipSpan.setAttribute(
|
||||||
"matrix.confId",
|
"matrix.confId",
|
||||||
this.groupCall.groupCallId
|
this.groupCall.groupCallId,
|
||||||
);
|
);
|
||||||
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
|
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
|
||||||
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
|
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
|
||||||
this.callMembershipSpan.setAttribute(
|
this.callMembershipSpan.setAttribute(
|
||||||
"matrix.displayName",
|
"matrix.displayName",
|
||||||
this.myMember ? this.myMember.name : "unknown-name"
|
this.myMember ? this.myMember.name : "unknown-name",
|
||||||
);
|
);
|
||||||
|
|
||||||
this.groupCallContext = opentelemetry.trace.setSpan(
|
this.groupCallContext = opentelemetry.trace.setSpan(
|
||||||
opentelemetry.context.active(),
|
opentelemetry.context.active(),
|
||||||
this.callMembershipSpan
|
this.callMembershipSpan,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.callMembershipSpan?.addEvent("matrix.joinCall");
|
this.callMembershipSpan?.addEvent("matrix.joinCall");
|
||||||
}
|
}
|
||||||
|
|
||||||
public onLeaveCall() {
|
public onLeaveCall(): void {
|
||||||
if (this.callMembershipSpan === undefined) {
|
if (this.callMembershipSpan === undefined) {
|
||||||
logger.warn("Call membership span is already ended");
|
logger.warn("Call membership span is already ended");
|
||||||
return;
|
return;
|
||||||
@@ -127,7 +130,7 @@ export class OTelGroupCallMembership {
|
|||||||
this.groupCallContext = undefined;
|
this.groupCallContext = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateRoomState(event: MatrixEvent) {
|
public onUpdateRoomState(event: MatrixEvent): void {
|
||||||
if (
|
if (
|
||||||
!event ||
|
!event ||
|
||||||
(!event.getType().startsWith("m.call") &&
|
(!event.getType().startsWith("m.call") &&
|
||||||
@@ -138,11 +141,11 @@ export class OTelGroupCallMembership {
|
|||||||
|
|
||||||
this.callMembershipSpan?.addEvent(
|
this.callMembershipSpan?.addEvent(
|
||||||
`matrix.roomStateEvent_${event.getType()}`,
|
`matrix.roomStateEvent_${event.getType()}`,
|
||||||
ObjectFlattener.flattenVoipEvent(event.getContent())
|
ObjectFlattener.flattenVoipEvent(event.getContent()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCallsChanged = (calls: CallsByUserAndDevice) => {
|
public onCallsChanged(calls: CallsByUserAndDevice): void {
|
||||||
for (const [userId, userCalls] of calls.entries()) {
|
for (const [userId, userCalls] of calls.entries()) {
|
||||||
for (const [deviceId, call] of userCalls.entries()) {
|
for (const [deviceId, call] of userCalls.entries()) {
|
||||||
if (!this.callsByCallId.has(call.callId)) {
|
if (!this.callsByCallId.has(call.callId)) {
|
||||||
@@ -150,7 +153,7 @@ export class OTelGroupCallMembership {
|
|||||||
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||||
`matrix.call`,
|
`matrix.call`,
|
||||||
undefined,
|
undefined,
|
||||||
this.groupCallContext
|
this.groupCallContext,
|
||||||
);
|
);
|
||||||
// XXX: anonymity
|
// XXX: anonymity
|
||||||
span.setAttribute("matrix.call.target.userId", userId);
|
span.setAttribute("matrix.call.target.userId", userId);
|
||||||
@@ -160,7 +163,7 @@ export class OTelGroupCallMembership {
|
|||||||
span.setAttribute("matrix.call.target.displayName", displayName);
|
span.setAttribute("matrix.call.target.displayName", displayName);
|
||||||
this.callsByCallId.set(
|
this.callsByCallId.set(
|
||||||
call.callId,
|
call.callId,
|
||||||
new OTelCall(userId, deviceId, call, span)
|
new OTelCall(userId, deviceId, call, span),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,9 +182,9 @@ export class OTelGroupCallMembership {
|
|||||||
this.callsByCallId.delete(callTrackingInfo.call.callId);
|
this.callsByCallId.delete(callTrackingInfo.call.callId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
public onCallStateChange(call: MatrixCall, newState: CallState) {
|
public onCallStateChange(call: MatrixCall, newState: CallState): void {
|
||||||
const callTrackingInfo = this.callsByCallId.get(call.callId);
|
const callTrackingInfo = this.callsByCallId.get(call.callId);
|
||||||
if (!callTrackingInfo) {
|
if (!callTrackingInfo) {
|
||||||
logger.error(`Got call state change for unknown call ID ${call.callId}`);
|
logger.error(`Got call state change for unknown call ID ${call.callId}`);
|
||||||
@@ -193,7 +196,7 @@ export class OTelGroupCallMembership {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSendEvent(call: MatrixCall, event: VoipEvent) {
|
public onSendEvent(call: MatrixCall, event: VoipEvent): void {
|
||||||
const eventType = event.eventType as string;
|
const eventType = event.eventType as string;
|
||||||
if (
|
if (
|
||||||
!eventType.startsWith("m.call") &&
|
!eventType.startsWith("m.call") &&
|
||||||
@@ -210,17 +213,17 @@ export class OTelGroupCallMembership {
|
|||||||
if (event.type === "toDevice") {
|
if (event.type === "toDevice") {
|
||||||
callTrackingInfo.span.addEvent(
|
callTrackingInfo.span.addEvent(
|
||||||
`matrix.sendToDeviceEvent_${event.eventType}`,
|
`matrix.sendToDeviceEvent_${event.eventType}`,
|
||||||
ObjectFlattener.flattenVoipEvent(event)
|
ObjectFlattener.flattenVoipEvent(event),
|
||||||
);
|
);
|
||||||
} else if (event.type === "sendEvent") {
|
} else if (event.type === "sendEvent") {
|
||||||
callTrackingInfo.span.addEvent(
|
callTrackingInfo.span.addEvent(
|
||||||
`matrix.sendToRoomEvent_${event.eventType}`,
|
`matrix.sendToRoomEvent_${event.eventType}`,
|
||||||
ObjectFlattener.flattenVoipEvent(event)
|
ObjectFlattener.flattenVoipEvent(event),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onReceivedVoipEvent(event: MatrixEvent) {
|
public onReceivedVoipEvent(event: MatrixEvent): void {
|
||||||
// These come straight from CallEventHandler so don't have
|
// These come straight from CallEventHandler so don't have
|
||||||
// a call already associated (in principle we could receive
|
// a call already associated (in principle we could receive
|
||||||
// events for calls we don't know about).
|
// events for calls we don't know about).
|
||||||
@@ -239,7 +242,7 @@ export class OTelGroupCallMembership {
|
|||||||
"matrix.receive_voip_event_unknown_callid",
|
"matrix.receive_voip_event_unknown_callid",
|
||||||
{
|
{
|
||||||
"sender.userId": event.getSender(),
|
"sender.userId": event.getSender(),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
logger.error("Received call event for unknown call ID " + callId);
|
logger.error("Received call event for unknown call ID " + callId);
|
||||||
return;
|
return;
|
||||||
@@ -251,37 +254,41 @@ export class OTelGroupCallMembership {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onToggleMicrophoneMuted(newValue: boolean) {
|
public onToggleMicrophoneMuted(newValue: boolean): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
|
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
|
||||||
"matrix.microphone.muted": newValue,
|
"matrix.microphone.muted": newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSetMicrophoneMuted(setMuted: boolean) {
|
public onSetMicrophoneMuted(setMuted: boolean): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
|
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
|
||||||
"matrix.microphone.muted": setMuted,
|
"matrix.microphone.muted": setMuted,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onToggleLocalVideoMuted(newValue: boolean) {
|
public onToggleLocalVideoMuted(newValue: boolean): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
|
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
|
||||||
"matrix.video.muted": newValue,
|
"matrix.video.muted": newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSetLocalVideoMuted(setMuted: boolean) {
|
public onSetLocalVideoMuted(setMuted: boolean): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
|
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
|
||||||
"matrix.video.muted": setMuted,
|
"matrix.video.muted": setMuted,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onToggleScreensharing(newValue: boolean) {
|
public onToggleScreensharing(newValue: boolean): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
|
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
|
||||||
"matrix.screensharing.enabled": newValue,
|
"matrix.screensharing.enabled": newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) {
|
public onSpeaking(
|
||||||
|
member: RoomMember,
|
||||||
|
deviceId: string,
|
||||||
|
speaking: boolean,
|
||||||
|
): void {
|
||||||
if (speaking) {
|
if (speaking) {
|
||||||
// Ensure that there's an audio activity span for this speaker
|
// Ensure that there's an audio activity span for this speaker
|
||||||
let deviceMap = this.speakingSpans.get(member);
|
let deviceMap = this.speakingSpans.get(member);
|
||||||
@@ -294,7 +301,7 @@ export class OTelGroupCallMembership {
|
|||||||
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||||
"matrix.audioActivity",
|
"matrix.audioActivity",
|
||||||
undefined,
|
undefined,
|
||||||
this.groupCallContext
|
this.groupCallContext,
|
||||||
);
|
);
|
||||||
span.setAttribute("matrix.userId", member.userId);
|
span.setAttribute("matrix.userId", member.userId);
|
||||||
span.setAttribute("matrix.displayName", member.rawDisplayName);
|
span.setAttribute("matrix.displayName", member.rawDisplayName);
|
||||||
@@ -311,7 +318,7 @@ export class OTelGroupCallMembership {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCallError(error: CallError, call: MatrixCall) {
|
public onCallError(error: CallError, call: MatrixCall): void {
|
||||||
const callTrackingInfo = this.callsByCallId.get(call.callId);
|
const callTrackingInfo = this.callsByCallId.get(call.callId);
|
||||||
if (!callTrackingInfo) {
|
if (!callTrackingInfo) {
|
||||||
logger.error(`Got error for unknown call ID ${call.callId}`);
|
logger.error(`Got error for unknown call ID ${call.callId}`);
|
||||||
@@ -321,17 +328,19 @@ export class OTelGroupCallMembership {
|
|||||||
callTrackingInfo.span.recordException(error);
|
callTrackingInfo.span.recordException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGroupCallError(error: GroupCallError) {
|
public onGroupCallError(error: GroupCallError): void {
|
||||||
this.callMembershipSpan?.recordException(error);
|
this.callMembershipSpan?.recordException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUndecryptableToDevice(event: MatrixEvent) {
|
public onUndecryptableToDevice(event: MatrixEvent): void {
|
||||||
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
|
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
|
||||||
"sender.userId": event.getSender(),
|
"sender.userId": event.getSender(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCallFeedStatsReport(report: GroupCallStatsReport<CallFeedReport>) {
|
public onCallFeedStatsReport(
|
||||||
|
report: GroupCallStatsReport<CallFeedReport>,
|
||||||
|
): void {
|
||||||
if (!ElementCallOpenTelemetry.instance) return;
|
if (!ElementCallOpenTelemetry.instance) return;
|
||||||
let call: OTelCall | undefined;
|
let call: OTelCall | undefined;
|
||||||
const callId = report.report?.callId;
|
const callId = report.report?.callId;
|
||||||
@@ -348,10 +357,10 @@ export class OTelGroupCallMembership {
|
|||||||
"call.opponentMemberId": report.report?.opponentMemberId
|
"call.opponentMemberId": report.report?.opponentMemberId
|
||||||
? report.report?.opponentMemberId
|
? report.report?.opponentMemberId
|
||||||
: "unknown",
|
: "unknown",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
logger.error(
|
logger.error(
|
||||||
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`
|
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -361,26 +370,26 @@ export class OTelGroupCallMembership {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onConnectionStatsReport(
|
public onConnectionStatsReport(
|
||||||
statsReport: GroupCallStatsReport<ConnectionStatsReport>
|
statsReport: GroupCallStatsReport<ConnectionStatsReport>,
|
||||||
) {
|
): void {
|
||||||
this.buildCallStatsSpan(
|
this.buildCallStatsSpan(
|
||||||
OTelStatsReportType.ConnectionReport,
|
OTelStatsReportType.ConnectionReport,
|
||||||
statsReport.report
|
statsReport.report,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onByteSentStatsReport(
|
public onByteSentStatsReport(
|
||||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
|
||||||
) {
|
): void {
|
||||||
this.buildCallStatsSpan(
|
this.buildCallStatsSpan(
|
||||||
OTelStatsReportType.ByteSentReport,
|
OTelStatsReportType.ByteSentReport,
|
||||||
statsReport.report
|
statsReport.report,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildCallStatsSpan(
|
public buildCallStatsSpan(
|
||||||
type: OTelStatsReportType,
|
type: OTelStatsReportType,
|
||||||
report: ByteSentStatsReport | ConnectionStatsReport
|
report: ByteSentStatsReport | ConnectionStatsReport,
|
||||||
): void {
|
): void {
|
||||||
if (!ElementCallOpenTelemetry.instance) return;
|
if (!ElementCallOpenTelemetry.instance) return;
|
||||||
let call: OTelCall | undefined;
|
let call: OTelCall | undefined;
|
||||||
@@ -403,7 +412,7 @@ export class OTelGroupCallMembership {
|
|||||||
const data = ObjectFlattener.flattenReportObject(type, report);
|
const data = ObjectFlattener.flattenReportObject(type, report);
|
||||||
const ctx = opentelemetry.trace.setSpan(
|
const ctx = opentelemetry.trace.setSpan(
|
||||||
opentelemetry.context.active(),
|
opentelemetry.context.active(),
|
||||||
call.span
|
call.span,
|
||||||
);
|
);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -417,21 +426,21 @@ export class OTelGroupCallMembership {
|
|||||||
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||||
type,
|
type,
|
||||||
options,
|
options,
|
||||||
ctx
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
span.setAttribute("matrix.callId", callId ?? "unknown");
|
span.setAttribute("matrix.callId", callId ?? "unknown");
|
||||||
span.setAttribute(
|
span.setAttribute(
|
||||||
"matrix.opponentMemberId",
|
"matrix.opponentMemberId",
|
||||||
report.opponentMemberId ? report.opponentMemberId : "unknown"
|
report.opponentMemberId ? report.opponentMemberId : "unknown",
|
||||||
);
|
);
|
||||||
span.addEvent("matrix.call.connection_stats_event", data);
|
span.addEvent("matrix.call.connection_stats_event", data);
|
||||||
span.end();
|
span.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSummaryStatsReport(
|
public onSummaryStatsReport(
|
||||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
statsReport: GroupCallStatsReport<SummaryStatsReport>,
|
||||||
) {
|
): void {
|
||||||
if (!ElementCallOpenTelemetry.instance) return;
|
if (!ElementCallOpenTelemetry.instance) return;
|
||||||
|
|
||||||
const type = OTelStatsReportType.SummaryReport;
|
const type = OTelStatsReportType.SummaryReport;
|
||||||
@@ -439,12 +448,12 @@ export class OTelGroupCallMembership {
|
|||||||
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
|
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
|
||||||
const ctx = setSpan(
|
const ctx = setSpan(
|
||||||
opentelemetry.context.active(),
|
opentelemetry.context.active(),
|
||||||
this.callMembershipSpan
|
this.callMembershipSpan,
|
||||||
);
|
);
|
||||||
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
|
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
|
||||||
"matrix.groupCallMembership.summaryReport",
|
"matrix.groupCallMembership.summaryReport",
|
||||||
undefined,
|
undefined,
|
||||||
ctx
|
ctx,
|
||||||
);
|
);
|
||||||
if (span === undefined) {
|
if (span === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -453,7 +462,7 @@ export class OTelGroupCallMembership {
|
|||||||
span.setAttribute("matrix.userId", this.myUserId);
|
span.setAttribute("matrix.userId", this.myUserId);
|
||||||
span.setAttribute(
|
span.setAttribute(
|
||||||
"matrix.displayName",
|
"matrix.displayName",
|
||||||
this.myMember ? this.myMember.name : "unknown-name"
|
this.myMember ? this.myMember.name : "unknown-name",
|
||||||
);
|
);
|
||||||
span.addEvent(type, data);
|
span.addEvent(type, data);
|
||||||
span.end();
|
span.end();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
export class ObjectFlattener {
|
export class ObjectFlattener {
|
||||||
public static flattenReportObject(
|
public static flattenReportObject(
|
||||||
prefix: string,
|
prefix: string,
|
||||||
report: ConnectionStatsReport | ByteSentStatsReport
|
report: ConnectionStatsReport | ByteSentStatsReport,
|
||||||
): Attributes {
|
): Attributes {
|
||||||
const flatObject = {};
|
const flatObject = {};
|
||||||
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
|
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
|
||||||
@@ -33,27 +33,27 @@ export class ObjectFlattener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static flattenByteSentStatsReportObject(
|
public static flattenByteSentStatsReportObject(
|
||||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
|
||||||
): Attributes {
|
): Attributes {
|
||||||
const flatObject = {};
|
const flatObject = {};
|
||||||
ObjectFlattener.flattenObjectRecursive(
|
ObjectFlattener.flattenObjectRecursive(
|
||||||
statsReport.report,
|
statsReport.report,
|
||||||
flatObject,
|
flatObject,
|
||||||
"matrix.stats.bytesSent.",
|
"matrix.stats.bytesSent.",
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
return flatObject;
|
return flatObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
static flattenSummaryStatsReportObject(
|
public static flattenSummaryStatsReportObject(
|
||||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
statsReport: GroupCallStatsReport<SummaryStatsReport>,
|
||||||
) {
|
): Attributes {
|
||||||
const flatObject = {};
|
const flatObject = {};
|
||||||
ObjectFlattener.flattenObjectRecursive(
|
ObjectFlattener.flattenObjectRecursive(
|
||||||
statsReport.report,
|
statsReport.report,
|
||||||
flatObject,
|
flatObject,
|
||||||
"matrix.stats.summary.",
|
"matrix.stats.summary.",
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
return flatObject;
|
return flatObject;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ export class ObjectFlattener {
|
|||||||
event as unknown as Record<string, unknown>, // XXX Types
|
event as unknown as Record<string, unknown>, // XXX Types
|
||||||
flatObject,
|
flatObject,
|
||||||
"matrix.event.",
|
"matrix.event.",
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return flatObject;
|
return flatObject;
|
||||||
@@ -77,12 +77,12 @@ export class ObjectFlattener {
|
|||||||
obj: Object,
|
obj: Object,
|
||||||
flatObject: Attributes,
|
flatObject: Attributes,
|
||||||
prefix: string,
|
prefix: string,
|
||||||
depth: number
|
depth: number,
|
||||||
): void {
|
): void {
|
||||||
if (depth > 10)
|
if (depth > 10)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
|
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
|
||||||
prefix
|
prefix,
|
||||||
);
|
);
|
||||||
let entries;
|
let entries;
|
||||||
if (obj instanceof Map) {
|
if (obj instanceof Map) {
|
||||||
@@ -101,7 +101,7 @@ export class ObjectFlattener {
|
|||||||
v,
|
v,
|
||||||
flatObject,
|
flatObject,
|
||||||
prefix + k + ".",
|
prefix + k + ".",
|
||||||
depth + 1
|
depth + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class ElementCallOpenTelemetry {
|
|||||||
private otlpExporter?: OTLPTraceExporter;
|
private otlpExporter?: OTLPTraceExporter;
|
||||||
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
|
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
|
||||||
|
|
||||||
static globalInit(): void {
|
public static globalInit(): void {
|
||||||
const config = Config.get();
|
const config = Config.get();
|
||||||
// we always enable opentelemetry in general. We only enable the OTLP
|
// we always enable opentelemetry in general. We only enable the OTLP
|
||||||
// collector if a URL is defined (and in future if another setting is defined)
|
// collector if a URL is defined (and in future if another setting is defined)
|
||||||
@@ -50,18 +50,18 @@ export class ElementCallOpenTelemetry {
|
|||||||
|
|
||||||
sharedInstance = new ElementCallOpenTelemetry(
|
sharedInstance = new ElementCallOpenTelemetry(
|
||||||
config.opentelemetry?.collector_url,
|
config.opentelemetry?.collector_url,
|
||||||
config.rageshake?.submit_url
|
config.rageshake?.submit_url,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get instance(): ElementCallOpenTelemetry {
|
public static get instance(): ElementCallOpenTelemetry {
|
||||||
return sharedInstance;
|
return sharedInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
private constructor(
|
||||||
collectorUrl: string | undefined,
|
collectorUrl: string | undefined,
|
||||||
rageshakeUrl: string | undefined
|
rageshakeUrl: string | undefined,
|
||||||
) {
|
) {
|
||||||
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
|
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
|
||||||
const providerConfig = {
|
const providerConfig = {
|
||||||
@@ -77,7 +77,7 @@ export class ElementCallOpenTelemetry {
|
|||||||
url: collectorUrl,
|
url: collectorUrl,
|
||||||
});
|
});
|
||||||
this._provider.addSpanProcessor(
|
this._provider.addSpanProcessor(
|
||||||
new SimpleSpanProcessor(this.otlpExporter)
|
new SimpleSpanProcessor(this.otlpExporter),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.info("OTLP collector disabled");
|
logger.info("OTLP collector disabled");
|
||||||
@@ -93,7 +93,7 @@ export class ElementCallOpenTelemetry {
|
|||||||
|
|
||||||
this._tracer = opentelemetry.trace.getTracer(
|
this._tracer = opentelemetry.trace.getTracer(
|
||||||
// This is not the serviceName shown in jaeger
|
// This is not the serviceName shown in jaeger
|
||||||
"my-element-call-otl-tracer"
|
"my-element-call-otl-tracer",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
|
|||||||
shouldCloseOnBlur: true,
|
shouldCloseOnBlur: true,
|
||||||
isDismissable: true,
|
isDismissable: true,
|
||||||
},
|
},
|
||||||
popoverRef
|
popoverRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,5 +56,5 @@ export const Popover = forwardRef<HTMLDivElement, Props>(
|
|||||||
</div>
|
</div>
|
||||||
</FocusScope>
|
</FocusScope>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const PopoverMenuTrigger = forwardRef<
|
|||||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||||
{},
|
{},
|
||||||
popoverMenuState,
|
popoverMenuState,
|
||||||
buttonRef
|
buttonRef,
|
||||||
);
|
);
|
||||||
|
|
||||||
const popoverRef = useRef(null);
|
const popoverRef = useRef(null);
|
||||||
@@ -62,7 +62,7 @@ export const PopoverMenuTrigger = forwardRef<
|
|||||||
typeof children[1] !== "function"
|
typeof children[1] !== "function"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
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.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ type ProfileSaveCallback = ({
|
|||||||
removeAvatar: boolean;
|
removeAvatar: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
|
|
||||||
export function useProfile(client: MatrixClient | undefined) {
|
interface UseProfile extends ProfileLoadState {
|
||||||
|
saveProfile: ProfileSaveCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProfile(client: MatrixClient | undefined): UseProfile {
|
||||||
const [{ success, loading, displayName, avatarUrl, error }, setState] =
|
const [{ success, loading, displayName, avatarUrl, error }, setState] =
|
||||||
useState<ProfileLoadState>(() => {
|
useState<ProfileLoadState>(() => {
|
||||||
let user: User | undefined = undefined;
|
let user: User | undefined = undefined;
|
||||||
@@ -59,8 +63,8 @@ export function useProfile(client: MatrixClient | undefined) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onChangeUser = (
|
const onChangeUser = (
|
||||||
_event: MatrixEvent | undefined,
|
_event: MatrixEvent | undefined,
|
||||||
{ displayName, avatarUrl }: User
|
{ displayName, avatarUrl }: User,
|
||||||
) => {
|
): void => {
|
||||||
setState({
|
setState({
|
||||||
success: false,
|
success: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -104,9 +108,8 @@ export function useProfile(client: MatrixClient | undefined) {
|
|||||||
if (removeAvatar) {
|
if (removeAvatar) {
|
||||||
await client.setAvatarUrl("");
|
await client.setAvatarUrl("");
|
||||||
} else if (avatar) {
|
} else if (avatar) {
|
||||||
({ content_uri: mxcAvatarUrl } = await client.uploadContent(
|
({ content_uri: mxcAvatarUrl } =
|
||||||
avatar
|
await client.uploadContent(avatar));
|
||||||
));
|
|
||||||
await client.setAvatarUrl(mxcAvatarUrl);
|
await client.setAvatarUrl(mxcAvatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +134,7 @@ export function useProfile(client: MatrixClient | undefined) {
|
|||||||
logger.error("Client not initialized before calling saveProfile");
|
logger.error("Client not initialized before calling saveProfile");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client]
|
[client],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
[setOpen]
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const roomSharedKey = useRoomSharedKey(roomId ?? "");
|
const roomSharedKey = useRoomSharedKey(roomId ?? "");
|
||||||
const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
|
const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
|
||||||
if (roomIsEncrypted && roomSharedKey === undefined) {
|
if (roomIsEncrypted && roomSharedKey === undefined) {
|
||||||
logger.error(
|
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(
|
const url = new URL(
|
||||||
roomId === null
|
roomId === null
|
||||||
? window.location.href
|
? 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
|
// 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
|
// time within the app, and to keep the user confined to the current room
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FC, FormEventHandler, useCallback, useState } from "react";
|
import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
@@ -64,7 +64,7 @@ export const CallEndedView: FC<Props> = ({
|
|||||||
PosthogAnalytics.instance.eventQualitySurvey.track(
|
PosthogAnalytics.instance.eventQualitySurvey.track(
|
||||||
endedCallId,
|
endedCallId,
|
||||||
feedbackText,
|
feedbackText,
|
||||||
starRating
|
starRating,
|
||||||
);
|
);
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@@ -83,7 +83,7 @@ export const CallEndedView: FC<Props> = ({
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
[endedCallId, history, isPasswordlessUser, confineToRoom, starRating]
|
[endedCallId, history, isPasswordlessUser, confineToRoom, starRating],
|
||||||
);
|
);
|
||||||
|
|
||||||
const createAccountDialog = isPasswordlessUser && (
|
const createAccountDialog = isPasswordlessUser && (
|
||||||
@@ -148,7 +148,7 @@ export const CallEndedView: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = (): ReactNode => {
|
||||||
if (leaveError) {
|
if (leaveError) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function GroupCallLoader({
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
history.push("/");
|
history.push("/");
|
||||||
},
|
},
|
||||||
[history]
|
[history],
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (groupCallState.kind) {
|
switch (groupCallState.kind) {
|
||||||
@@ -66,7 +66,7 @@ export function GroupCallLoader({
|
|||||||
<Heading>{t("Call not found")}</Heading>
|
<Heading>{t("Call not found")}</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
{t(
|
{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>
|
</Text>
|
||||||
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Room, isE2EESupported } from "livekit-client";
|
import { Room, isE2EESupported } from "livekit-client";
|
||||||
@@ -61,14 +61,14 @@ interface Props {
|
|||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallView({
|
export const GroupCallView: FC<Props> = ({
|
||||||
client,
|
client,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
preload,
|
preload,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
}: Props) {
|
}) => {
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export function GroupCallView({
|
|||||||
// Count each member only once, regardless of how many devices they use
|
// Count each member only once, regardless of how many devices they use
|
||||||
const participantCount = useMemo(
|
const participantCount = useMemo(
|
||||||
() => new Set<string>(memberships.map((m) => m.sender!)).size,
|
() => new Set<string>(memberships.map((m) => m.sender!)).size,
|
||||||
[memberships]
|
[memberships],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceContext = useMediaDevices();
|
const deviceContext = useMediaDevices();
|
||||||
@@ -125,7 +125,9 @@ export function GroupCallView({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && preload) {
|
if (widget && preload) {
|
||||||
// In preload mode, wait for a join action before entering
|
// In preload mode, wait for a join action before entering
|
||||||
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onJoin = async (
|
||||||
|
ev: CustomEvent<IWidgetApiRequest>,
|
||||||
|
): Promise<void> => {
|
||||||
// XXX: I think this is broken currently - LiveKit *won't* request
|
// XXX: I think this is broken currently - LiveKit *won't* request
|
||||||
// permissions and give you device names unless you specify a kind, but
|
// permissions and give you device names unless you specify a kind, but
|
||||||
// here we want all kinds of devices. This needs a fix in livekit-client
|
// here we want all kinds of devices. This needs a fix in livekit-client
|
||||||
@@ -141,14 +143,14 @@ export function GroupCallView({
|
|||||||
const deviceId = await findDeviceByName(
|
const deviceId = await findDeviceByName(
|
||||||
audioInput,
|
audioInput,
|
||||||
"audioinput",
|
"audioinput",
|
||||||
devices
|
devices,
|
||||||
);
|
);
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
logger.warn("Unknown audio input: " + audioInput);
|
logger.warn("Unknown audio input: " + audioInput);
|
||||||
latestMuteStates.current!.audio.setEnabled?.(false);
|
latestMuteStates.current!.audio.setEnabled?.(false);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Found audio input ID ${deviceId} for name ${audioInput}`
|
`Found audio input ID ${deviceId} for name ${audioInput}`,
|
||||||
);
|
);
|
||||||
latestDevices.current!.audioInput.select(deviceId);
|
latestDevices.current!.audioInput.select(deviceId);
|
||||||
latestMuteStates.current!.audio.setEnabled?.(true);
|
latestMuteStates.current!.audio.setEnabled?.(true);
|
||||||
@@ -161,14 +163,14 @@ export function GroupCallView({
|
|||||||
const deviceId = await findDeviceByName(
|
const deviceId = await findDeviceByName(
|
||||||
videoInput,
|
videoInput,
|
||||||
"videoinput",
|
"videoinput",
|
||||||
devices
|
devices,
|
||||||
);
|
);
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
logger.warn("Unknown video input: " + videoInput);
|
logger.warn("Unknown video input: " + videoInput);
|
||||||
latestMuteStates.current!.video.setEnabled?.(false);
|
latestMuteStates.current!.video.setEnabled?.(false);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Found video input ID ${deviceId} for name ${videoInput}`
|
`Found video input ID ${deviceId} for name ${videoInput}`,
|
||||||
);
|
);
|
||||||
latestDevices.current!.videoInput.select(deviceId);
|
latestDevices.current!.videoInput.select(deviceId);
|
||||||
latestMuteStates.current!.video.setEnabled?.(true);
|
latestMuteStates.current!.video.setEnabled?.(true);
|
||||||
@@ -180,7 +182,7 @@ export function GroupCallView({
|
|||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
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
|
// we only have room sessions right now, so call ID is the emprty string - we use the room ID
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(
|
PosthogAnalytics.instance.eventCallStarted.track(
|
||||||
rtcSession.room.roomId
|
rtcSession.room.roomId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -211,7 +213,7 @@ export function GroupCallView({
|
|||||||
PosthogAnalytics.instance.eventCallEnded.track(
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
rtcSession.room.roomId,
|
rtcSession.room.roomId,
|
||||||
rtcSession.memberships.length,
|
rtcSession.memberships.length,
|
||||||
sendInstantly
|
sendInstantly,
|
||||||
);
|
);
|
||||||
|
|
||||||
await leaveRTCSession(rtcSession);
|
await leaveRTCSession(rtcSession);
|
||||||
@@ -235,14 +237,16 @@ export function GroupCallView({
|
|||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rtcSession, isPasswordlessUser, confineToRoom, history]
|
[rtcSession, isPasswordlessUser, confineToRoom, history],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && isJoined) {
|
if (widget && isJoined) {
|
||||||
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onHangup = async (
|
||||||
|
ev: CustomEvent<IWidgetApiRequest>,
|
||||||
|
): Promise<void> => {
|
||||||
leaveRTCSession(rtcSession);
|
leaveRTCSession(rtcSession);
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
widget!.api.transport.reply(ev.detail, {});
|
||||||
widget!.api.setAlwaysOnScreen(false);
|
widget!.api.setAlwaysOnScreen(false);
|
||||||
};
|
};
|
||||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||||
@@ -256,7 +260,7 @@ export function GroupCallView({
|
|||||||
|
|
||||||
const e2eeConfig = useMemo(
|
const e2eeConfig = useMemo(
|
||||||
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
|
() => (e2eeSharedKey ? { sharedKey: e2eeSharedKey } : undefined),
|
||||||
[e2eeSharedKey]
|
[e2eeSharedKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
@@ -270,12 +274,12 @@ export function GroupCallView({
|
|||||||
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
||||||
const onDismissInviteModal = useCallback(
|
const onDismissInviteModal = useCallback(
|
||||||
() => setInviteModalOpen(false),
|
() => setInviteModalOpen(false),
|
||||||
[setInviteModalOpen]
|
[setInviteModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onShareClickFn = useCallback(
|
const onShareClickFn = useCallback(
|
||||||
() => setInviteModalOpen(true),
|
() => setInviteModalOpen(true),
|
||||||
[setInviteModalOpen]
|
[setInviteModalOpen],
|
||||||
);
|
);
|
||||||
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||||
|
|
||||||
@@ -284,7 +288,7 @@ export function GroupCallView({
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
history.push("/");
|
history.push("/");
|
||||||
},
|
},
|
||||||
[history]
|
[history],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -294,7 +298,7 @@ export function GroupCallView({
|
|||||||
<ErrorView
|
<ErrorView
|
||||||
error={
|
error={
|
||||||
new 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.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -305,7 +309,7 @@ export function GroupCallView({
|
|||||||
<Heading>Incompatible Browser</Heading>
|
<Heading>Incompatible Browser</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
{t(
|
{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>
|
</Text>
|
||||||
<Link href="/" onClick={onHomeClick}>
|
<Link href="/" onClick={onHomeClick}>
|
||||||
@@ -381,7 +385,7 @@ export function GroupCallView({
|
|||||||
client={client}
|
client={client}
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
onEnter={() => enterRTCSession(rtcSession)}
|
onEnter={(): void => enterRTCSession(rtcSession)}
|
||||||
confineToRoom={confineToRoom}
|
confineToRoom={confineToRoom}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
@@ -390,4 +394,4 @@ export function GroupCallView({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -27,7 +27,16 @@ import { ConnectionState, Room, Track } from "livekit-client";
|
|||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
|
||||||
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
Ref,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -91,12 +100,12 @@ export interface ActiveCallProps
|
|||||||
e2eeConfig?: E2EEConfig;
|
e2eeConfig?: E2EEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActiveCall(props: ActiveCallProps) {
|
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||||
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||||
const { livekitRoom, connState } = useLiveKit(
|
const { livekitRoom, connState } = useLiveKit(
|
||||||
props.muteStates,
|
props.muteStates,
|
||||||
sfuConfig,
|
sfuConfig,
|
||||||
props.e2eeConfig
|
props.e2eeConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!livekitRoom) {
|
if (!livekitRoom) {
|
||||||
@@ -112,7 +121,7 @@ export function ActiveCall(props: ActiveCallProps) {
|
|||||||
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
|
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface InCallViewProps {
|
export interface InCallViewProps {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
@@ -128,7 +137,7 @@ export interface InCallViewProps {
|
|||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InCallView({
|
export const InCallView: FC<InCallViewProps> = ({
|
||||||
client,
|
client,
|
||||||
matrixInfo,
|
matrixInfo,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
@@ -140,7 +149,7 @@ export function InCallView({
|
|||||||
otelGroupCallMembership,
|
otelGroupCallMembership,
|
||||||
connState,
|
connState,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}: InCallViewProps) {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
@@ -163,10 +172,10 @@ export function InCallView({
|
|||||||
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
|
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
|
||||||
{
|
{
|
||||||
room: livekitRoom,
|
room: livekitRoom,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const { layout, setLayout } = useVideoGridLayout(
|
const { layout, setLayout } = useVideoGridLayout(
|
||||||
screenSharingTracks.length > 0
|
screenSharingTracks.length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showConnectionStats] = useShowConnectionStats();
|
const [showConnectionStats] = useShowConnectionStats();
|
||||||
@@ -179,11 +188,11 @@ export function InCallView({
|
|||||||
|
|
||||||
const toggleMicrophone = useCallback(
|
const toggleMicrophone = useCallback(
|
||||||
() => muteStates.audio.setEnabled?.((e) => !e),
|
() => muteStates.audio.setEnabled?.((e) => !e),
|
||||||
[muteStates]
|
[muteStates],
|
||||||
);
|
);
|
||||||
const toggleCamera = useCallback(
|
const toggleCamera = useCallback(
|
||||||
() => muteStates.video.setEnabled?.((e) => !e),
|
() => muteStates.video.setEnabled?.((e) => !e),
|
||||||
[muteStates]
|
[muteStates],
|
||||||
);
|
);
|
||||||
|
|
||||||
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
||||||
@@ -192,7 +201,7 @@ export function InCallView({
|
|||||||
containerRef1,
|
containerRef1,
|
||||||
toggleMicrophone,
|
toggleMicrophone,
|
||||||
toggleCamera,
|
toggleCamera,
|
||||||
(muted) => muteStates.audio.setEnabled?.(!muted)
|
(muted) => muteStates.audio.setEnabled?.(!muted),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLeavePress = useCallback(() => {
|
const onLeavePress = useCallback(() => {
|
||||||
@@ -204,32 +213,32 @@ export function InCallView({
|
|||||||
layout === "grid"
|
layout === "grid"
|
||||||
? ElementWidgetActions.TileLayout
|
? ElementWidgetActions.TileLayout
|
||||||
: ElementWidgetActions.SpotlightLayout,
|
: ElementWidgetActions.SpotlightLayout,
|
||||||
{}
|
{},
|
||||||
);
|
);
|
||||||
}, [layout]);
|
}, [layout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||||
setLayout("grid");
|
setLayout("grid");
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
widget!.api.transport.reply(ev.detail, {});
|
||||||
};
|
};
|
||||||
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||||
setLayout("spotlight");
|
setLayout("spotlight");
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
widget!.api.transport.reply(ev.detail, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
widget.lazyActions.on(
|
widget.lazyActions.on(
|
||||||
ElementWidgetActions.SpotlightLayout,
|
ElementWidgetActions.SpotlightLayout,
|
||||||
onSpotlightLayout
|
onSpotlightLayout,
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
widget!.lazyActions.off(
|
widget!.lazyActions.off(
|
||||||
ElementWidgetActions.SpotlightLayout,
|
ElementWidgetActions.SpotlightLayout,
|
||||||
onSpotlightLayout
|
onSpotlightLayout,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -252,7 +261,7 @@ export function InCallView({
|
|||||||
(noControls
|
(noControls
|
||||||
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
|
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
|
||||||
: null),
|
: null),
|
||||||
[fullscreenItem, noControls, items]
|
[fullscreenItem, noControls, items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const Grid =
|
const Grid =
|
||||||
@@ -295,7 +304,7 @@ export function InCallView({
|
|||||||
disableAnimations={prefersReducedMotion || isSafari}
|
disableAnimations={prefersReducedMotion || isSafari}
|
||||||
layoutStates={layoutStates}
|
layoutStates={layoutStates}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props): ReactNode => (
|
||||||
<VideoTile
|
<VideoTile
|
||||||
maximised={false}
|
maximised={false}
|
||||||
fullscreen={false}
|
fullscreen={false}
|
||||||
@@ -311,18 +320,18 @@ export function InCallView({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||||
rtcSession.room.roomId
|
rtcSession.room.roomId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||||
|
|
||||||
const openSettings = useCallback(
|
const openSettings = useCallback(
|
||||||
() => setSettingsModalOpen(true),
|
() => setSettingsModalOpen(true),
|
||||||
[setSettingsModalOpen]
|
[setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
const closeSettings = useCallback(
|
const closeSettings = useCallback(
|
||||||
() => setSettingsModalOpen(false),
|
() => setSettingsModalOpen(false),
|
||||||
[setSettingsModalOpen]
|
[setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleScreensharing = useCallback(async () => {
|
const toggleScreensharing = useCallback(async () => {
|
||||||
@@ -356,7 +365,7 @@ export function InCallView({
|
|||||||
onPress={toggleCamera}
|
onPress={toggleCamera}
|
||||||
disabled={muteStates.video.setEnabled === null}
|
disabled={muteStates.video.setEnabled === null}
|
||||||
data-testid="incall_videomute"
|
data-testid="incall_videomute"
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!reducedControls) {
|
if (!reducedControls) {
|
||||||
@@ -367,14 +376,18 @@ export function InCallView({
|
|||||||
enabled={isScreenShareEnabled}
|
enabled={isScreenShareEnabled}
|
||||||
onPress={toggleScreensharing}
|
onPress={toggleScreensharing}
|
||||||
data-testid="incall_screenshare"
|
data-testid="incall_screenshare"
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<HangupButton key="6" onPress={onLeavePress} data-testid="incall_leave" />
|
<HangupButton
|
||||||
|
key="6"
|
||||||
|
onPress={onLeavePress}
|
||||||
|
data-testid="incall_leave"
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
footer = (
|
footer = (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
@@ -434,11 +447,11 @@ export function InCallView({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function findMatrixMember(
|
function findMatrixMember(
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
id: string
|
id: string,
|
||||||
): RoomMember | undefined {
|
): RoomMember | undefined {
|
||||||
if (!id) return undefined;
|
if (!id) return undefined;
|
||||||
|
|
||||||
@@ -446,7 +459,7 @@ function findMatrixMember(
|
|||||||
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
|
// 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) {
|
if (parts.length < 3) {
|
||||||
logger.warn(
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -460,7 +473,7 @@ function findMatrixMember(
|
|||||||
function useParticipantTiles(
|
function useParticipantTiles(
|
||||||
livekitRoom: Room,
|
livekitRoom: Room,
|
||||||
matrixRoom: MatrixRoom,
|
matrixRoom: MatrixRoom,
|
||||||
connState: ECConnectionState
|
connState: ECConnectionState,
|
||||||
): TileDescriptor<ItemData>[] {
|
): TileDescriptor<ItemData>[] {
|
||||||
const previousTiles = useRef<TileDescriptor<ItemData>[]>([]);
|
const previousTiles = useRef<TileDescriptor<ItemData>[]>([]);
|
||||||
|
|
||||||
@@ -489,7 +502,7 @@ function useParticipantTiles(
|
|||||||
// connected, this is fine and we'll be in "all ghosts" mode.
|
// connected, this is fine and we'll be in "all ghosts" mode.
|
||||||
if (id !== "" && member === undefined) {
|
if (id !== "" && member === undefined) {
|
||||||
logger.warn(
|
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;
|
allGhosts &&= member === undefined;
|
||||||
@@ -533,11 +546,11 @@ function useParticipantTiles(
|
|||||||
return screenShareTile
|
return screenShareTile
|
||||||
? [userMediaTile, screenShareTile]
|
? [userMediaTile, screenShareTile]
|
||||||
: [userMediaTile];
|
: [userMediaTile];
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
||||||
tiles.length
|
tiles.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If every item is a ghost, that probably means we're still connecting and
|
// If every item is a ghost, that probably means we're still connecting and
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
|||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
||||||
[room, roomSharedKey]
|
[room, roomSharedKey],
|
||||||
);
|
);
|
||||||
const [, setCopied] = useClipboard(url);
|
const [, setCopied] = useClipboard(url);
|
||||||
const [toastOpen, setToastOpen] = useState(false);
|
const [toastOpen, setToastOpen] = useState(false);
|
||||||
@@ -53,7 +53,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
|||||||
onDismiss();
|
onDismiss();
|
||||||
setToastOpen(true);
|
setToastOpen(true);
|
||||||
},
|
},
|
||||||
[setCopied, onDismiss]
|
[setCopied, onDismiss],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const LayoutToggle: FC<Props> = ({ layout, setLayout, className }) => {
|
|||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
|
(e: ChangeEvent<HTMLInputElement>) => setLayout(e.target.value as Layout),
|
||||||
[setLayout]
|
[setLayout],
|
||||||
);
|
);
|
||||||
|
|
||||||
const spotlightId = useId();
|
const spotlightId = useId();
|
||||||
|
|||||||
@@ -63,22 +63,22 @@ export const LobbyView: FC<Props> = ({
|
|||||||
|
|
||||||
const onAudioPress = useCallback(
|
const onAudioPress = useCallback(
|
||||||
() => muteStates.audio.setEnabled?.((e) => !e),
|
() => muteStates.audio.setEnabled?.((e) => !e),
|
||||||
[muteStates]
|
[muteStates],
|
||||||
);
|
);
|
||||||
const onVideoPress = useCallback(
|
const onVideoPress = useCallback(
|
||||||
() => muteStates.video.setEnabled?.((e) => !e),
|
() => muteStates.video.setEnabled?.((e) => !e),
|
||||||
[muteStates]
|
[muteStates],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||||
|
|
||||||
const openSettings = useCallback(
|
const openSettings = useCallback(
|
||||||
() => setSettingsModalOpen(true),
|
() => setSettingsModalOpen(true),
|
||||||
[setSettingsModalOpen]
|
[setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
const closeSettings = useCallback(
|
const closeSettings = useCallback(
|
||||||
() => setSettingsModalOpen(false),
|
() => setSettingsModalOpen(false),
|
||||||
[setSettingsModalOpen]
|
[setSettingsModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|||||||
@@ -49,18 +49,18 @@ export interface MuteStates {
|
|||||||
|
|
||||||
function useMuteState(
|
function useMuteState(
|
||||||
device: MediaDevice,
|
device: MediaDevice,
|
||||||
enabledByDefault: () => boolean
|
enabledByDefault: () => boolean,
|
||||||
): MuteState {
|
): MuteState {
|
||||||
const [enabled, setEnabled] = useReactiveState<boolean>(
|
const [enabled, setEnabled] = useReactiveState<boolean>(
|
||||||
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
|
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
|
||||||
[device]
|
[device],
|
||||||
);
|
);
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
device.available.length === 0
|
device.available.length === 0
|
||||||
? deviceUnavailable
|
? deviceUnavailable
|
||||||
: { enabled, setEnabled },
|
: { enabled, setEnabled },
|
||||||
[device, enabled, setEnabled]
|
[device, enabled, setEnabled],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export function useMuteStates(participantCount: number): MuteStates {
|
|||||||
|
|
||||||
const audio = useMuteState(
|
const audio = useMuteState(
|
||||||
devices.audioInput,
|
devices.audioInput,
|
||||||
() => participantCount <= MUTE_PARTICIPANT_COUNT
|
() => participantCount <= MUTE_PARTICIPANT_COUNT,
|
||||||
);
|
);
|
||||||
const video = useMuteState(devices.videoInput, () => true);
|
const video = useMuteState(devices.videoInput, () => true);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import { FC, useEffect } from "react";
|
import { FC, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Modal, ModalProps } from "../Modal";
|
import { Modal, Props as ModalProps } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||||
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||||
@@ -47,13 +47,13 @@ export const RageshakeRequestModal: FC<Props> = ({
|
|||||||
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
|
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
|
||||||
<Body>
|
<Body>
|
||||||
{t(
|
{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>
|
</Body>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<Button
|
<Button
|
||||||
onPress={() =>
|
onPress={(): void =>
|
||||||
submitRageshake({
|
void submitRageshake({
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
roomId,
|
roomId,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { FC, useCallback, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -29,7 +29,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
|||||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
|
|
||||||
export function RoomAuthView() {
|
export const RoomAuthView: FC = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export function RoomAuthView() {
|
|||||||
setError(error);
|
setError(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[registerPasswordlessUser]
|
[registerPasswordlessUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -122,4 +122,4 @@ export function RoomAuthView() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const RoomPage: FC = () => {
|
|||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[client, passwordlessUser, confineToRoom, preload, hideHeader]
|
[client, passwordlessUser, confineToRoom, preload, hideHeader],
|
||||||
);
|
);
|
||||||
|
|
||||||
let content: ReactNode;
|
let content: ReactNode;
|
||||||
|
|||||||
@@ -82,14 +82,14 @@ export const VideoPreview: FC<Props> = ({
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.error("Error while creating preview Tracks:", error);
|
logger.error("Error while creating preview Tracks:", error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const videoTrack = useMemo(
|
const videoTrack = useMemo(
|
||||||
() =>
|
() =>
|
||||||
tracks?.find((t) => t.kind === Track.Kind.Video) as
|
tracks?.find((t) => t.kind === Track.Kind.Video) as
|
||||||
| LocalVideoTrack
|
| LocalVideoTrack
|
||||||
| undefined,
|
| undefined,
|
||||||
[tracks]
|
[tracks],
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoEl = useRef<HTMLVideoElement | null>(null);
|
const videoEl = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { deepCompare } from "matrix-js-sdk/src/utils";
|
|||||||
import { LivekitFocus } from "../livekit/LivekitFocus";
|
import { LivekitFocus } from "../livekit/LivekitFocus";
|
||||||
|
|
||||||
function getActiveFocus(
|
function getActiveFocus(
|
||||||
rtcSession: MatrixRTCSession
|
rtcSession: MatrixRTCSession,
|
||||||
): LivekitFocus | undefined {
|
): LivekitFocus | undefined {
|
||||||
const oldestMembership = rtcSession.getOldestMembership();
|
const oldestMembership = rtcSession.getOldestMembership();
|
||||||
return oldestMembership?.getActiveFoci()[0] as LivekitFocus;
|
return oldestMembership?.getActiveFoci()[0] as LivekitFocus;
|
||||||
@@ -36,10 +36,10 @@ function getActiveFocus(
|
|||||||
* and the same focus.
|
* and the same focus.
|
||||||
*/
|
*/
|
||||||
export function useActiveFocus(
|
export function useActiveFocus(
|
||||||
rtcSession: MatrixRTCSession
|
rtcSession: MatrixRTCSession,
|
||||||
): LivekitFocus | undefined {
|
): LivekitFocus | undefined {
|
||||||
const [activeFocus, setActiveFocus] = useState(() =>
|
const [activeFocus, setActiveFocus] = useState(() =>
|
||||||
getActiveFocus(rtcSession)
|
getActiveFocus(rtcSession),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMembershipsChanged = useCallback(() => {
|
const onMembershipsChanged = useCallback(() => {
|
||||||
@@ -53,13 +53,13 @@ export function useActiveFocus(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rtcSession.on(
|
rtcSession.on(
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
onMembershipsChanged
|
onMembershipsChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
rtcSession.off(
|
rtcSession.off(
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
onMembershipsChanged
|
onMembershipsChanged,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import { TileDescriptor } from "../video-grid/VideoGrid";
|
|||||||
import { useReactiveState } from "../useReactiveState";
|
import { useReactiveState } from "../useReactiveState";
|
||||||
import { useEventTarget } from "../useEvents";
|
import { useEventTarget } from "../useEvents";
|
||||||
|
|
||||||
const isFullscreen = () =>
|
const isFullscreen = (): boolean =>
|
||||||
Boolean(document.fullscreenElement) ||
|
Boolean(document.fullscreenElement) ||
|
||||||
Boolean(document.webkitFullscreenElement);
|
Boolean(document.webkitFullscreenElement);
|
||||||
|
|
||||||
function enterFullscreen() {
|
function enterFullscreen(): void {
|
||||||
if (document.body.requestFullscreen) {
|
if (document.body.requestFullscreen) {
|
||||||
document.body.requestFullscreen();
|
document.body.requestFullscreen();
|
||||||
} else if (document.body.webkitRequestFullscreen) {
|
} else if (document.body.webkitRequestFullscreen) {
|
||||||
@@ -36,7 +36,7 @@ function enterFullscreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitFullscreen() {
|
function exitFullscreen(): void {
|
||||||
if (document.exitFullscreen) {
|
if (document.exitFullscreen) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
} else if (document.webkitExitFullscreen) {
|
} else if (document.webkitExitFullscreen) {
|
||||||
@@ -46,7 +46,7 @@ function exitFullscreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFullscreenChange(onFullscreenChange: () => void) {
|
function useFullscreenChange(onFullscreenChange: () => void): void {
|
||||||
useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
|
useEventTarget(document.body, "fullscreenchange", onFullscreenChange);
|
||||||
useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
|
useEventTarget(document.body, "webkitfullscreenchange", onFullscreenChange);
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
|||||||
prevItem == null
|
prevItem == null
|
||||||
? null
|
? null
|
||||||
: items.find((i) => i.id === prevItem.id) ?? null,
|
: items.find((i) => i.id === prevItem.id) ?? null,
|
||||||
[items]
|
[items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const latestItems = useRef<TileDescriptor<T>[]>(items);
|
const latestItems = useRef<TileDescriptor<T>[]>(items);
|
||||||
@@ -80,15 +80,15 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
|||||||
setFullscreenItem(
|
setFullscreenItem(
|
||||||
latestFullscreenItem.current === null
|
latestFullscreenItem.current === null
|
||||||
? latestItems.current.find((i) => i.id === itemId) ?? null
|
? latestItems.current.find((i) => i.id === itemId) ?? null
|
||||||
: null
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setFullscreenItem]
|
[setFullscreenItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const exitFullscreenCallback = useCallback(
|
const exitFullscreenCallback = useCallback(
|
||||||
() => setFullscreenItem(null),
|
() => setFullscreenItem(null),
|
||||||
[setFullscreenItem]
|
[setFullscreenItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -103,7 +103,7 @@ export function useFullscreen<T>(items: TileDescriptor<T>[]): {
|
|||||||
useFullscreenChange(
|
useFullscreenChange(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (!isFullscreen()) setFullscreenItem(null);
|
if (!isFullscreen()) setFullscreenItem(null);
|
||||||
}, [setFullscreenItem])
|
}, [setFullscreenItem]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { useRoomState } from "./useRoomState";
|
import { useRoomState } from "./useRoomState";
|
||||||
|
|
||||||
export const useJoinRule = (room: Room) =>
|
export function useJoinRule(room: Room): JoinRule {
|
||||||
useRoomState(
|
return useRoomState(
|
||||||
room,
|
room,
|
||||||
useCallback((state) => state.getJoinRule(), [])
|
useCallback((state) => state.getJoinRule(), []),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export interface GroupCallLoadState {
|
|||||||
export const useLoadGroupCall = (
|
export const useLoadGroupCall = (
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
roomIdOrAlias: string,
|
roomIdOrAlias: string,
|
||||||
viaServers: string[]
|
viaServers: string[],
|
||||||
): GroupCallStatus => {
|
): GroupCallStatus => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||||
@@ -70,7 +70,7 @@ export const useLoadGroupCall = (
|
|||||||
// join anyway but the js-sdk recreates the room if you pass the alias for a
|
// 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).
|
// room you're already joined to (which it probably ought not to).
|
||||||
const lookupResult = await client.getRoomIdForAlias(
|
const lookupResult = await client.getRoomIdForAlias(
|
||||||
roomIdOrAlias.toLowerCase()
|
roomIdOrAlias.toLowerCase(),
|
||||||
);
|
);
|
||||||
logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`);
|
logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`);
|
||||||
room = client.getRoom(lookupResult.room_id);
|
room = client.getRoom(lookupResult.room_id);
|
||||||
@@ -81,7 +81,7 @@ export const useLoadGroupCall = (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Already in room ${lookupResult.room_id}, not rejoining.`
|
`Already in room ${lookupResult.room_id}, not rejoining.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -92,7 +92,7 @@ export const useLoadGroupCall = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
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);
|
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
||||||
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
||||||
@@ -107,13 +107,13 @@ export const useLoadGroupCall = (
|
|||||||
return rtcSession;
|
return rtcSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForClientSyncing = async () => {
|
const waitForClientSyncing = async (): Promise<void> => {
|
||||||
if (client.getSyncState() !== SyncState.Syncing) {
|
if (client.getSyncState() !== SyncState.Syncing) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"useLoadGroupCall: waiting for client to start syncing..."
|
"useLoadGroupCall: waiting for client to start syncing...",
|
||||||
);
|
);
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const onSync = () => {
|
const onSync = (): void => {
|
||||||
if (client.getSyncState() === SyncState.Syncing) {
|
if (client.getSyncState() === SyncState.Syncing) {
|
||||||
client.off(ClientEvent.Sync, onSync);
|
client.off(ClientEvent.Sync, onSync);
|
||||||
return resolve();
|
return resolve();
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import { useEffect } from "react";
|
|||||||
|
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
|
|
||||||
export function usePageUnload(callback: () => void) {
|
export function usePageUnload(callback: () => void): void {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
|
let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
function onBeforeUnload(event: PageTransitionEvent) {
|
function onBeforeUnload(event: PageTransitionEvent): void {
|
||||||
if (event.type === "visibilitychange") {
|
if (event.type === "visibilitychange") {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
clearTimeout(pageVisibilityTimeout);
|
clearTimeout(pageVisibilityTimeout);
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||||||
|
|
||||||
import { useRoomState } from "./useRoomState";
|
import { useRoomState } from "./useRoomState";
|
||||||
|
|
||||||
export const useRoomAvatar = (room: Room) =>
|
export function useRoomAvatar(room: Room): string | null {
|
||||||
useRoomState(
|
return useRoomState(
|
||||||
room,
|
room,
|
||||||
useCallback(() => room.getMxcAvatarUrl(), [room])
|
useCallback(() => room.getMxcAvatarUrl(), [room]),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const useRoomState = <T>(room: Room, f: (state: RoomState) => T): T => {
|
|||||||
useTypedEventEmitter(
|
useTypedEventEmitter(
|
||||||
room,
|
room,
|
||||||
RoomStateEvent.Update,
|
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
|
// We want any change to the update counter to trigger an update here
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function makeFocus(livekitAlias: string): LivekitFocus {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enterRTCSession(rtcSession: MatrixRTCSession) {
|
export function enterRTCSession(rtcSession: MatrixRTCSession): void {
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function leaveRTCSession(
|
export async function leaveRTCSession(
|
||||||
rtcSession: MatrixRTCSession
|
rtcSession: MatrixRTCSession,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
//groupCallOTelMembership?.onLeaveCall();
|
//groupCallOTelMembership?.onLeaveCall();
|
||||||
await rtcSession.leaveRoomSession();
|
await rtcSession.leaveRoomSession();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { FC, useCallback } from "react";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ interface Props {
|
|||||||
roomId?: string;
|
roomId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedbackSettingsTab({ roomId }: Props) {
|
export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
const sendRageshakeRequest = useRageshakeRequest();
|
const sendRageshakeRequest = useRageshakeRequest();
|
||||||
@@ -57,7 +57,7 @@ export function FeedbackSettingsTab({ roomId }: Props) {
|
|||||||
sendRageshakeRequest(roomId, rageshakeRequestId);
|
sendRageshakeRequest(roomId, rageshakeRequestId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[submitRageshake, roomId, sendRageshakeRequest]
|
[submitRageshake, roomId, sendRageshakeRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +65,7 @@ export function FeedbackSettingsTab({ roomId }: Props) {
|
|||||||
<h4 className={styles.label}>{t("Submit feedback")}</h4>
|
<h4 className={styles.label}>{t("Submit feedback")}</h4>
|
||||||
<Body>
|
<Body>
|
||||||
{t(
|
{t(
|
||||||
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below."
|
"If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
<form onSubmit={onSubmitFeedback}>
|
<form onSubmit={onSubmitFeedback}>
|
||||||
@@ -104,4 +104,4 @@ export function FeedbackSettingsTab({ roomId }: Props) {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import { FC, useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ import styles from "./ProfileSettingsTab.module.css";
|
|||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
}
|
}
|
||||||
export function ProfileSettingsTab({ client }: Props) {
|
export const ProfileSettingsTab: FC<Props> = ({ client }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
|
const { error, displayName, avatarUrl, saveProfile } = useProfile(client);
|
||||||
const userId = useMemo(() => client.getUserId(), [client]);
|
const userId = useMemo(() => client.getUserId(), [client]);
|
||||||
@@ -120,4 +120,4 @@ export function ProfileSettingsTab({ client }: Props) {
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCallback } from "react";
|
import { FC, useCallback } from "react";
|
||||||
|
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
@@ -26,7 +26,7 @@ interface Props {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RageshakeButton = ({ description }: Props) => {
|
export const RageshakeButton: FC<Props> = ({ description }) => {
|
||||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChangeEvent, Key, useCallback, useState } from "react";
|
import { ChangeEvent, FC, Key, ReactNode, useCallback, useState } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { MatrixClient } from "matrix-js-sdk";
|
import { MatrixClient } from "matrix-js-sdk";
|
||||||
@@ -56,7 +56,7 @@ interface Props {
|
|||||||
defaultTab?: string;
|
defaultTab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsModal = (props: Props) => {
|
export const SettingsModal: FC<Props> = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||||
@@ -67,7 +67,10 @@ export const SettingsModal = (props: Props) => {
|
|||||||
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
|
const [enableE2EE, setEnableE2EE] = useEnableE2EE();
|
||||||
|
|
||||||
// Generate a `SelectInput` with a list of devices for a given device kind.
|
// Generate a `SelectInput` with a list of devices for a given device kind.
|
||||||
const generateDeviceSelection = (devices: MediaDevice, caption: string) => {
|
const generateDeviceSelection = (
|
||||||
|
devices: MediaDevice,
|
||||||
|
caption: string,
|
||||||
|
): ReactNode => {
|
||||||
if (devices.available.length == 0) return null;
|
if (devices.available.length == 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +81,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
? "default"
|
? "default"
|
||||||
: devices.selectedId
|
: devices.selectedId
|
||||||
}
|
}
|
||||||
onSelectionChange={(id) => devices.select(id.toString())}
|
onSelectionChange={(id): void => devices.select(id.toString())}
|
||||||
>
|
>
|
||||||
{devices.available.map(({ deviceId, label }, index) => (
|
{devices.available.map(({ deviceId, label }, index) => (
|
||||||
<Item key={deviceId}>
|
<Item key={deviceId}>
|
||||||
@@ -97,7 +100,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
(tab: Key) => {
|
(tab: Key) => {
|
||||||
setSelectedTab(tab.toString());
|
setSelectedTab(tab.toString());
|
||||||
},
|
},
|
||||||
[setSelectedTab]
|
[setSelectedTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
const optInDescription = (
|
const optInDescription = (
|
||||||
@@ -191,7 +194,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
checked={developerSettingsTab}
|
checked={developerSettingsTab}
|
||||||
label={t("Developer Settings")}
|
label={t("Developer Settings")}
|
||||||
description={t("Expose developer settings in the settings window.")}
|
description={t("Expose developer settings in the settings window.")}
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: ChangeEvent<HTMLInputElement>): void =>
|
||||||
setDeveloperSettingsTab(event.target.checked)
|
setDeveloperSettingsTab(event.target.checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -203,7 +206,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={optInAnalytics ?? undefined}
|
checked={optInAnalytics ?? undefined}
|
||||||
description={optInDescription}
|
description={optInDescription}
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
setOptInAnalytics?.(event.target.checked);
|
setOptInAnalytics?.(event.target.checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -235,7 +238,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
label={t("Show connection stats")}
|
label={t("Show connection stats")}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showConnectionStats}
|
checked={showConnectionStats}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||||
setShowConnectionStats(e.target.checked)
|
setShowConnectionStats(e.target.checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -252,7 +255,7 @@ export const SettingsModal = (props: Props) => {
|
|||||||
disabled={!setEnableE2EE}
|
disabled={!setEnableE2EE}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={enableE2EE ?? undefined}
|
checked={enableE2EE ?? undefined}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||||
setEnableE2EE?.(e.target.checked)
|
setEnableE2EE?.(e.target.checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import EventEmitter from "events";
|
|||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { LoggingMethod } from "loglevel";
|
||||||
|
|
||||||
// the length of log data we keep in indexeddb (and include in the reports)
|
// the length of log data we keep in indexeddb (and include in the reports)
|
||||||
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
||||||
@@ -130,9 +131,9 @@ class IndexedDBLogStore {
|
|||||||
private flushAgainPromise?: Promise<void>;
|
private flushAgainPromise?: Promise<void>;
|
||||||
private id: string;
|
private id: string;
|
||||||
|
|
||||||
constructor(
|
public constructor(
|
||||||
private indexedDB: IDBFactory,
|
private indexedDB: IDBFactory,
|
||||||
private loggerInstance: ConsoleLogger
|
private loggerInstance: ConsoleLogger,
|
||||||
) {
|
) {
|
||||||
this.id = "instance-" + randomString(16);
|
this.id = "instance-" + randomString(16);
|
||||||
|
|
||||||
@@ -146,20 +147,20 @@ class IndexedDBLogStore {
|
|||||||
public connect(): Promise<void> {
|
public connect(): Promise<void> {
|
||||||
const req = this.indexedDB.open("logs");
|
const req = this.indexedDB.open("logs");
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = () => {
|
req.onsuccess = (): void => {
|
||||||
this.db = req.result;
|
this.db = req.result;
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
req.onerror = () => {
|
req.onerror = (): void => {
|
||||||
const err = "Failed to open log database: " + req?.error?.name;
|
const err = "Failed to open log database: " + req?.error?.name;
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
reject(new Error(err));
|
reject(new Error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
// First time: Setup the object store
|
// First time: Setup the object store
|
||||||
req.onupgradeneeded = () => {
|
req.onupgradeneeded = (): void => {
|
||||||
const db = req.result;
|
const db = req.result;
|
||||||
// This is the log entries themselves. Each entry is a chunk of
|
// This is the log entries themselves. Each entry is a chunk of
|
||||||
// logs (ie multiple lines). 'id' is the instance ID (so logs with
|
// logs (ie multiple lines). 'id' is the instance ID (so logs with
|
||||||
@@ -176,7 +177,7 @@ class IndexedDBLogStore {
|
|||||||
logObjStore.createIndex("id", "id", { unique: false });
|
logObjStore.createIndex("id", "id", { unique: false });
|
||||||
|
|
||||||
logObjStore.add(
|
logObjStore.add(
|
||||||
this.generateLogEntry(new Date() + " ::: Log database was created.")
|
this.generateLogEntry(new Date() + " ::: Log database was created."),
|
||||||
);
|
);
|
||||||
|
|
||||||
// This records the last time each instance ID generated a log message, such
|
// This records the last time each instance ID generated a log message, such
|
||||||
@@ -190,7 +191,7 @@ class IndexedDBLogStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLoggerLog = () => {
|
private onLoggerLog = (): void => {
|
||||||
if (!this.db) return;
|
if (!this.db) return;
|
||||||
|
|
||||||
this.throttledFlush();
|
this.throttledFlush();
|
||||||
@@ -207,7 +208,7 @@ class IndexedDBLogStore {
|
|||||||
{
|
{
|
||||||
leading: false,
|
leading: false,
|
||||||
trailing: true,
|
trailing: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,10 +262,10 @@ class IndexedDBLogStore {
|
|||||||
}
|
}
|
||||||
const txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
const txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
const objStore = txn.objectStore("logs");
|
const objStore = txn.objectStore("logs");
|
||||||
txn.oncomplete = () => {
|
txn.oncomplete = (): void => {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
txn.onerror = (event) => {
|
txn.onerror = (event): void => {
|
||||||
logger.error("Failed to flush logs : ", event);
|
logger.error("Failed to flush logs : ", event);
|
||||||
reject(new Error("Failed to write logs: " + txn?.error?.message));
|
reject(new Error("Failed to write logs: " + txn?.error?.message));
|
||||||
};
|
};
|
||||||
@@ -305,10 +306,10 @@ class IndexedDBLogStore {
|
|||||||
.index("id")
|
.index("id")
|
||||||
.openCursor(IDBKeyRange.only(id), "prev");
|
.openCursor(IDBKeyRange.only(id), "prev");
|
||||||
let lines = "";
|
let lines = "";
|
||||||
query.onerror = () => {
|
query.onerror = (): void => {
|
||||||
reject(new Error("Query failed: " + query?.error?.message));
|
reject(new Error("Query failed: " + query?.error?.message));
|
||||||
};
|
};
|
||||||
query.onsuccess = () => {
|
query.onsuccess = (): void => {
|
||||||
const cursor = query.result;
|
const cursor = query.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(lines);
|
resolve(lines);
|
||||||
@@ -351,7 +352,7 @@ class IndexedDBLogStore {
|
|||||||
const o = txn.objectStore("logs");
|
const o = txn.objectStore("logs");
|
||||||
// only load the key path, not the data which may be huge
|
// only load the key path, not the data which may be huge
|
||||||
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
|
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
|
||||||
query.onsuccess = () => {
|
query.onsuccess = (): void => {
|
||||||
const cursor = query.result;
|
const cursor = query.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
return;
|
return;
|
||||||
@@ -359,14 +360,14 @@ class IndexedDBLogStore {
|
|||||||
o.delete(cursor.primaryKey);
|
o.delete(cursor.primaryKey);
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
};
|
};
|
||||||
txn.oncomplete = () => {
|
txn.oncomplete = (): void => {
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
txn.onerror = () => {
|
txn.onerror = (): void => {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
"Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`
|
"Failed to delete logs for " + `'${id}' : ${txn?.error?.message}`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// delete last modified entries
|
// delete last modified entries
|
||||||
@@ -409,7 +410,7 @@ class IndexedDBLogStore {
|
|||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return logs;
|
return logs;
|
||||||
@@ -444,16 +445,16 @@ class IndexedDBLogStore {
|
|||||||
function selectQuery<T>(
|
function selectQuery<T>(
|
||||||
store: IDBObjectStore,
|
store: IDBObjectStore,
|
||||||
keyRange: IDBKeyRange | undefined,
|
keyRange: IDBKeyRange | undefined,
|
||||||
resultMapper: (cursor: IDBCursorWithValue) => T
|
resultMapper: (cursor: IDBCursorWithValue) => T,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const query = store.openCursor(keyRange);
|
const query = store.openCursor(keyRange);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const results: T[] = [];
|
const results: T[] = [];
|
||||||
query.onerror = () => {
|
query.onerror = (): void => {
|
||||||
reject(new Error("Query failed: " + query?.error?.message));
|
reject(new Error("Query failed: " + query?.error?.message));
|
||||||
};
|
};
|
||||||
// collect results
|
// collect results
|
||||||
query.onsuccess = () => {
|
query.onsuccess = (): void => {
|
||||||
const cursor = query.result;
|
const cursor = query.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(results);
|
resolve(results);
|
||||||
@@ -509,7 +510,7 @@ function tryInitStorage(): Promise<void> {
|
|||||||
if (indexedDB) {
|
if (indexedDB) {
|
||||||
global.mx_rage_store = new IndexedDBLogStore(
|
global.mx_rage_store = new IndexedDBLogStore(
|
||||||
indexedDB,
|
indexedDB,
|
||||||
global.mx_rage_logger
|
global.mx_rage_logger,
|
||||||
);
|
);
|
||||||
global.mx_rage_initStoragePromise = global.mx_rage_store.connect();
|
global.mx_rage_initStoragePromise = global.mx_rage_store.connect();
|
||||||
return global.mx_rage_initStoragePromise;
|
return global.mx_rage_initStoragePromise;
|
||||||
@@ -546,7 +547,7 @@ export async function getLogsForReport(): Promise<LogEntry[]> {
|
|||||||
type StringifyReplacer = (
|
type StringifyReplacer = (
|
||||||
this: unknown,
|
this: unknown,
|
||||||
key: string,
|
key: string,
|
||||||
value: unknown
|
value: unknown,
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
|
||||||
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
|
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
|
||||||
@@ -593,10 +594,14 @@ type LogLevelString = keyof typeof LogLevel;
|
|||||||
* took loglevel's example honouring log levels). Adds a loglevel logging extension
|
* took loglevel's example honouring log levels). Adds a loglevel logging extension
|
||||||
* in the recommended way.
|
* in the recommended way.
|
||||||
*/
|
*/
|
||||||
export function setLogExtension(extension: LogExtensionFunc) {
|
export function setLogExtension(extension: LogExtensionFunc): void {
|
||||||
const originalFactory = logger.methodFactory;
|
const originalFactory = logger.methodFactory;
|
||||||
|
|
||||||
logger.methodFactory = function (methodName, configLevel, loggerName) {
|
logger.methodFactory = function (
|
||||||
|
methodName,
|
||||||
|
configLevel,
|
||||||
|
loggerName,
|
||||||
|
): LoggingMethod {
|
||||||
const rawMethod = originalFactory(methodName, configLevel, loggerName);
|
const rawMethod = originalFactory(methodName, configLevel, loggerName);
|
||||||
|
|
||||||
const logLevel = LogLevel[methodName as LogLevelString];
|
const logLevel = LogLevel[methodName as LogLevelString];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user