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